Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f3b06af54 | ||
|
|
788dae1bf7 | ||
|
|
3812145c74 | ||
|
|
bfc69d5598 | ||
|
|
a946785c7b | ||
|
|
2104d1d54c | ||
|
|
6c1d19704e | ||
|
|
579751f54a | ||
|
|
87e3181a2a | ||
|
|
199d39b347 | ||
|
|
43cbb6bb7f | ||
|
|
8cfb81d071 | ||
|
|
2687450c2d | ||
|
|
796fe5b52f | ||
|
|
4924bb12e8 | ||
|
|
54c1af7edf | ||
|
|
113892ab67 | ||
|
|
5e55c2dbb9 | ||
|
|
c64c199e70 | ||
|
|
aa1da698f4 | ||
|
|
25a731f6c4 | ||
|
|
5a2745987f | ||
|
|
8a364bee5e | ||
|
|
3d2cf44608 | ||
|
|
eb92b49531 | ||
|
|
09d10625c3 | ||
|
|
05aa019738 | ||
|
|
d965aeb886 | ||
|
|
32f65c3ddc | ||
|
|
8fd0483b42 | ||
|
|
029f6d7cb4 | ||
|
|
07a6fa1e55 | ||
|
|
6b52dd790b | ||
|
|
3c015e9266 |
@@ -1,5 +0,0 @@
|
||||
# Golden files and project sources are byte-stable across platforms.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Preserve the upstream font license exactly as committed (it uses CRLF).
|
||||
tools/gendocs/site/static/fonts/IBM-Plex-OFL.txt -text
|
||||
+1
-39
@@ -1,41 +1,3 @@
|
||||
# CI workflows
|
||||
|
||||
Gitea is the source of truth. The Linux workflow runs there; optional Gitea
|
||||
platform runners remain controlled by `PLATFORM_RUNNERS`.
|
||||
|
||||
`github-native.yml` is for the GitHub mirror. It builds and tests on native
|
||||
Windows, macOS (arm64 and amd64), and Linux runners, and uploads unsigned CI
|
||||
artifacts and diagnostic logs. The Windows installer smoke is restricted to an
|
||||
ephemeral GitHub-hosted runner. macOS bundles are ad-hoc signed, not notarized.
|
||||
These jobs are not evidence of platform support until they actually pass on
|
||||
their respective runners.
|
||||
|
||||
On Gitea, the GitHub-only matrix routes to the Linux runner before being
|
||||
skipped. This prevents Windows and macOS entries from waiting indefinitely for
|
||||
runners that Gitea does not have. On GitHub, the actual native labels are used.
|
||||
macOS and Windows build the pinned GLSL reference validator from verified
|
||||
source, rather than depending on a changing package-manager version.
|
||||
|
||||
To show native GitHub results on the Gitea commit, create the GitHub Actions
|
||||
secret `GITEA_STATUS_TOKEN` containing a Gitea API token with repository write
|
||||
permission. Prefer a non-admin bot that has access only to `sanspie/aego`:
|
||||
Gitea's repository-write token is broader than commit statuses. The reporter
|
||||
publishes pending and final statuses with links to the real GitHub jobs only
|
||||
for trusted `master` pushes or manual runs, never for pull requests. If the
|
||||
secret is missing, CI still runs but no Gitea statuses are published.
|
||||
|
||||
Keep GitHub Actions disabled through the first mirror sync: historical tags
|
||||
can contain older workflows. Before enabling Actions, verify mirrored refs and
|
||||
restrict the allowed workflows so historical release jobs cannot run. Use a
|
||||
separate repository-scoped credential for the mirror; do not put it in a URL.
|
||||
Gitea's build-and-publish job remains paused. `github-release-mirror.yml`
|
||||
copies already-published Gitea releases and assets to GitHub after the current
|
||||
master passes Gitea Linux CI and all four real GitHub native jobs. It also
|
||||
checks periodically and can be dispatched manually from master. Versioned
|
||||
stable releases are published normally, with the highest stable version marked
|
||||
Latest; nightly, beta versions, and the stable-channel pointer are prereleases.
|
||||
The mirror verifies matching git refs and asset sizes/checksums, stages new
|
||||
releases as drafts, and never moves tags or replaces existing assets. If a
|
||||
mutable nightly or another mirrored snapshot changes, the mirror stops rather
|
||||
than overwriting it; a separate reviewed update is needed in that case. Signed
|
||||
update manifests are copied unchanged and retain their original download URLs.
|
||||
The workflows in this directory automate checks, builds, tests, and release publishing for Aego. Optional platform-runner jobs cover Windows and macOS alongside the main Linux checks.
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# Native runs on Windows and macOS machines; the Windows build itself is cross-compiled on Linux in ci.yml (D301).
|
||||
smoke:
|
||||
name: ${{ matrix.goos }} ${{ matrix.goarch }} smoke
|
||||
if: github.server_url != 'https://github.com' && vars.PLATFORM_RUNNERS == 'true'
|
||||
if: vars.PLATFORM_RUNNERS == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
+1
-117
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
name: github-release-mirror
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [github-native]
|
||||
types: [completed]
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '17 */6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: github-release-mirror
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
if: github.server_url == 'https://github.com' && github.repository == 'Alexander-D-Karpov/aego' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: refs/heads/master
|
||||
persist-credentials: false
|
||||
- name: Test release mirror
|
||||
run: python3 -m unittest discover -s tools/ci -p mirror_releases_test.py -v
|
||||
- name: Mirror published Gitea releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python3 tools/ci/mirror_releases.py --apply
|
||||
File diff suppressed because it is too large
Load Diff
+4
-9
@@ -182,21 +182,16 @@ func writeArtCapture(path string, out *Output) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("art: cannot create capture: %w", err)
|
||||
}
|
||||
return finishArtCapture(path, f, out.Images[0].Data)
|
||||
}
|
||||
|
||||
// On failure leave the path alone: it may have been replaced while the opened
|
||||
// file was being written. The caller is told that an incomplete file may remain.
|
||||
func finishArtCapture(path string, f io.WriteCloser, data []byte) error {
|
||||
n, err := f.Write(data)
|
||||
if err == nil && n != len(data) {
|
||||
n, err := f.Write(out.Images[0].Data)
|
||||
if err == nil && n != len(out.Images[0].Data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("art: cannot write capture: %w; incomplete output may remain at %q", err, path)
|
||||
_ = os.Remove(path)
|
||||
return fmt.Errorf("art: cannot write capture: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package editors
|
||||
|
||||
import (
|
||||
"aego/app/assembly"
|
||||
"aego/modules/animation"
|
||||
animationeditor "aego/modules/animation/editor"
|
||||
"aego/modules/audio"
|
||||
audioeditor "aego/modules/audio/editor"
|
||||
"aego/modules/camera"
|
||||
@@ -26,7 +24,6 @@ import (
|
||||
|
||||
func With(c assembly.Catalog) assembly.Catalog {
|
||||
return c.
|
||||
With(animation.ID, animationeditor.Hook).
|
||||
With(inputmod.ID, inputeditor.Hook).
|
||||
With(particles.ID, particleseditor.Hook).
|
||||
With(render2d.ID, render2deditor.Hook).
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"aego/contracts/persistence"
|
||||
@@ -20,13 +19,10 @@ func UseMemorySaves(e *engine.Engine) {
|
||||
}
|
||||
|
||||
type memUser struct {
|
||||
mu sync.RWMutex
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
@@ -35,22 +31,16 @@ func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (m *memUser) WriteFile(p string, data []byte) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.files[path.Clean(p)] = append([]byte(nil), data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Remove(p string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.files, path.Clean(p))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return platform.UserEntry{}, fs.ErrNotExist
|
||||
@@ -59,8 +49,6 @@ func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||
}
|
||||
|
||||
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
prefix := path.Clean(dir) + "/"
|
||||
var out []platform.UserEntry
|
||||
for p, data := range m.files {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMemoryUserConcurrentReadWriteAndList(t *testing.T) {
|
||||
m := &memUser{files: map[string][]byte{}}
|
||||
start := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
for worker := range 4 {
|
||||
wg.Go(func() {
|
||||
<-start
|
||||
for i := range 1000 {
|
||||
if worker == 0 {
|
||||
if err := m.WriteFile("saves/slot", []byte{byte(i)}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if i%2 == 0 {
|
||||
if err := m.Remove("saves/slot"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err := m.ReadFile("saves/slot"); err != nil && err != fs.ErrNotExist {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := m.Stat("saves/slot"); err != nil && err != fs.ErrNotExist {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := m.List("saves"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestMemoryUserCopiesBytes(t *testing.T) {
|
||||
m := &memUser{files: map[string][]byte{}}
|
||||
data := []byte("save")
|
||||
if err := m.WriteFile("saves/slot", data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data[0] = 'X'
|
||||
read, err := m.ReadFile("saves/slot")
|
||||
if err != nil || string(read) != "save" {
|
||||
t.Fatalf("write did not retain a copy: %q %v", read, err)
|
||||
}
|
||||
read[0] = 'Y'
|
||||
read, err = m.ReadFile("saves/slot")
|
||||
if err != nil || string(read) != "save" {
|
||||
t.Fatalf("read exposed the stored bytes: %q %v", read, err)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -99,8 +98,7 @@ func TestKeyTracksNativeSources(t *testing.T) {
|
||||
|
||||
func TestLocalReplaces(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
absolute := filepath.VolumeName(root) + "/abs/c"
|
||||
writeFile(t, filepath.Join(root, "go.mod"), fmt.Sprintf(`module game
|
||||
writeFile(t, filepath.Join(root, "go.mod"), `module game
|
||||
|
||||
go 1.27
|
||||
|
||||
@@ -109,9 +107,9 @@ replace aego => ../engine // local engine
|
||||
replace (
|
||||
example.com/a v1.2.3 => ./vendor/a
|
||||
example.com/b => example.com/b v1.0.0
|
||||
example.com/c => %s
|
||||
example.com/c => /abs/c
|
||||
)
|
||||
`, absolute))
|
||||
`)
|
||||
got, err := LocalReplaces(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -119,7 +117,7 @@ replace (
|
||||
want := map[string]bool{
|
||||
filepath.Clean(filepath.Join(root, "../engine")): true,
|
||||
filepath.Clean(filepath.Join(root, "vendor", "a")): true,
|
||||
filepath.Clean(absolute): true,
|
||||
filepath.Clean("/abs/c"): true,
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("replaces = %v", got)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"aego/app/assembly"
|
||||
"aego/app/assembly/builtin"
|
||||
"aego/app/docicons"
|
||||
"aego/asset/loader"
|
||||
"aego/asset/pipeline"
|
||||
"aego/contracts/persistence"
|
||||
@@ -154,7 +153,6 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
||||
ch.Warn("cannot describe the script bindings", log.F("err", derr.Error()))
|
||||
} else {
|
||||
h.Docs = docs.FromManifest(m, h.Types.Schema(), h.Modules)
|
||||
h.Docs.SetNavigationIcons(docicons.Builtin())
|
||||
m.Keywords = h.Docs.Keywords()
|
||||
h.Workspace.SetDocs(m)
|
||||
for _, p := range h.Docs.Problems() {
|
||||
|
||||
+3
-13
@@ -78,21 +78,11 @@ type Bundle struct {
|
||||
func sanitizer() func(string) string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return func(s string) string {
|
||||
return hideHome(s, home)
|
||||
}
|
||||
}
|
||||
|
||||
func hideHome(s, home string) string {
|
||||
if home == "" {
|
||||
if home != "" {
|
||||
s = strings.ReplaceAll(s, home, "~")
|
||||
}
|
||||
return s
|
||||
}
|
||||
// Windows paths can appear with either separator in stack traces and logs.
|
||||
for _, prefix := range [2]string{home, strings.ReplaceAll(home, `\`, "/")} {
|
||||
s = strings.ReplaceAll(s, prefix+`\`, "~/")
|
||||
s = strings.ReplaceAll(s, prefix+"/", "~/")
|
||||
s = strings.ReplaceAll(s, prefix, "~")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Write(b Bundle) (string, error) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package crash
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -11,9 +10,6 @@ import (
|
||||
func TestBundleHidesHome(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Setenv("USERPROFILE", home)
|
||||
}
|
||||
root := filepath.Join(home, "game")
|
||||
dir, err := Write(Bundle{Root: root, Kind: "panic", Where: "panel inspector", Panic: "boom in " + root, Stack: []byte(root + "/x.go:1")})
|
||||
if err != nil {
|
||||
@@ -29,12 +25,3 @@ func TestBundleHidesHome(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHideHomeWithWindowsSeparators(t *testing.T) {
|
||||
home := `C:\Users\Alice`
|
||||
for _, input := range []string{`C:\Users\Alice\game\main.go`, `C:/Users/Alice/game/main.go`} {
|
||||
if got := hideHome(input, home); got != `~/game/main.go` && got != `~/game\main.go` {
|
||||
t.Fatalf("hideHome(%q) = %q", input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Package docicons holds the built-in documentation icon policy at the
|
||||
// application composition boundary. Modules and editor panels do not depend on
|
||||
// this policy; it is supplied to the documentation registry by the host.
|
||||
package docicons
|
||||
|
||||
import "maps"
|
||||
|
||||
// These maps mirror tools/gendocs/site/icons.go; the parity test checks every
|
||||
// key and value, including module-owned guides, API packages and settings.
|
||||
var taskIcons = map[string]string{
|
||||
"install": "file.download", "quickstart": "doc.project", "platformer": "doc.tutorial",
|
||||
"platformer-effects": "render2d.point_light", "credits": "help.about", "recipes": "doc.recipe",
|
||||
"editor": "view.dock", "editor/agent": "panel.agent", "agents": "panel.agent",
|
||||
"editor/icons": "view.grid", "cli": "panel.console", "diagnostics": "panel.problems",
|
||||
"curves": "ui.curve", "tween": "code.timer", "animation": "animation.player",
|
||||
"animation/animator": "animation.animator", "animation/controller": "anim.graph",
|
||||
"animation/clips": "anim.key", "animation/events": "anim.event", "animation/time": "play.speed",
|
||||
"tween/timers": "code.timer", "render2d/materials": "file.preset",
|
||||
"render2d/normal-mapping": "render2d.normals", "render2d/art": "file.art",
|
||||
"render2d/image-viewer": "file.texture", "render2d.PostProcess": "view.overlays",
|
||||
"camera.CameraBrain2D": "view.split", "formats/overview": "file.data",
|
||||
"formats/aeanim": "anim.key", "formats/aectrl": "anim.graph",
|
||||
}
|
||||
|
||||
var refIcons = map[string]string{
|
||||
"aego/builtin": "cat.node", "events": "panel.events", "aego/action": "panel.tasks", "aego/audio": "cat.audio",
|
||||
"aego/camera": "cat.camera", "aego/curve": "ui.curve", "aego/data": "file.data", "aego/debug": "panel.debug_draw",
|
||||
"aego/input": "panel.input_devices", "aego/log": "panel.console", "aego/math": "cat.transform", "aego/physics": "cat.physics",
|
||||
"aego/rand": "shape.dice", "aego/save": "panel.saves", "aego/scene": "file.scene", "aego/settings": "cat.settings",
|
||||
"aego/testing": "status.ok", "aego/time": "play.speed", "aego/timer": "code.timer", "aego/tween": "vec.velocity",
|
||||
"section/engine": "panel.settings", "section/input": "panel.input_devices", "section/render2d": "cat.rendering",
|
||||
"section/physics": "cat.physics", "section/debug": "panel.debug_draw", "section/player": "play.start", "section/audio": "cat.audio",
|
||||
"section/animation": "animation.player", "section/art": "file.art", "section/update": "file.download",
|
||||
}
|
||||
|
||||
// Builtin returns independent task and reference maps for a host to install.
|
||||
func Builtin() (tasks, references map[string]string) {
|
||||
return maps.Clone(taskIcons), maps.Clone(refIcons)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package docicons
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuiltinReturnsIndependentPolicies(t *testing.T) {
|
||||
tasks, refs := Builtin()
|
||||
tasks["animation"] = "wrong"
|
||||
refs["aego/audio"] = "wrong"
|
||||
otherTasks, otherRefs := Builtin()
|
||||
if otherTasks["animation"] != "animation.player" || otherRefs["aego/audio"] != "cat.audio" {
|
||||
t.Fatal("one host changed the built-in policy for another")
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"aego/app/docicons"
|
||||
"aego/core/log"
|
||||
"aego/engine"
|
||||
"aego/project"
|
||||
)
|
||||
|
||||
func TestEditorHostInstallsDocumentationIconPolicy(t *testing.T) {
|
||||
p, err := project.Create(filepath.Join(t.TempDir(), "project"), "Docs icons", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := Build(p, Config{Engine: engine.Config{Headless: true}, Logger: log.Nop(), Docs: true, Editor: true, NoProfile: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer h.Close()
|
||||
if h.Docs == nil {
|
||||
t.Fatal("editor host did not build its documentation registry")
|
||||
}
|
||||
tasks, refs := docicons.Builtin()
|
||||
for id, want := range tasks {
|
||||
if got := h.Docs.NavigationTaskIcon(id); got != want {
|
||||
t.Errorf("host task %q = %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
for id, want := range refs {
|
||||
if got := h.Docs.NavigationReferenceIcon(id); got != want {
|
||||
t.Errorf("host reference %q = %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"animation", "animation/animator", "render2d.PostProcess", "camera.CameraBrain2D", "aego/audio", "aego/timer", "section/animation"} {
|
||||
if _, ok := h.Docs.Get(id); !ok {
|
||||
t.Errorf("editor host lacks module documentation entry %q", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"aego/app/assembly"
|
||||
"aego/app/assembly/builtin"
|
||||
"aego/app/docicons"
|
||||
"aego/app/playground"
|
||||
"aego/engine/docs"
|
||||
"aego/engine/module"
|
||||
@@ -61,7 +60,6 @@ func build(p *project.Project, mods []module.Module, pg *playground.Playground)
|
||||
src.Project = p.Name
|
||||
}
|
||||
r := docs.Assemble(src)
|
||||
r.SetNavigationIcons(docicons.Builtin())
|
||||
localize(r)
|
||||
bm.Keywords = r.Keywords()
|
||||
if ws != nil {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package docstool
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// siteHandler serves the generator's clean URLs, never a directory inventory.
|
||||
// A guide can have both physics.html and a physics/ directory of child pages.
|
||||
func siteHandler(root string) http.Handler {
|
||||
files := http.FileServer(http.Dir(root))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clean := path.Clean("/" + r.URL.Path)
|
||||
local := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(clean, "/")))
|
||||
if info, err := os.Stat(local + ".html"); err == nil && !info.IsDir() {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
target := (&url.URL{Path: clean, RawQuery: r.URL.RawQuery}).String()
|
||||
http.Redirect(w, r, target, http.StatusPermanentRedirect)
|
||||
return
|
||||
}
|
||||
copy := r.Clone(r.Context())
|
||||
copy.URL.Path, copy.URL.RawPath = clean+".html", ""
|
||||
files.ServeHTTP(w, copy)
|
||||
return
|
||||
}
|
||||
if info, err := os.Stat(local); err == nil && info.IsDir() {
|
||||
if index, err := os.Stat(filepath.Join(local, "index.html")); err != nil || index.IsDir() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
files.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package docstool
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDocsServerCleanURLsDoNotExposeDirectoryListings(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for name, content := range map[string]string{"physics.html": "<h1>Physics</h1>", "physics/units.html": "<h1>Units</h1>", "play/index.html": "<h1>Playground</h1>", "hidden/file.txt": "private inventory"} {
|
||||
file := filepath.Join(root, name)
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(file, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, item := range []struct {
|
||||
path string
|
||||
status int
|
||||
text, location string
|
||||
}{
|
||||
{"/physics", 200, "<h1>Physics</h1>", ""},
|
||||
{"/physics/", 308, "", "/physics"},
|
||||
{"/physics/?test=1", 308, "", "/physics?test=1"},
|
||||
{"/physics/units", 200, "<h1>Units</h1>", ""},
|
||||
{"/play/", 200, "<h1>Playground</h1>", ""},
|
||||
{"/hidden/", 404, "", ""},
|
||||
} {
|
||||
r := httptest.NewRecorder()
|
||||
siteHandler(root).ServeHTTP(r, httptest.NewRequest(http.MethodGet, item.path, nil))
|
||||
if r.Code != item.status || !strings.Contains(r.Body.String(), item.text) || r.Header().Get("Location") != item.location || strings.Contains(r.Body.String(), "Directory listing") {
|
||||
t.Errorf("%s: status=%d location=%q body=%s", item.path, r.Code, r.Header().Get("Location"), r.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-91
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -85,8 +85,6 @@ type App struct {
|
||||
textOn bool
|
||||
shown *engine.World
|
||||
closed bool
|
||||
control ControlState
|
||||
viewport Viewport
|
||||
}
|
||||
|
||||
func noBlock(string) *aetext.Node {
|
||||
@@ -348,7 +346,7 @@ func (a *App) showPhysics() {
|
||||
func (a *App) Update(dt float64) {
|
||||
e := a.e
|
||||
now := e.Platform().Now()
|
||||
a.tickSession(dt)
|
||||
a.session.Tick(dt)
|
||||
a.showPhysics()
|
||||
ui := a.ui
|
||||
ui.Time = now
|
||||
@@ -363,7 +361,6 @@ func (a *App) Update(dt float64) {
|
||||
a.toolbar(bar)
|
||||
body := mathx.Rect{Y: bar.H, W: size.X, H: max(0, size.Y-bar.H)}
|
||||
gameR, inspR := layout(body, 1)
|
||||
a.viewport = Viewport{X: gameR.X, Y: gameR.Y, Width: gameR.W, Height: gameR.H, CanvasWidth: m.LogicalW, CanvasHeight: m.LogicalH}
|
||||
a.panel(&frame, gameR, a.game.Draw)
|
||||
a.panel(&frame, inspR, a.insp.Draw)
|
||||
ctx.DnD.Frame(ui)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/modules/audio"
|
||||
)
|
||||
|
||||
func TestAudioListenerTryDisplaysEffectivePositionAndReset(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("audio.AudioListener2D")
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("audio listener contract is absent")
|
||||
}
|
||||
code, err := p.TryCode(e.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply := a.Call("load", req); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := func(want float32) {
|
||||
t.Helper()
|
||||
sc := a.session.Controller().Scene()
|
||||
marker := sc.Find(sc.Root(), "Effective listener")
|
||||
if !sc.IsAlive(marker) {
|
||||
t.Fatal("listener indicator is absent")
|
||||
}
|
||||
service := audio.Of(a.session.Controller().World())
|
||||
if service == nil || service.Listener().X != want || sc.WorldPosition(marker).X != want {
|
||||
t.Fatalf("marker does not reflect actual audio listener position %v", want)
|
||||
}
|
||||
r := a.Call("inspect", []byte(`{"component":"audio.AudioListener2D","fields":["audio.AudioListener2D.ListenerX"]}`))
|
||||
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["audio.AudioListener2D.ListenerX"] != float64(want) {
|
||||
t.Fatalf("effective listener position is not observable: %+v", r)
|
||||
}
|
||||
}
|
||||
check(2)
|
||||
if r := a.Call("set", []byte(`{"component":"audio.AudioListener2D","field":"Active","value":0}`)); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(0)
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(2)
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("listener Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/asset"
|
||||
"aego/modules/audio"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
// The example requests a real looped voice at volume zero. The color reads the
|
||||
// audio service's actual SourcePlaying state, not the editable checkbox.
|
||||
func TestAudioSourceTryStartsRealSilentVoiceAndResets(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("audio.AudioSource2D")
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("audio source contract is absent")
|
||||
}
|
||||
code, err := p.TryCode(e.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply := a.Call("load", req); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := func() (*audio.AudioSource2D, *render2d.SpriteRenderer, bool) {
|
||||
t.Helper()
|
||||
sc := a.session.Controller().Scene()
|
||||
h := sc.Find(sc.Root(), "Source")
|
||||
src, ok := scene.Get[audio.AudioSource2D](sc, h)
|
||||
if !ok || src.Clip.ID != asset.BuiltinHum || src.Volume != 0 || !src.Loop {
|
||||
t.Fatal("expected a silent looping builtin source")
|
||||
}
|
||||
marker, ok := scene.Get[render2d.SpriteRenderer](sc, h)
|
||||
if !ok {
|
||||
t.Fatal("audio source indicator is absent")
|
||||
}
|
||||
service := audio.Of(a.session.Controller().World())
|
||||
if service == nil {
|
||||
t.Fatal("audio service is absent")
|
||||
}
|
||||
return src, marker, service.SourcePlaying(h)
|
||||
}
|
||||
src, marker, playing := state()
|
||||
idleColor := marker.Tint
|
||||
if src.PlayOnStart || src.Playing || playing {
|
||||
t.Fatal("source should start without a voice")
|
||||
}
|
||||
if reply := a.Call("inspect", []byte(`{"component":"audio.AudioSource2D","fields":["audio.AudioSource2D.Playing"],"dependencies":["ae900000000000000000000000000a03"]}`)); reply.Error != "" || reply.Inspection == nil || len(reply.Inspection.Dependencies) != 1 {
|
||||
t.Fatalf("the actual builtin audio dependency was not resolved: %+v", reply)
|
||||
}
|
||||
if reply := a.Call("set", []byte(`{"component":"audio.AudioSource2D","field":"PlayOnStart","value":1}`)); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, marker, playing = state()
|
||||
if !src.PlayOnStart || !src.Playing || !playing || marker.Tint == idleColor {
|
||||
t.Fatalf("real silent voice did not start: enabled=%v playing=%v service=%v tint=%v", src.PlayOnStart, src.Playing, playing, marker.Tint)
|
||||
}
|
||||
if reply := a.Call("reset", nil); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, marker, playing = state()
|
||||
if src.PlayOnStart || src.Playing || playing || marker.Tint != idleColor {
|
||||
t.Fatal("Reset did not stop the voice and restore the indicator")
|
||||
}
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("audio Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/engine"
|
||||
"aego/modules/audio"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
func TestAudioZoneTryChangesRealWorldBusAndResets(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("audio.AudioZone2D")
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("audio zone contract is absent")
|
||||
}
|
||||
code, err := p.TryCode(e.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(16); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := func(want float64) *render2d.SpriteRenderer {
|
||||
t.Helper()
|
||||
world := a.session.Controller().World()
|
||||
mod, ok := engine.ServiceOf[*audio.Module](world, audio.MixerID)
|
||||
if !ok || mod.Mixer() == nil || math.Abs(float64(mod.Mixer().BusGain(int(audio.BusMusic)))-want) > 0.001 {
|
||||
t.Fatalf("actual mixer music bus did not reach gain %v", want)
|
||||
}
|
||||
r := a.Call("inspect", []byte(`{"component":"audio.AudioZone2D","fields":["audio.AudioZone2D.WorldGain"]}`))
|
||||
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["audio.AudioZone2D.WorldGain"] != want {
|
||||
t.Fatalf("runtime world gain is not observable: %+v", r)
|
||||
}
|
||||
sc := a.session.Controller().Scene()
|
||||
h := sc.Find(sc.Root(), "Indicator")
|
||||
marker, ok := scene.Get[render2d.SpriteRenderer](sc, h)
|
||||
if !ok {
|
||||
t.Fatal("gain indicator is absent")
|
||||
}
|
||||
return marker
|
||||
}
|
||||
before := check(0.25).Tint
|
||||
if r := a.Call("set", []byte(`{"component":"audio.AudioZone2D","field":"Gain","value":0.75}`)); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(16); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if marker := check(0.75); marker.Tint == before {
|
||||
t.Fatal("indicator did not follow the actual world gain")
|
||||
}
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(16); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if marker := check(0.25); marker.Tint != before {
|
||||
t.Fatal("Reset did not restore the gain indicator")
|
||||
}
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("audio zone Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
+2
-63
File diff suppressed because it is too large
Load Diff
@@ -1,83 +0,0 @@
|
||||
//go:build aego_editor && aego_gltest && linux && cgo && !aego_nonative
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/modules/camera"
|
||||
)
|
||||
|
||||
// In addition to the rendered-frame contract, check the actual camera service
|
||||
// so a moving decoration cannot stand in for a changing camera pose.
|
||||
func TestCameraShakeComponentDemoDrivesRealCamera(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entry, ok := p.Docs().Get("camera.CameraShake2D")
|
||||
if !ok || entry.Demo == nil || len(entry.Demo.Actions) != 1 {
|
||||
t.Fatal("camera shake contract is absent")
|
||||
}
|
||||
ctx := context.Background()
|
||||
opened, err := (&nativeTryRunner{p: p}).Open(ctx, *entry.Demo, "native")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := opened.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
instance := opened.(*nativeTryInstance)
|
||||
service := func() *camera.Service {
|
||||
t.Helper()
|
||||
s := camera.Of(instance.app.session.Controller().World())
|
||||
if s == nil || s.Trauma("") < 0.99 {
|
||||
t.Fatal("the real camera service did not receive the hit")
|
||||
}
|
||||
return s
|
||||
}
|
||||
checkStill := func() {
|
||||
t.Helper()
|
||||
s := service()
|
||||
if diff := math.Abs(float64(s.Render("").Rotation - s.Logical("").Rotation)); diff > 1e-6 {
|
||||
t.Fatalf("zero Max Angle unexpectedly rotated the view: %g", diff)
|
||||
}
|
||||
}
|
||||
checkStill()
|
||||
if err := instance.Action(ctx, entry.Demo.Actions[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
world := instance.app.session.Controller().World()
|
||||
startTick := world.Time.Tick()
|
||||
var peak float64
|
||||
for range 60 {
|
||||
// Presentation frames do not advance a controlled Try world. Advance
|
||||
// each simulation frame explicitly before sampling its camera pose.
|
||||
if err := instance.advance(ctx, 1, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := service()
|
||||
peak = max(peak, math.Abs(float64(s.Render("").Rotation-s.Logical("").Rotation)))
|
||||
}
|
||||
if world.Time.Tick() <= startTick {
|
||||
t.Fatal("camera samples did not advance the Try simulation")
|
||||
}
|
||||
if peak < 0.01 {
|
||||
t.Fatalf("changing Max Angle did not move the real camera pose: peak %g", peak)
|
||||
}
|
||||
if err := instance.Reset(ctx, entry.Demo.Reset); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkStill()
|
||||
if messages := errorsOf(instance.app); len(messages) != 0 {
|
||||
t.Fatalf("Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/modules/gameplay"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
// The indicator follows the target's actual Health after the real gameplay
|
||||
// system processes the overlap; changing the edited field alone is not enough.
|
||||
func TestCombatComponentTriesDealDamageAndReset(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
id, field string
|
||||
value int
|
||||
wantHP int32
|
||||
}{
|
||||
{"gameplay.Hitbox2D", "Damage", 3, 7},
|
||||
{"gameplay.Hurtbox2D", "Multiplier", 2, 6},
|
||||
} {
|
||||
t.Run(tc.id, func(t *testing.T) {
|
||||
e, ok := p.Docs().Get(tc.id)
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("component contract is absent")
|
||||
}
|
||||
code, err := p.TryCode(e.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply := a.Call("load", req); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := func() (*gameplay.Health, *render2d.SpriteRenderer) {
|
||||
t.Helper()
|
||||
sc := a.session.Controller().Scene()
|
||||
target := sc.Find(sc.Root(), "Target")
|
||||
hp, ok := scene.Get[gameplay.Health](sc, target)
|
||||
if !ok {
|
||||
t.Fatal("target Health is absent")
|
||||
}
|
||||
marker, ok := scene.Get[render2d.SpriteRenderer](sc, target)
|
||||
if !ok {
|
||||
t.Fatal("target indicator is absent")
|
||||
}
|
||||
return hp, marker
|
||||
}
|
||||
hp, marker := state()
|
||||
initialColor := marker.Tint
|
||||
if hp.Max != 10 || hp.Current != 10 || hp.Dead {
|
||||
t.Fatalf("initial target health: %+v", hp)
|
||||
}
|
||||
payload := fmt.Sprintf(`{"component":%q,"field":%q,"value":%d}`, tc.id, tc.field, tc.value)
|
||||
if reply := a.Call("set", []byte(payload)); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hp, marker = state()
|
||||
if hp.Current != tc.wantHP || hp.Dead || marker.Tint == initialColor {
|
||||
t.Fatalf("the actual hit and indicator disagree: health=%d want=%d tint=%v", hp.Current, tc.wantHP, marker.Tint)
|
||||
}
|
||||
if reply := a.Call("reset", nil); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hp, marker = state()
|
||||
if hp.Current != 10 || hp.Dead || marker.Tint != initialColor {
|
||||
t.Fatalf("Reset did not restore target health and indicator: health=%d tint=%v", hp.Current, marker.Tint)
|
||||
}
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("combat Try runtime errors: %v", messages)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,32 +24,13 @@ func TestComponentTryGuidesPreserveAuthoredScenes(t *testing.T) {
|
||||
{"camera/outputs", "camera.CameraBrain2D", "d475b935c8c41973653b44aa1ff75d4871263a0ad2cb9d56097f234818be1aa4", ""},
|
||||
{"camera/virtual", "camera.VirtualCamera2D", "14e775168e8f7f5250d42970db4578d39219a9e3bb9d0dab839892fd92ae88a8", "af3febb779542a8605bd2899667e87d5cf81826ca4d15efcbab85a0c0efcffab"},
|
||||
{"physics", "physics.Body2D", "87081fb3f66fb919ef02833f30d052457b456c841b2b6406c9930ad47884644c", ""},
|
||||
{"physics/components", "physics.CircleCollider2D", "a572890607943e3af0727666c0501e605543dc28a0a9398777ea2fe987e4e213", ""},
|
||||
{"gameplay/hitbox", "gameplay.Hitbox2D", "06cebaff0c35ea72d0a8a9112bbbe59eacca4016841c92e25bbe7b6fc8354f35", "10b0b317c79134c3d333f17a42381db8e314e343058a8dcfe7d2806f88da5b1c"},
|
||||
{"gameplay/hurtbox", "gameplay.Hurtbox2D", "4cb707ab73abcf3f50af599dc7a6313837044230c95f48d83809ff33d8d3d2a4", "e7a7ad3733ab9f989f50b6f20fed6ed6b9a3cff635c9d04808f336452e287b9e"},
|
||||
{"gameplay/killzone", "gameplay.KillZone2D", "ea7ce3acf87237e86e2988a93951ccdfb762b7fe6622ab1a6e84ad3f7ba10fb9", "fe5d7ad926cfc8029e9451992285748c197ef5a018b4eba023a30a74b93d0ecf"},
|
||||
{"gameplay/pickup", "gameplay.Pickup2D", "685494b9be74b228fc2762e4cfc870cda000f83afe05c814ddfd190e4ecbbaab", "7b038ac87d73a29ee2044b8ffc66f44b681538f5457631fea471df8808ddd33a"},
|
||||
{"gameplay/checkpoint", "gameplay.Checkpoint2D", "9d35c26516469c4180cfbdb94edf524d8b54f47f2c897a946e93b829152a974f", "951d0c477e0baeeced3c7a64093e324d30588844af32f952279985cf9a62fa61"},
|
||||
{"gameplay/respawn", "gameplay.Respawn", "c604b98824cc3483b506e1f529e51159cbe8d3c8e058a37eb437c0e35890089a", "d13601abe19642eee5a4f3cd7056e0812dc93f5f59f6e904c972d1e7b09cc260"},
|
||||
{"audio", "audio.AudioSource2D", "0c43758ee5bf1ad7304668d36b138a3f417dc795e56730d2226ca935e204cf7e", "1f0bde6059abe3241de2e799bd9e3e06f5a4259176bb636ba3e0d60fa7d0750c"},
|
||||
{"audio/spatial", "audio.AudioListener2D", "3ffe126eea8b20663538fd4e1b2d083c9157edd2f7adfbe983bfec962229ff69", "2e79adf7b30530df3479608124ec67678cf9ef7847b570866731cc402652cac3"},
|
||||
{"audio/zones", "audio.AudioZone2D", "2181dfaf04ca60b26b93a263c58d686423babf61139b9662b8502cd67ba2228c", "994b455b9af397862a58d284f763fd26b05aa4d51e80965464c4ff9379f80c58"},
|
||||
{"save", "save.Persist", "19499400ad68dded38129ccec04546a5b018add92114f2e21f0c8bf6f61506cb", "8cde05fe8d4c3f6e6b722ff60b8be7a687160e50fefd5a09001f25a76299675a"},
|
||||
{"tags/example", "engine.Tags", "e642c5c9983312c9f75b7966b3a9ecfeb2f08196f2cb7ebe9e3bf0adc24bdbbe", "a6459199adcbbd52aab200b5c04e331b478b4e1b4b0ddb6a688e7028416a5215"},
|
||||
{"language/script-example", "engine.Script", "8927658496fa7c6afc6a77ba8818018faa5b93936c8a32a13bd72f5ec7f59d6d", "18f02ddc587e8719b73b9d68d749f533be2290512075403a1560191c05655ca8"},
|
||||
{"render2d/materials", "engine.MaterialOverride", "c0a63c2b8cfdf22028ba1df4c7152a75c1f69a973c70175ec44f476aadf19405", "b5c1b4249a9b3232b9845f2a61b2cf2a5af48c3d9d065bf95f7ff877305de3b3"},
|
||||
{"prefabs/example", "engine.PrefabInstance", "90289773f897db2a386ad6cf9380dbc874f03f6f0cb76a03d2c2d08a1de833da", "5e258045feacf7591993ce7397df8aed9e5247d776fe2d2055b78b55571d1ea1"},
|
||||
} {
|
||||
t.Run(spec.guide, func(t *testing.T) {
|
||||
guide, ok := p.Docs().Get(spec.guide)
|
||||
if !ok {
|
||||
t.Fatal("guide not found")
|
||||
}
|
||||
wantSelect := spec.component
|
||||
if spec.guide == "render2d/materials" {
|
||||
wantSelect = "" // This authored guide never specified a component selection.
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.Try))); got != spec.sum || guide.TrySelect != wantSelect {
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.Try))); got != spec.sum || guide.TrySelect != spec.component {
|
||||
t.Fatalf("authored guide scene or selection changed: sha=%s select=%s", got, guide.TrySelect)
|
||||
}
|
||||
if spec.script != "" {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/modules/physics"
|
||||
"aego/modules/physics/components"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
func TestDistanceTryChangesRealJointAndResets(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("physics.DistanceJoint2D")
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("Distance Joint component contract is absent")
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := func() (*components.DistanceJoint2D, float32) {
|
||||
t.Helper()
|
||||
sc := a.session.Controller().Scene()
|
||||
bob := sc.Find(sc.Root(), "Bob")
|
||||
anchor := sc.Find(sc.Root(), "Anchor")
|
||||
joint, ok := scene.Get[components.DistanceJoint2D](sc, bob)
|
||||
if !ok || !sc.IsAlive(anchor) {
|
||||
t.Fatal("distance joint or anchor is absent")
|
||||
}
|
||||
sim := physics.SimOf(a.session.Controller().World())
|
||||
if sim == nil || sim.Stats().Joints != 1 {
|
||||
t.Fatal("the physics world did not create the distance joint")
|
||||
}
|
||||
return joint, sc.WorldPosition(bob).Sub(sc.WorldPosition(anchor)).Len()
|
||||
}
|
||||
joint, distance := state()
|
||||
if joint.Length != 4 || math.Abs(float64(distance-4)) > 0.05 {
|
||||
t.Fatalf("initial joint length=%v distance=%v", joint.Length, distance)
|
||||
}
|
||||
if r := a.Call("set", []byte(`{"component":"physics.DistanceJoint2D","field":"Length","value":2}`)); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(30); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joint, distance = state()
|
||||
if joint.Length != 2 || math.Abs(float64(distance-2)) > 0.1 {
|
||||
t.Fatalf("the real joint did not shorten: length=%v distance=%v", joint.Length, distance)
|
||||
}
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joint, distance = state()
|
||||
if joint.Length != 4 || math.Abs(float64(distance-4)) > 0.05 {
|
||||
t.Fatalf("Reset did not restore the joint: length=%v distance=%v", joint.Length, distance)
|
||||
}
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("Distance Joint Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
+2
-179
File diff suppressed because it is too large
Load Diff
@@ -8,22 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/asset"
|
||||
)
|
||||
|
||||
func TestTryAudioDependencyRequiresPlayableArtifact(t *testing.T) {
|
||||
raw, ok := asset.BuiltinAudio(asset.BuiltinHum)
|
||||
if !ok || !validTryAudio(raw) {
|
||||
t.Fatal("the real builtin Hum was not resolved and decoded")
|
||||
}
|
||||
broken := asset.EncodeAudio(&asset.AudioArtifact{Rate: 32000, Channels: 1, Frames: 100, Data: []byte("not audio")})
|
||||
for _, data := range [][]byte{nil, raw[:20], broken} {
|
||||
if validTryAudio(data) {
|
||||
t.Fatal("invalid or truncated audio dependency was accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryInspectionReadsSelectedRunningComponent(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
@@ -54,12 +40,6 @@ func TestTryInspectionReadsSelectedRunningComponent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
inspect(1)
|
||||
// Animation dependencies are resolved through the real asset loader too;
|
||||
// merely finding a builtin ID must not stand in for loading its resource.
|
||||
animDeps := []byte(`{"component":"engine.SpriteRenderer","dependencies":["ae900000000000000000000000000b10","ae900000000000000000000000000b20"]}`)
|
||||
if r := a.Call("inspect", animDeps); r.Error != "" || r.Inspection == nil || len(r.Inspection.Dependencies) != 2 {
|
||||
t.Fatalf("cannot resolve the actual animation and controller: %+v", r)
|
||||
}
|
||||
before := string(a.session.Text())
|
||||
inspect(1)
|
||||
if string(a.session.Text()) != before || a.session.State().CanUndo {
|
||||
@@ -90,57 +70,3 @@ func TestTryInspectionReadsSelectedRunningComponent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryPrefabDependencyMustParseAndExpand(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entry, ok := p.Docs().Get("engine.SpriteRenderer")
|
||||
if !ok {
|
||||
t.Fatal("SpriteRenderer Try is unavailable")
|
||||
}
|
||||
prefab, ok := p.Docs().Get("engine.PrefabInstance")
|
||||
if !ok {
|
||||
t.Fatal("PrefabInstance Try is unavailable")
|
||||
}
|
||||
code, err := p.TryCode(prefab.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const id = "d5600000000000000000000000000f02"
|
||||
const nested = "d5600000000000000000000000000f03"
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
sources map[string]string
|
||||
valid bool
|
||||
}{
|
||||
{"actual prefab", map[string]string{id: prefab.TryPrefabs[id]}, true},
|
||||
{"valid nested prefab", map[string]string{id: `prefab version=1 { node "d5600000000000000000000000000311" prefab=asset("` + nested + `") {} }`, nested: prefab.TryPrefabs[id]}, true},
|
||||
{"truncated source", map[string]string{id: "prefab version=1 {"}, false},
|
||||
{"missing nested root", map[string]string{id: `prefab version=1 { node "d5600000000000000000000000000311" prefab=asset("d5600000000000000000000000000fff") {} }`}, false},
|
||||
{"missing nested child", map[string]string{id: `prefab version=1 { node "d5600000000000000000000000000311" name="root" { node "d5600000000000000000000000000312" prefab=asset("d5600000000000000000000000000fff") {} } }`}, false},
|
||||
{"prefab cycle", map[string]string{id: `prefab version=1 { node "d5600000000000000000000000000311" prefab=asset("` + id + `") {} }`}, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": entry.Try, "select": entry.TrySelect, "code": code, "prefabs": tc.sources, "artifacts": entry.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := a.Call("inspect", []byte(`{"component":"engine.SpriteRenderer","dependencies":["d5600000000000000000000000000f02"]}`))
|
||||
if tc.valid && (r.Error != "" || r.Inspection == nil || len(r.Inspection.Dependencies) != 1) {
|
||||
t.Fatalf("valid prefab was not resolved: %+v", r)
|
||||
}
|
||||
if !tc.valid && r.Error == "" {
|
||||
t.Fatal("an invalid prefab was accepted as a resolved dependency")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
func TestMaterialOverrideTryChangesOnlyOneRealInstanceAndResets(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("engine.MaterialOverride")
|
||||
if !ok || e.Demo == nil {
|
||||
t.Fatal("MaterialOverride contract is absent")
|
||||
}
|
||||
code, err := p.TryCode(e.TryScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := newHeadless(t)
|
||||
req, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
check := func(want float64, explicit bool) {
|
||||
t.Helper()
|
||||
if err := a.Step(12); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sc := a.session.Controller().Scene()
|
||||
target, reference := sc.Find(sc.Root(), "Flash instance"), sc.Find(sc.Root(), "Reference instance")
|
||||
m1, err := render2d.NodeMaterial(sc, target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m2, err := render2d.NodeMaterial(sc, reference)
|
||||
if err != nil || m1 != m2 {
|
||||
t.Fatalf("both instances must use the same resolved material: %v", err)
|
||||
}
|
||||
value, err := render2d.GetMaterialParameter(sc, target, "Flash")
|
||||
if err != nil || value.F != want {
|
||||
t.Fatalf("actual target Flash=%v, want %v: %v", value.F, want, err)
|
||||
}
|
||||
b, err := render2d.GetMaterialParameter(sc, reference, "Flash")
|
||||
if err != nil || b.F != 0 {
|
||||
t.Fatalf("shared material/reference changed: %v, %v", b.F, err)
|
||||
}
|
||||
o, ok := scene.Get[render2d.MaterialOverride](sc, target)
|
||||
if !ok || o.ExplicitFor(m1, "Flash") != explicit {
|
||||
t.Fatalf("actual per-node override explicit=%v", explicit)
|
||||
}
|
||||
lanes, err := o.Lanes(m1)
|
||||
param, found := m1.Shader().Schema.Param("Flash")
|
||||
if err != nil || !found || param.Lane < 0 || param.Comp < 0 || param.Lane*4+param.Comp >= len(lanes) || lanes[param.Lane*4+param.Comp] != float32(want) {
|
||||
t.Fatalf("shader lanes do not contain the instance value: %v", err)
|
||||
}
|
||||
r := a.Call("inspect", []byte(`{"component":"engine.MaterialOverride","fields":["engine.MaterialOverride.FlashValue","engine.MaterialOverride.FlashExplicit","engine.MaterialOverride.ReferenceFlash"]}`))
|
||||
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["engine.MaterialOverride.FlashValue"] != want || r.Inspection.Observables["engine.MaterialOverride.FlashExplicit"] != explicit || r.Inspection.Observables["engine.MaterialOverride.ReferenceFlash"] != float64(0) {
|
||||
t.Fatalf("actual material inspection: %+v", r)
|
||||
}
|
||||
}
|
||||
check(0, false)
|
||||
if r := a.Call("scriptEnabled", []byte(`{"script":"game.demo.SetFlash","enabled":true}`)); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
check(1, true)
|
||||
if r := a.Call("undo", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
check(0, false)
|
||||
if r := a.Call("redo", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
check(1, true)
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
check(0, false)
|
||||
if messages := errorsOf(a); len(messages) != 0 {
|
||||
t.Fatalf("MaterialOverride Try runtime errors: %v", messages)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user