Files

307 lines
10 KiB
Go

//go:build aego_editor
package docsview
import (
"bytes"
"fmt"
"strings"
"aego/asset"
"aego/audio/codec"
"aego/core/ids"
"aego/modules/audio"
"aego/modules/particles"
"aego/modules/render2d"
"aego/modules/savegame"
"aego/scene"
"aego/scene/prefab"
"aego/scene/sceneio"
"aego/scene/scriptcomp"
"aego/schema"
)
// Inspection is a read-only observation of the running Try world, not the
// source text or a simulated result. Fields refer to the selected component.
type Inspection struct {
Components []string `json:"components"`
Dependencies []string `json:"dependencies"`
Observables map[string]any `json:"observables"`
RuntimeExecuting bool `json:"runtimeExecuting"`
}
func (a *App) inspectTry(component string, fields, dependencies []string) (*Inspection, error) {
if component == "" || len(fields) > 256 || len(dependencies) > 256 {
return nil, fmt.Errorf("invalid Try inspection request")
}
sc := a.session.Controller().Scene()
if sc == nil {
return nil, fmt.Errorf("the Try scene is not running")
}
td, ok := sc.Registry().ByName(component)
if !ok {
return nil, fmt.Errorf("component is not registered: %s", component)
}
selected := a.session.Selected()
if len(selected) != 1 {
return nil, fmt.Errorf("the Try scene does not have exactly one selected node")
}
h := sc.Handle(selected[0])
if h.IsZero() || !sc.IsAlive(h) {
return nil, fmt.Errorf("selected node is absent from the running scene")
}
if _, ok := sc.GetByType(h, td.ID); !ok {
return nil, fmt.Errorf("selected node does not have %s", component)
}
st := a.session.State()
out := &Inspection{Components: []string{td.FullName}, Observables: map[string]any{}, RuntimeExecuting: st.Running && !st.Paused && st.Frames > 0}
seen := make(map[string]bool, len(dependencies))
for _, dep := range dependencies {
if seen[dep] {
return nil, fmt.Errorf("duplicate dependency %s", dep)
}
seen[dep] = true
id, err := ids.ParseAssetID(dep)
if err != nil {
return nil, fmt.Errorf("invalid dependency %s: %w", dep, err)
}
kind := asset.ArtifactKind("")
if builtin, ok := asset.LookupBuiltin(id); ok {
kind = builtin.Kind
} else if entry, ok := a.loader.Index().Lookup(id); ok && !entry.Missing {
kind = entry.Kind
}
resolved := false
switch kind {
case asset.ArtMesh:
_, resolved = a.loader.Mesh(id)
case asset.ArtTexture, asset.ArtSpriteRegion, asset.ArtAtlasPage:
_, resolved = a.loader.Texture(id)
case asset.ArtMaterial:
_, resolved = a.loader.Material(id)
case asset.ArtShader:
_, resolved = a.loader.Shader(id)
case asset.ArtAnim:
_, resolved = a.loader.Anim(id)
case asset.ArtCtrl:
_, resolved = a.loader.Controller(id)
case asset.ArtPrefab:
// A registered ID or raw bytes alone do not establish that an
// instance can be loaded: parse and expand the actual prefab,
// including its nested prefab dependencies. Expand preserves
// missing nested prefabs as placeholders, so reject those too.
lib := sceneio.NewLibrary(sc.Registry(), a.loader)
if source, ok := lib.Prefab(id); ok {
tree, _, err := prefab.Expand(lib, source, nil)
if err == nil && tree != nil {
resolved = true
tree.Walk(func(n, _ *prefab.Node) { resolved = resolved && !n.Missing })
}
}
case asset.ArtAudio:
raw, ok := a.loader.Audio(id)
resolved = ok && validTryAudio(raw)
case "":
return nil, fmt.Errorf("unresolved dependency %s", dep)
default:
return nil, fmt.Errorf("dependency kind %s is not supported by Try inspection", kind)
}
if !resolved {
return nil, fmt.Errorf("unresolved dependency %s", dep)
}
out.Dependencies = append(out.Dependencies, dep)
}
for _, observable := range fields {
field, ok := strings.CutPrefix(observable, component+".")
target, targetComponent := h, component
if path, crossNode := strings.CutPrefix(observable, "node:"); crossNode {
name, property, valid := strings.Cut(path, ":")
if !valid || name == "" || len(name) > 256 {
return nil, fmt.Errorf("invalid target node observable %s", observable)
}
target = sc.Find(sc.Root(), name)
if target.IsZero() || !sc.IsAlive(target) {
return nil, fmt.Errorf("observable node %s is absent", name)
}
field, ok = property, strings.HasPrefix(property, "Node.")
if !ok {
for _, typ := range sc.Registry().Components() {
if suffix, has := strings.CutPrefix(property, typ.FullName+"."); has {
targetComponent, field, ok = typ.FullName, suffix, true
break
}
}
}
}
if !ok || field == "" || len(observable) > 512 {
return nil, fmt.Errorf("unsupported observable %s", observable)
}
if _, duplicate := out.Observables[observable]; duplicate {
return nil, fmt.Errorf("duplicate observable %s", observable)
}
// Cross-node observations must use that node and its component, never
// the selected node handled by the legacy component-specific cases.
if target != h || targetComponent != component {
value, err := a.observeValue(sc, target, targetComponent, field)
if err != nil {
return nil, err
}
out.Observables[observable] = value
continue
}
if component == particles.NameSystem && field == "LiveParticles" {
s := particles.Of(a.session.Controller().World())
if s == nil {
return nil, fmt.Errorf("particle runtime is unavailable")
}
out.Observables[observable] = s.Count(h)
continue
}
if component == audio.NameListener && field == "ListenerX" {
s := audio.Of(a.session.Controller().World())
if s == nil {
return nil, fmt.Errorf("audio runtime is unavailable")
}
out.Observables[observable] = float64(s.Listener().X)
continue
}
if component == audio.NameZone && field == "WorldGain" {
s := audio.Of(a.session.Controller().World())
zone, ok := scene.Get[audio.AudioZone2D](sc, h)
if s == nil || !ok {
return nil, fmt.Errorf("audio zone runtime is unavailable")
}
out.Observables[observable] = float64(s.WorldBusGain(zone.Bus))
continue
}
if component == savegame.MarkerName && field == "PositionX" {
out.Observables[observable] = float64(sc.WorldPosition(h).X)
continue
}
if component == scene.TagsName && field == "HasEnemy" {
out.Observables[observable] = sc.HasTag(h, "Enemy")
continue
}
if component == scriptcomp.ScriptName && field == "AttachmentEnabled" {
scripts, ok := scriptcomp.ScriptsOf(sc, h)
if !ok || len(scripts.Attachments) != 1 {
return nil, fmt.Errorf("exactly one script attachment is required")
}
out.Observables[observable] = scripts.Attachments[0].Enabled
continue
}
if component == scriptcomp.ScriptName && field == "PositionX" {
out.Observables[observable] = float64(sc.WorldPosition(h).X)
continue
}
if component == sceneio.PrefabName {
switch field {
case "LinkedPrefab", "ReferencePrefab", "PositionX", "ReferencePositionX":
target := h
if field == "ReferencePrefab" || field == "ReferencePositionX" {
target = sc.Find(sc.Root(), "Reference instance")
}
if target.IsZero() || !sc.IsAlive(target) {
return nil, fmt.Errorf("prefab instance is absent")
}
instance, ok := sceneio.InstanceOf(sc, target)
if !ok || !instance.Owner || instance.Missing || instance.Prefab.IsZero() {
return nil, fmt.Errorf("node is not a resolved authored prefab instance")
}
if field == "LinkedPrefab" || field == "ReferencePrefab" {
out.Observables[observable] = instance.Prefab.String()
} else {
out.Observables[observable] = float64(sc.WorldPosition(target).X)
}
continue
}
}
if component == "engine.MaterialOverride" {
switch field {
case "FlashValue", "ReferenceFlash":
target := h
if field == "ReferenceFlash" {
target = sc.Find(sc.Root(), "Reference instance")
if target.IsZero() || !sc.IsAlive(target) {
return nil, fmt.Errorf("material reference instance is absent")
}
}
value, err := render2d.GetMaterialParameter(sc, target, "Flash")
if err != nil || value.Kind != schema.KindFloat32 {
return nil, fmt.Errorf("material Flash is unavailable: %v", err)
}
out.Observables[observable] = value.F
continue
case "FlashExplicit":
o, ok := scene.Get[render2d.MaterialOverride](sc, h)
m, err := render2d.NodeMaterial(sc, h)
if !ok || err != nil {
return nil, fmt.Errorf("material override is unavailable: %v", err)
}
out.Observables[observable] = o.ExplicitFor(m, "Flash")
continue
}
}
var fd *schema.FieldDescriptor
for i := range td.Fields {
if strings.EqualFold(td.Fields[i].Name, field) {
fd = &td.Fields[i]
break
}
}
if fd == nil {
value, err := a.observeValue(sc, target, targetComponent, field)
if err != nil {
return nil, err
}
out.Observables[observable] = value
continue
}
v, err := sc.GetProperty(h, td.ID, fd.ID)
if err != nil {
return nil, err
}
switch v.Kind {
case schema.KindBool:
out.Observables[observable] = v.I != 0
case schema.KindInt32, schema.KindInt64, schema.KindEnum:
out.Observables[observable] = v.I
case schema.KindFloat32, schema.KindFloat64:
out.Observables[observable] = v.F
case schema.KindString:
out.Observables[observable] = v.S
case schema.KindList:
if fd.Elem != schema.KindString {
return nil, fmt.Errorf("observable list kind %v is not supported", fd.Elem)
}
list := make([]string, len(v.List))
for i, item := range v.List {
if item.Kind != schema.KindString {
return nil, fmt.Errorf("observable list contains a non-string value")
}
list[i] = item.S
}
out.Observables[observable] = list
default:
return nil, fmt.Errorf("observable kind %v is not supported", v.Kind)
}
}
return out, nil
}
// Resolve the prepared artifact and open its real audio payload. Merely
// finding an audio ID is not enough evidence that a Try can play the sound.
func validTryAudio(raw []byte) bool {
a, err := asset.DecodeAudio(raw)
if err != nil || len(a.Data) == 0 || a.Frames <= 0 {
return false
}
d, err := codec.Open(bytes.NewReader(a.Data), "")
if err != nil {
return false
}
info := d.Info()
err = d.Close()
return err == nil && info.SampleRate == a.Rate && info.Channels == a.Channels && (info.Frames < 0 || info.Frames == a.Frames)
}