ci / checks (push) Successful in 5m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m39s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 5m44s
The agent attaches and removes scripts with add-comp and remove-comp by the script's type name, reads and sets their exported fields like component fields with short names (EnemyAI.PointA), and node reference fields take a node path. scene_get lists a node's script fields. scene_run works inside the editor: the game runs headless in a child process of the editor's own binary (--tool agent-run), with the open scene as the editor has it, unsaved changes included; scripts with unsaved changes are named in the answer. watch samples the world positions of nodes through the run, so the agent can check that something moves. Following brings the Scene View back in front of the code editor on a scene step and shows the Inspector, and a scene_apply leads to the node its transaction names.
291 lines
7.6 KiB
Go
291 lines
7.6 KiB
Go
package mutate
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/format/aetext"
|
|
"aego/scene/sceneio"
|
|
"aego/schema"
|
|
)
|
|
|
|
func ParseValue(text string) (aetext.Value, error) {
|
|
doc, err := aetext.Parse([]byte("v {\n x="+text+"\n}\n"), "value")
|
|
if err != nil {
|
|
if bare(text) {
|
|
return aetext.String(text), nil
|
|
}
|
|
return aetext.Value{}, err
|
|
}
|
|
v, ok := doc.Field("x")
|
|
if !ok {
|
|
return aetext.Value{}, errs.New(errs.SceneIOParse, "not a value", log.F("text", text))
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// bare is a node path or a #id written without quotes: Root/Player, #c000…
|
|
func bare(text string) bool {
|
|
if text == "" {
|
|
return false
|
|
}
|
|
for _, r := range text {
|
|
if !(r == '/' || r == '#' || r == '_' || r == '-' || r == '.' || r >= '0' && r <= '9' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func FormatValue(v aetext.Value) string {
|
|
out := string(aetext.Write(&aetext.Node{Head: "v", Fields: []aetext.Field{{Name: "x", Value: v}}}))
|
|
i := strings.Index(out, "x=")
|
|
if i < 0 {
|
|
return strings.TrimSpace(out)
|
|
}
|
|
s := out[i+2:]
|
|
if j := strings.LastIndex(s, "}"); j >= 0 {
|
|
s = s[:j]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
func plain(k schema.Kind) *schema.FieldDescriptor {
|
|
return &schema.FieldDescriptor{Kind: k, Flags: schema.FlagSerialize}
|
|
}
|
|
|
|
func elemOf(fd *schema.FieldDescriptor) *schema.FieldDescriptor {
|
|
return &schema.FieldDescriptor{Kind: fd.Elem, Struct: fd.Struct, Enum: fd.Enum, EnumName: fd.EnumName, AssetKind: fd.AssetKind, Component: fd.Component, Flags: schema.FlagSerialize}
|
|
}
|
|
|
|
func numeric(v aetext.Value) bool {
|
|
return v.Kind == aetext.VInt || v.Kind == aetext.VFloat
|
|
}
|
|
|
|
func numbers(v aetext.Value, n int) bool {
|
|
if v.Kind != aetext.VList || len(v.List) != n {
|
|
return false
|
|
}
|
|
for _, e := range v.List {
|
|
if !numeric(e) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func shape(fd *schema.FieldDescriptor, v aetext.Value) error {
|
|
ok := true
|
|
want := ""
|
|
switch fd.Kind {
|
|
case schema.KindBool:
|
|
ok, want = v.Kind == aetext.VBool, "true or false"
|
|
case schema.KindInt32, schema.KindInt64:
|
|
ok, want = v.Kind == aetext.VInt || v.Kind == aetext.VFloat && v.Float == math.Trunc(v.Float), "a whole number"
|
|
case schema.KindFloat32, schema.KindFloat64:
|
|
ok, want = numeric(v), "a number"
|
|
case schema.KindString:
|
|
ok, want = v.Kind == aetext.VString, "a quoted string"
|
|
case schema.KindVec2:
|
|
ok, want = numbers(v, 2), "[x, y]"
|
|
case schema.KindVec3:
|
|
ok, want = numbers(v, 3), "[x, y, z]"
|
|
case schema.KindVec4, schema.KindQuat:
|
|
ok, want = numbers(v, 4), "[x, y, z, w]"
|
|
case schema.KindRect:
|
|
ok, want = numbers(v, 4), "[x, y, w, h]"
|
|
case schema.KindColor:
|
|
ok, want = v.Kind == aetext.VColor, "a colour like #rrggbbaa"
|
|
case schema.KindEnum:
|
|
ok, want = v.Kind == aetext.VIdent || v.Kind == aetext.VInt, "an enum name"
|
|
case schema.KindAssetRef:
|
|
ok, want = v.Kind == aetext.VAsset || v.Kind == aetext.VString, `asset("<id>") or a project path`
|
|
case schema.KindNodeRef:
|
|
ok, want = v.Kind == aetext.VNode || v.Kind == aetext.VString, `node("<id>"), "#<id>" or a node path like "Root/Player"`
|
|
case schema.KindCurve:
|
|
ok, want = v.Kind == aetext.VStruct, "{ ease=… keys=[…] }"
|
|
case schema.KindList:
|
|
ok, want = v.Kind == aetext.VList, "a list [ … ]"
|
|
}
|
|
if !ok {
|
|
return errs.New(errs.EditorProps, fd.Name+" takes "+want, log.F("value", FormatValue(v)))
|
|
}
|
|
if fd.Kind == schema.KindList {
|
|
el := elemOf(fd)
|
|
el.Name = fd.Name
|
|
for _, e := range v.List {
|
|
if err := shape(el, e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) read(fd *schema.FieldDescriptor, v aetext.Value) (schema.Value, error) {
|
|
if fd.Kind == schema.KindNodeRef && (v.Kind == aetext.VString || v.Kind == aetext.VIdent) {
|
|
if v.Kind == aetext.VIdent {
|
|
v = aetext.String(v.Ident)
|
|
}
|
|
if v.Str == "" || v.Str == "none" {
|
|
return sceneio.ReadValue(d.reg, fd, aetext.Value{Kind: aetext.VNode})
|
|
}
|
|
if _, err := ids.ParseNodeID(v.Str); err == nil && len(v.Str) == 32 {
|
|
v = aetext.NodeRef(v.Str)
|
|
} else {
|
|
h, err := d.node(v.Str)
|
|
if err != nil {
|
|
return schema.Value{}, err
|
|
}
|
|
v = aetext.NodeRef(d.Scene.ID(h).String())
|
|
}
|
|
}
|
|
if err := shape(fd, v); err != nil {
|
|
return schema.Value{}, err
|
|
}
|
|
out, err := sceneio.ReadValue(d.reg, fd, v)
|
|
if err == nil || fd.Kind != schema.KindAssetRef || d.env.Lookup == nil || v.Str == "" {
|
|
return out, err
|
|
}
|
|
id, ok := d.env.Lookup(v.Str)
|
|
if !ok {
|
|
return out, errs.New(errs.AssetNotFound, "no asset at this path", log.F("path", v.Str))
|
|
}
|
|
return sceneio.ReadValue(d.reg, fd, aetext.AssetRef(id.String()))
|
|
}
|
|
|
|
func (d *Document) text(fd *schema.FieldDescriptor, v schema.Value) string {
|
|
return FormatValue(sceneio.WriteValue(d.reg, fd, v))
|
|
}
|
|
|
|
func lane(v *schema.Value, name string) *float32 {
|
|
switch v.Kind {
|
|
case schema.KindVec2:
|
|
switch name {
|
|
case "x":
|
|
return &v.V.X
|
|
case "y":
|
|
return &v.V.Y
|
|
}
|
|
case schema.KindVec3:
|
|
switch name {
|
|
case "x":
|
|
return &v.V3.X
|
|
case "y":
|
|
return &v.V3.Y
|
|
case "z":
|
|
return &v.V3.Z
|
|
}
|
|
case schema.KindVec4:
|
|
switch name {
|
|
case "x":
|
|
return &v.V4.X
|
|
case "y":
|
|
return &v.V4.Y
|
|
case "z":
|
|
return &v.V4.Z
|
|
case "w":
|
|
return &v.V4.W
|
|
}
|
|
case schema.KindQuat:
|
|
switch name {
|
|
case "x":
|
|
return &v.Q.X
|
|
case "y":
|
|
return &v.Q.Y
|
|
case "z":
|
|
return &v.Q.Z
|
|
case "w":
|
|
return &v.Q.W
|
|
}
|
|
case schema.KindColor:
|
|
switch name {
|
|
case "r":
|
|
return &v.C.R
|
|
case "g":
|
|
return &v.C.G
|
|
case "b":
|
|
return &v.C.B
|
|
case "a":
|
|
return &v.C.A
|
|
}
|
|
case schema.KindRect:
|
|
switch name {
|
|
case "x":
|
|
return &v.R.X
|
|
case "y":
|
|
return &v.R.Y
|
|
case "w":
|
|
return &v.R.W
|
|
case "h":
|
|
return &v.R.H
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setLane(cur schema.Value, name string, v aetext.Value) (schema.Value, error) {
|
|
next := cur
|
|
p := lane(&next, name)
|
|
if p == nil {
|
|
return cur, errs.New(errs.EditorProps, "the value has no part "+name)
|
|
}
|
|
f, err := strconv.ParseFloat(FormatValue(v), 32)
|
|
if err != nil || !numeric(v) || math.IsInf(f, 0) {
|
|
return cur, errs.New(errs.EditorProps, "a part takes a number", log.F("part", name))
|
|
}
|
|
*p = float32(f)
|
|
return next, nil
|
|
}
|
|
|
|
func (d *Document) validate(fd *schema.FieldDescriptor, v schema.Value, what string, live bool) error {
|
|
if fd.Flags.Has(schema.FlagReadOnly) {
|
|
return errs.New(errs.EditorProps, what+" is read-only")
|
|
}
|
|
if !live && !fd.Flags.Has(schema.FlagSerialize) {
|
|
return errs.New(errs.EditorProps, what+" is not saved with the scene")
|
|
}
|
|
return d.check(fd, v, what)
|
|
}
|
|
|
|
func errNoAsset(text string) error {
|
|
return errs.New(errs.AssetNotFound, "no asset with this id or path", log.F("asset", text))
|
|
}
|
|
|
|
func (d *Document) check(fd *schema.FieldDescriptor, v schema.Value, what string) error {
|
|
x, numeric := 0.0, false
|
|
switch v.Kind {
|
|
case schema.KindInt32, schema.KindInt64:
|
|
x, numeric = float64(v.I), true
|
|
case schema.KindFloat32, schema.KindFloat64:
|
|
x, numeric = v.F, true
|
|
if math.IsNaN(x) || math.IsInf(x, 0) {
|
|
return errs.New(errs.EditorProps, what+" must be a finite number")
|
|
}
|
|
case schema.KindAssetRef:
|
|
if !v.Asset.IsZero() && d.env.Assets != nil && !d.env.Assets(v.Asset) {
|
|
return errs.New(errs.AssetNotFound, what+" references an asset that is not in the project", log.F("asset", v.Asset.String()))
|
|
}
|
|
case schema.KindList:
|
|
el := elemOf(fd)
|
|
for i, e := range v.List {
|
|
if err := d.check(el, e, what+"["+strconv.Itoa(i)+"]"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if numeric {
|
|
if fd.HasMin && x < fd.Min {
|
|
return errs.New(errs.EditorProps, what+" must be at least "+strconv.FormatFloat(fd.Min, 'g', -1, 64), log.F("value", x))
|
|
}
|
|
if fd.HasMax && x > fd.Max {
|
|
return errs.New(errs.EditorProps, what+" must be at most "+strconv.FormatFloat(fd.Max, 'g', -1, 64), log.F("value", x))
|
|
}
|
|
}
|
|
return nil
|
|
}
|