Author SHA1 Message Date
sanspie 2f3b06af54 docs: exercise camera outputs and box collider Try 2026-11-07 13:05:26 +03:00
sanspie 788dae1bf7 docs: add camera and rigid body Try contracts 2026-11-07 12:22:07 +03:00
sanspie 3812145c74 test: run pinned native and browser art parity 2026-11-07 11:28:49 +03:00
sanspie bfc69d5598 docs: add executable VirtualCamera2D Try contract 2026-11-07 11:28:28 +03:00
sanspie a946785c7b docs: link component references to Studio lessons 2026-11-07 11:28:13 +03:00
sanspie 2104d1d54c test: preserve screenshot markers in native composer workflow 2026-11-07 11:27:52 +03:00
sanspie 6c1d19704e fix: satisfy release HTTP lint checks 2026-11-07 10:38:41 +03:00
sanspie 579751f54a Integrate pinned Asset Studio web release 2026-11-07 10:36:17 +03:00
sanspie 87e3181a2a fix: align art source frames and add workspace goldens 2026-11-07 09:25:36 +03:00
sanspie 199d39b347 docs: follow Go error message conventions 2026-11-07 08:26:54 +03:00
sanspie 43cbb6bb7f art: require consent before lossy web imports
Do not open an Aseprite document with omitted metadata until the browser user reviews and accepts diagnostics; exercise cancel and confirmation through the real UI.
2026-11-07 08:16:51 +03:00
sanspie 8cfb81d071 docs: verify PostProcess output Try
Exercise the real prepared Bloom scene, resolve its dependencies and check runtime and image Reset in native and Chromium.
2026-11-07 08:10:24 +03:00
sanspie 2687450c2d docs: publish Art Studio as a lazy site entry
Build the existing Go/WASM studio with version-bound browser resources and link it from English and Russian site navigation without loading it on docs pages.
2026-11-07 08:10:07 +03:00
sanspie 796fe5b52f scene: detach nested prefab override values
Clone nested lists, structs and curves in orphan properties and added components so repeated snapshot restores cannot alias live edits.
2026-11-07 08:09:44 +03:00
sanspie 4924bb12e8 docs: bind shared Try contracts only to components
Allow guide pages to reuse the same scene without claiming its behavioral sidecar; continue rejecting wrong or duplicate component contracts.
2026-11-07 08:03:41 +03:00
sanspie 54c1af7edf scene: clone frequent snapshot components directly
Detach mutable prefab, particle, occluder and edge data with typed copies; test repeated restore, snapshot reuse and mutable-field coverage.
2026-11-07 07:25:51 +03:00
sanspie 113892ab67 vm: skip slice traversal for string-only snapshots
Track live slice objects through allocation, collection and restore, while keeping independent heap snapshots and the existing slice-capacity semantics.
2026-11-07 07:22:55 +03:00
sanspie 5e55c2dbb9 docs: verify particle emission Try
Use a prewarmed emitter and observe the real live-particle count and Game View before and after Reset on native and Chromium.
2026-11-07 07:20:51 +03:00
sanspie c64c199e70 art: preserve transparent surface channel data
Keep authored normal, height and emission RGB when the composited pixel is transparent, without covering visible pixels. Check source round trips, partial updates, undo and prepared runner artifacts.
2026-11-07 07:06:12 +03:00
sanspie aa1da698f4 docs: verify MeshRenderer double-sided Try
Exercise back-face culling and Reset in native and Chromium without relaxing the full component inventory gate.
2026-11-07 07:02:04 +03:00
sanspie 25a731f6c4 docs: verify light and ambient Try contracts
Use deterministic light scenes and assert real visual changes and Reset across native and Chromium; overall component coverage remains incomplete.
2026-11-07 06:16:08 +03:00
sanspie 5a2745987f feat(art): import Aseprite through native editor
Create guarded editable sources with existing prepared frame and clip subassets; require confirmation for reported metadata loss and preserve the original file.
2026-11-07 06:07:41 +03:00
sanspie 8a364bee5e test(art): validate real Aseprite files against LibreSprite
Pin MIT-licensed upstream fixtures and independent reference frames; verify linked cels, undo, metadata round-trip, and explicit unsupported-data diagnostics.
2026-11-07 05:43:47 +03:00
sanspie 3d2cf44608 test: require actual null-DACL rejection on Windows
Protect the fixture from inherited ACEs and assert the rejection reason; fix the editor-tagged staticcheck error.
2026-11-07 05:34:15 +03:00
sanspie eb92b49531 test: create Windows null-DACL fixture without elevation
Keep the fail-closed assertion and fix Go vet's keyed color literals.
2026-11-07 05:30:38 +03:00
sanspie 09d10625c3 fix(examples): recognize Windows system volume owner
Trust only the exact Windows TrustedInstaller SID and add an ancestor DELETE_CHILD rejection test; native validation remains in progress.
2026-11-07 05:27:16 +03:00
sanspie 05aa019738 WIP: validate Windows remote store ACLs and reparse points
Fail closed for untrusted writes, null or unknown ACLs, and recheck managed trees. Native Windows validation is still required.
2026-11-07 05:23:49 +03:00
sanspie d965aeb886 fix: portable Stage paths and deduplicate Try sidecars
Allow only macOS system temp aliases while rejecting nested links, and preserve duplicate-contract detection across distinct Try sources.
2026-11-07 05:17:54 +03:00
sanspie 32f65c3ddc feat(examples): open checked remote copies in the launcher
Confirm the HTTPS origin, preserve entry scenes and offline edits, and disable external import tools in managed remote projects. Native Windows store support and full remote-example acceptance remain incomplete.
2026-11-07 05:06:50 +03:00
sanspie 8fd0483b42 art: add native ellipse and lasso selections 2026-11-07 04:36:18 +03:00
sanspie 029f6d7cb4 docs: add executable light occluder contract 2026-11-07 04:35:48 +03:00
sanspie 07a6fa1e55 art: add native eyedropper and preserve chosen color bytes 2026-11-07 04:23:45 +03:00
sanspie 6b52dd790b docs: run component contracts in native and browser runtimes
Verify real Game View changes and reset in both languages; add Camera2D and Camera3D demos. The strict gate remains intentionally failing until all public components are covered.
2026-11-07 04:22:46 +03:00
sanspie 3c015e9266 WIP: integrate Stage 12.5 art authoring and examples
Partial implementation; native studio, cross-platform examples, Try coverage, and acceptance remain incomplete.
2026-11-07 04:01:11 +03:00
936 changed files with 7292 additions and 74066 deletions
-5
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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:
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
+12 -77
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -9
View File
@@ -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
}
-3
View File
@@ -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).
-12
View File
@@ -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 {
-60
View File
@@ -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)
}
}
+4 -6
View File
@@ -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)
-2
View File
@@ -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
View File
@@ -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) {
-13
View File
@@ -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)
}
}
}
-39
View File
@@ -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)
}
-13
View File
@@ -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")
}
}
-42
View File
@@ -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
-2
View File
@@ -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 {
-38
View File
@@ -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)
})
}
-41
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -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)
}
}
-82
View File
@@ -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
View File
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
-96
View File
@@ -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
+1 -20
View File
@@ -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
-78
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-74
View File
@@ -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