Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9612140432 | ||
|
|
31f2da9f79 | ||
|
|
a10633ec8d | ||
|
|
ee267286a3 | ||
|
|
e3d7b8c8a5 | ||
|
|
1ffd388350 | ||
|
|
747b6bc3b9 | ||
|
|
047132e05d |
+1
-113
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"aego/art"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ArtHost is supplied only by the running editor. Offline commands must not
|
|
||||||
// reopen files and silently replace the user's dirty graphical documents.
|
|
||||||
// Like Host.ApplyScene, these calls run on the editor mutation thread.
|
|
||||||
type ArtHost interface {
|
|
||||||
ReadArt(file, section string, offset, limit int) (*Output, error)
|
|
||||||
CaptureArt(file string, frame art.ID, channel art.Channel) (*Output, error)
|
|
||||||
PrepareArt(file string, revision uint64, label string, ops []art.Operation) (*Output, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ArtProposal is an opaque, prepared transaction carried by the existing agent
|
|
||||||
// proposal queue. Commit is invoked only after its normal approval decision.
|
|
||||||
// Keeping the prepared transaction also preserves IDs returned by its preview.
|
|
||||||
type ArtProposal interface {
|
|
||||||
Commit() (*Output, error)
|
|
||||||
RetainedBytes() int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func artHost(s *Session) (ArtHost, error) {
|
|
||||||
if s != nil {
|
|
||||||
if h, ok := s.host.(ArtHost); ok {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("art: connect to the running Aego editor; offline writes to graphical documents are disabled")
|
|
||||||
}
|
|
||||||
func artCommands() []*Command {
|
|
||||||
fileArg := Arg{Name: "file", Type: "string", Doc: "Project-relative .aeart source, including unsaved editor content", Required: true, Pos: true}
|
|
||||||
return []*Command{
|
|
||||||
{Name: "art.read", Doc: "Read the current art document, not its last saved file. Lists are paginated; revision is a decimal string for art.apply.", Args: []Arg{
|
|
||||||
fileArg, {Name: "section", Type: "string", Doc: "summary, layers, frames, cels, palette, tags or slices"},
|
|
||||||
{Name: "offset", Type: "integer", Doc: "First item, default 0"}, {Name: "limit", Type: "integer", Doc: "1..1024 items; default 256"},
|
|
||||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
|
||||||
h, err := artHost(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
offset, err := a.Int("offset", 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
limit, err := a.Int("limit", 256)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if offset < 0 || limit < 1 || limit > 1024 {
|
|
||||||
return nil, fmt.Errorf("art: invalid page")
|
|
||||||
}
|
|
||||||
return h.ReadArt(a.Str("file"), a.Str("section"), offset, limit)
|
|
||||||
}},
|
|
||||||
{Name: "art.capture", Doc: "Capture unsaved CPU pixels with document/frame/channel identity and revision. MCP returns a PNG image; the CLI writes it only with --out to a new file.", Args: []Arg{
|
|
||||||
fileArg, {Name: "frame", Type: "string", Doc: "Stable frame ID; defaults to current frame"},
|
|
||||||
{Name: "channel", Type: "string", Doc: "albedo, normal, height or emission; defaults to current channel"},
|
|
||||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
|
||||||
h, err := artHost(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return h.CaptureArt(a.Str("file"), art.ID(a.Str("frame")), art.Channel(a.Str("channel")))
|
|
||||||
}},
|
|
||||||
{Name: "art.apply", Doc: "Propose one atomic graphics edit at the revision returned by art.read. Agent panel edits require approval; CLI and external agents follow the editor's confirmation setting. All use the document's common Undo. Name new entities with as and refer to them as $name later.", Write: true, Destroys: true, Args: []Arg{
|
|
||||||
fileArg, {Name: "revision", Type: "string", Doc: "Exact decimal revision returned by art.read", Required: true},
|
|
||||||
{Name: "label", Type: "string", Doc: "Human-readable operation label", Required: true},
|
|
||||||
{Name: "operations", Type: "array", Doc: "1..4096 typed art.Operation objects; no paths or shell commands", Required: true, Items: map[string]any{"type": "object"}},
|
|
||||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
|
||||||
h, err := artHost(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !a.Bool("dry-run") {
|
|
||||||
return nil, fmt.Errorf("art: edits must pass through the editor proposal queue")
|
|
||||||
}
|
|
||||||
revision, err := strconv.ParseUint(a.Str("revision"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("art: revision must be an unsigned decimal integer")
|
|
||||||
}
|
|
||||||
label := strings.TrimSpace(a.Str("label"))
|
|
||||||
if label == "" || len(label) > 256 {
|
|
||||||
return nil, fmt.Errorf("art: label must have 1..256 bytes")
|
|
||||||
}
|
|
||||||
ops, err := art.DecodeOperations([]byte(a.Str("operations")))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return h.PrepareArt(a.Str("file"), revision, label, ops)
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"aego/project"
|
"aego/project"
|
||||||
@@ -15,7 +14,6 @@ type cliFlags struct {
|
|||||||
json bool
|
json bool
|
||||||
quiet bool
|
quiet bool
|
||||||
strict bool
|
strict bool
|
||||||
artOut string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Usage(w io.Writer) {
|
func Usage(w io.Writer) {
|
||||||
@@ -103,22 +101,6 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
case "out":
|
|
||||||
if c.Name != "art.capture" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !has {
|
|
||||||
if i+1 >= len(toks) {
|
|
||||||
return nil, f, fmt.Errorf("--out needs a value")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
val = toks[i]
|
|
||||||
}
|
|
||||||
if val == "" || f.artOut != "" {
|
|
||||||
return nil, f, fmt.Errorf("--out requires one non-empty path")
|
|
||||||
}
|
|
||||||
f.artOut = val
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
x, ok := byName[name]
|
x, ok := byName[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -147,9 +129,6 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
|
func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
|
||||||
if err == nil && f.artOut != "" {
|
|
||||||
err = writeArtCapture(f.artOut, out)
|
|
||||||
}
|
|
||||||
switch {
|
switch {
|
||||||
case f.json:
|
case f.json:
|
||||||
var v any = out
|
var v any = out
|
||||||
@@ -172,31 +151,3 @@ func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// An explicit output path never overwrites a user's existing file.
|
|
||||||
func writeArtCapture(path string, out *Output) error {
|
|
||||||
if out == nil || len(out.Images) != 1 || out.Images[0].Mime != "image/png" {
|
|
||||||
return errors.New("art: editor did not return a PNG capture")
|
|
||||||
}
|
|
||||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ type Output struct {
|
|||||||
// Images go to the model as image content, such as the screenshots of a run.
|
// Images go to the model as image content, such as the screenshots of a run.
|
||||||
Images []Image `json:"-"`
|
Images []Image `json:"-"`
|
||||||
Drawing *DrawingEdit `json:"-"`
|
Drawing *DrawingEdit `json:"-"`
|
||||||
Art ArtProposal `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image is a picture an answer carries.
|
// Image is a picture an answer carries.
|
||||||
@@ -74,7 +73,6 @@ func catalog() []*Command {
|
|||||||
out = append(out, materialCommands()...)
|
out = append(out, materialCommands()...)
|
||||||
out = append(out, postEffectCommands()...)
|
out = append(out, postEffectCommands()...)
|
||||||
out = append(out, textureCommands()...)
|
out = append(out, textureCommands()...)
|
||||||
out = append(out, artCommands()...)
|
|
||||||
out = append(out, schemaCommands()...)
|
out = append(out, schemaCommands()...)
|
||||||
out = append(out, projectCommands()...)
|
out = append(out, projectCommands()...)
|
||||||
out = append(out, scriptCommands()...)
|
out = append(out, scriptCommands()...)
|
||||||
@@ -84,7 +82,7 @@ func catalog() []*Command {
|
|||||||
if c.Name == "asset.import-variant" {
|
if c.Name == "asset.import-variant" {
|
||||||
c.Look = ls["asset.pixels"]
|
c.Look = ls["asset.pixels"]
|
||||||
}
|
}
|
||||||
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.") || strings.HasPrefix(c.Name, "art.")) {
|
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.")) {
|
||||||
name := strings.ReplaceAll(c.Name, ".", " ")
|
name := strings.ReplaceAll(c.Name, ".", " ")
|
||||||
c.Look = Look{Icon: "comp.sprite", Title: func(a Args) string { return name + " " + file(a) }, Summary: changes}
|
c.Look = Look{Icon: "comp.sprite", Title: func(a Args) string { return name + " " + file(a) }, Summary: changes}
|
||||||
}
|
}
|
||||||
@@ -145,9 +143,6 @@ func (c *Command) Usage() string {
|
|||||||
if c.Write {
|
if c.Write {
|
||||||
parts = append(parts, "[--dry-run]")
|
parts = append(parts, "[--dry-run]")
|
||||||
}
|
}
|
||||||
if c.Name == "art.capture" {
|
|
||||||
parts = append(parts, "[--out path]")
|
|
||||||
}
|
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"aego/art"
|
|
||||||
"aego/asset"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Source-only scene validation must not treat a partial sprite sheet as a
|
|
||||||
// complete generic material texture when native runtime rejects that sampler.
|
|
||||||
func TestAgentSourceMaterialRejectsPartialArtSheet(t *testing.T) {
|
|
||||||
for _, multi := range []bool{false, true} {
|
|
||||||
t.Run(fmt.Sprintf("multi=%v", multi), func(t *testing.T) {
|
|
||||||
s := materialSession(t)
|
|
||||||
doc, err := art.New(4, 4, art.RGBA, art.DefaultLimits())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer doc.Close()
|
|
||||||
if multi {
|
|
||||||
edit, err := doc.Begin(doc.Revision(), "Add frame")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := edit.AddFrame(doc.Info().Frames[0].ID, 100); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
change, err := edit.Prepare()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := change.Apply(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data, err := doc.Encode()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
artID, err := s.create("assets/art.aeart", data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
content := fmt.Sprintf("material version=1 shader=asset(%q) { params { Noise=asset(%q) } }", asset.BuiltinShaderDissolve.String(), artID.String())
|
|
||||||
materialID, err := s.create("assets/art.aemat", []byte(content))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if info, ok := (sources{s.Pipeline}).TextureMetadata(artID); !ok || info.Region != multi || info.Width != 4 || info.Height != 4 {
|
|
||||||
t.Fatalf("source-only metadata lost first-frame bounds: %+v/%v", info, ok)
|
|
||||||
}
|
|
||||||
if _, ok := (sources{s.Pipeline}).Material(materialID); ok == multi {
|
|
||||||
t.Fatalf("source-only material resolver result=%v for multi=%v", ok, multi)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -470,23 +470,6 @@ func (r sources) Material(id ids.AssetID) (*material.Material, bool) {
|
|||||||
}
|
}
|
||||||
e, ok := r.pl.Entry(tid)
|
e, ok := r.pl.Entry(tid)
|
||||||
info, valid := r.pl.TextureSourceInfo(tid, nil)
|
info, valid := r.pl.TextureSourceInfo(tid, nil)
|
||||||
if !valid && strings.EqualFold(path.Ext(e.Source), ".aeart") {
|
|
||||||
// The PNG source inspector cannot decode an art document. Use the
|
|
||||||
// same prepared frame semantics as the runtime instead of silently
|
|
||||||
// accepting its whole animation sheet as a generic sampler.
|
|
||||||
prepared, ready := r.TextureMetadata(tid)
|
|
||||||
if !ok || e.Missing || e.Error != "" || !ready {
|
|
||||||
return material.Tex{}, false
|
|
||||||
}
|
|
||||||
mode := texture.EmissionNone
|
|
||||||
switch prepared.Semantic {
|
|
||||||
case asset.TextureEmissionRGB:
|
|
||||||
mode = texture.EmissionRGB
|
|
||||||
case asset.TextureEmissionMask:
|
|
||||||
mode = texture.EmissionMask
|
|
||||||
}
|
|
||||||
return material.Tex{RegionOnly: prepared.Region, NormalMap: prepared.Semantic == asset.TextureNormal, EmissionMode: mode}, true
|
|
||||||
}
|
|
||||||
mode := texture.EmissionNone
|
mode := texture.EmissionNone
|
||||||
switch info.Role {
|
switch info.Role {
|
||||||
case asset.RoleEmissionRGB:
|
case asset.RoleEmissionRGB:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ var versions = []string{"2025-06-18", "2025-03-26", "2024-11-05"}
|
|||||||
|
|
||||||
var skipDirs = []string{".aego", ".git", "exports", "native"}
|
var skipDirs = []string{".aego", ".git", "exports", "native"}
|
||||||
|
|
||||||
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. For editable .aeart in a running editor, art_read and art_capture inspect the unsaved document; art_apply proposes an undoable batch at the returned revision. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
|
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
|
||||||
|
|
||||||
type request struct {
|
type request struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
|||||||
+1
-17
@@ -3,7 +3,6 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -92,7 +91,7 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
|
|||||||
var rep struct {
|
var rep struct {
|
||||||
Result struct {
|
Result struct {
|
||||||
Content []struct {
|
Content []struct {
|
||||||
Type, Text, Data, MimeType string
|
Type, Text string
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
IsError bool `json:"isError"`
|
IsError bool `json:"isError"`
|
||||||
} `json:"result"`
|
} `json:"result"`
|
||||||
@@ -123,21 +122,6 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
|
|||||||
return &Output{OK: true, Text: text}, nil
|
return &Output{OK: true, Text: text}, nil
|
||||||
}
|
}
|
||||||
o := out.Output
|
o := out.Output
|
||||||
if c.Name == "art.capture" {
|
|
||||||
for _, part := range rep.Result.Content {
|
|
||||||
if part.Type != "image" || part.MimeType != "image/png" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(part.Data) > base64.StdEncoding.EncodedLen(16<<20) {
|
|
||||||
return nil, errors.New("art: PNG exceeds agent capture budget")
|
|
||||||
}
|
|
||||||
data, err := base64.StdEncoding.DecodeString(part.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("art: invalid PNG response: %w", err)
|
|
||||||
}
|
|
||||||
o.Images = append(o.Images, Image{Mime: part.MimeType, Data: data})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var s string
|
var s string
|
||||||
switch {
|
switch {
|
||||||
case c.Name == "scene.run":
|
case c.Name == "scene.run":
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package editors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"aego/app/assembly"
|
"aego/app/assembly"
|
||||||
"aego/modules/animation"
|
|
||||||
animationeditor "aego/modules/animation/editor"
|
|
||||||
"aego/modules/audio"
|
"aego/modules/audio"
|
||||||
audioeditor "aego/modules/audio/editor"
|
audioeditor "aego/modules/audio/editor"
|
||||||
"aego/modules/camera"
|
"aego/modules/camera"
|
||||||
@@ -26,7 +24,6 @@ import (
|
|||||||
|
|
||||||
func With(c assembly.Catalog) assembly.Catalog {
|
func With(c assembly.Catalog) assembly.Catalog {
|
||||||
return c.
|
return c.
|
||||||
With(animation.ID, animationeditor.Hook).
|
|
||||||
With(inputmod.ID, inputeditor.Hook).
|
With(inputmod.ID, inputeditor.Hook).
|
||||||
With(particles.ID, particleseditor.Hook).
|
With(particles.ID, particleseditor.Hook).
|
||||||
With(render2d.ID, render2deditor.Hook).
|
With(render2d.ID, render2deditor.Hook).
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"aego/contracts/persistence"
|
"aego/contracts/persistence"
|
||||||
@@ -20,13 +19,10 @@ func UseMemorySaves(e *engine.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type memUser struct {
|
type memUser struct {
|
||||||
mu sync.RWMutex
|
|
||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
data, ok := m.files[path.Clean(p)]
|
data, ok := m.files[path.Clean(p)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fs.ErrNotExist
|
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 {
|
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...)
|
m.files[path.Clean(p)] = append([]byte(nil), data...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memUser) Remove(p string) error {
|
func (m *memUser) Remove(p string) error {
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
delete(m.files, path.Clean(p))
|
delete(m.files, path.Clean(p))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
data, ok := m.files[path.Clean(p)]
|
data, ok := m.files[path.Clean(p)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return platform.UserEntry{}, fs.ErrNotExist
|
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) {
|
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
prefix := path.Clean(dir) + "/"
|
prefix := path.Clean(dir) + "/"
|
||||||
var out []platform.UserEntry
|
var out []platform.UserEntry
|
||||||
for p, data := range m.files {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"aego/app/assembly"
|
"aego/app/assembly"
|
||||||
"aego/app/assembly/builtin"
|
"aego/app/assembly/builtin"
|
||||||
"aego/app/docicons"
|
|
||||||
"aego/asset/loader"
|
"aego/asset/loader"
|
||||||
"aego/asset/pipeline"
|
"aego/asset/pipeline"
|
||||||
"aego/contracts/persistence"
|
"aego/contracts/persistence"
|
||||||
@@ -46,9 +45,6 @@ type Config struct {
|
|||||||
Platform platform.Platform
|
Platform platform.Platform
|
||||||
Profile string
|
Profile string
|
||||||
NoProfile bool
|
NoProfile bool
|
||||||
// NoExternalImportTools prevents project assets from starting external
|
|
||||||
// converters and validators, including on later reimport.
|
|
||||||
NoExternalImportTools bool
|
|
||||||
// SharedIndex opens the asset index read-only without a warning when another process, the editor, holds it.
|
// SharedIndex opens the asset index read-only without a warning when another process, the editor, holds it.
|
||||||
SharedIndex bool
|
SharedIndex bool
|
||||||
}
|
}
|
||||||
@@ -138,9 +134,6 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
|||||||
if h.Importers, err = DevImporters(h.Workspace, h.Modules); err != nil {
|
if h.Importers, err = DevImporters(h.Workspace, h.Modules); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cfg.NoExternalImportTools {
|
|
||||||
disableExternalImportTools(h.Importers)
|
|
||||||
}
|
|
||||||
if err = h.Types.Schema().Freeze(); err != nil {
|
if err = h.Types.Schema().Freeze(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -154,7 +147,6 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
|||||||
ch.Warn("cannot describe the script bindings", log.F("err", derr.Error()))
|
ch.Warn("cannot describe the script bindings", log.F("err", derr.Error()))
|
||||||
} else {
|
} else {
|
||||||
h.Docs = docs.FromManifest(m, h.Types.Schema(), h.Modules)
|
h.Docs = docs.FromManifest(m, h.Types.Schema(), h.Modules)
|
||||||
h.Docs.SetNavigationIcons(docicons.Builtin())
|
|
||||||
m.Keywords = h.Docs.Keywords()
|
m.Keywords = h.Docs.Keywords()
|
||||||
h.Workspace.SetDocs(m)
|
h.Workspace.SetDocs(m)
|
||||||
for _, p := range h.Docs.Problems() {
|
for _, p := range h.Docs.Problems() {
|
||||||
|
|||||||
@@ -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"
|
||||||
"aego/app/assembly/builtin"
|
"aego/app/assembly/builtin"
|
||||||
"aego/app/docicons"
|
|
||||||
"aego/app/playground"
|
"aego/app/playground"
|
||||||
"aego/engine/docs"
|
"aego/engine/docs"
|
||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
@@ -61,7 +60,6 @@ func build(p *project.Project, mods []module.Module, pg *playground.Playground)
|
|||||||
src.Project = p.Name
|
src.Project = p.Name
|
||||||
}
|
}
|
||||||
r := docs.Assemble(src)
|
r := docs.Assemble(src)
|
||||||
r.SetNavigationIcons(docicons.Builtin())
|
|
||||||
localize(r)
|
localize(r)
|
||||||
bm.Keywords = r.Keywords()
|
bm.Keywords = r.Keywords()
|
||||||
if ws != nil {
|
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-191
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
|
textOn bool
|
||||||
shown *engine.World
|
shown *engine.World
|
||||||
closed bool
|
closed bool
|
||||||
control ControlState
|
|
||||||
viewport Viewport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func noBlock(string) *aetext.Node {
|
func noBlock(string) *aetext.Node {
|
||||||
@@ -348,7 +346,7 @@ func (a *App) showPhysics() {
|
|||||||
func (a *App) Update(dt float64) {
|
func (a *App) Update(dt float64) {
|
||||||
e := a.e
|
e := a.e
|
||||||
now := e.Platform().Now()
|
now := e.Platform().Now()
|
||||||
a.tickSession(dt)
|
a.session.Tick(dt)
|
||||||
a.showPhysics()
|
a.showPhysics()
|
||||||
ui := a.ui
|
ui := a.ui
|
||||||
ui.Time = now
|
ui.Time = now
|
||||||
@@ -363,7 +361,6 @@ func (a *App) Update(dt float64) {
|
|||||||
a.toolbar(bar)
|
a.toolbar(bar)
|
||||||
body := mathx.Rect{Y: bar.H, W: size.X, H: max(0, size.Y-bar.H)}
|
body := mathx.Rect{Y: bar.H, W: size.X, H: max(0, size.Y-bar.H)}
|
||||||
gameR, inspR := layout(body, 1)
|
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, gameR, a.game.Draw)
|
||||||
a.panel(&frame, inspR, a.insp.Draw)
|
a.panel(&frame, inspR, a.insp.Draw)
|
||||||
ctx.DnD.Frame(ui)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+19
-104
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
@@ -1,71 +0,0 @@
|
|||||||
//go:build aego_editor
|
|
||||||
|
|
||||||
package docsview
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"aego/app/playground"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Component-specific demonstrations must not silently replace the existing
|
|
||||||
// authored scenes and scripts used by the broader guides.
|
|
||||||
func TestComponentTryGuidesPreserveAuthoredScenes(t *testing.T) {
|
|
||||||
p, err := playground.New()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, spec := range []struct{ guide, component, sum, script string }{
|
|
||||||
{"camera", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
|
|
||||||
{"camera/follow", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
|
|
||||||
{"camera/zones", "camera.CameraZone2D", "9f5b36a8de5584836d466b8437510c1990e3c719d41a4c65dfa591b2b7c634e8", ""},
|
|
||||||
{"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 {
|
|
||||||
t.Fatalf("authored guide scene or selection changed: sha=%s select=%s", got, guide.TrySelect)
|
|
||||||
}
|
|
||||||
if spec.script != "" {
|
|
||||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.TryScript))); got != spec.script {
|
|
||||||
t.Fatalf("authored guide script changed: sha=%s", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
component, ok := p.Docs().Get(spec.component)
|
|
||||||
if !ok || component.Demo == nil || component.Try == guide.Try {
|
|
||||||
t.Fatalf("%s needs its distinct executable component demo", spec.component)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for _, problem := range p.Docs().Problems() {
|
|
||||||
if problem.Error {
|
|
||||||
t.Fatalf("documentation registry error: %+v", problem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ func TestMaterialTryCompilesItsQualifiedScriptAndAnimatesFlash(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
entries := p.TryScenes()
|
entries := p.TryScenes()
|
||||||
for _, id := range []string{"render2d/materials"} {
|
for _, id := range []string{"render2d/materials", "engine.MaterialOverride"} {
|
||||||
t.Run(id, func(t *testing.T) {
|
t.Run(id, func(t *testing.T) {
|
||||||
var entry docs.Entry
|
var entry docs.Entry
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user