Files
2026-09-26 16:32:40 +03:00

440 lines
11 KiB
Go

package sceneio
import (
"aego/asset"
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/core/mathx"
"aego/format/aetext"
"aego/scene"
"aego/schema"
)
const FormatVersion = 1
type LoadOptions struct {
Strict bool
KeepIDs bool
}
type Diag struct {
Path string
Pos aetext.Pos
Msg string
}
type Unresolved struct {
Node ids.NodeID
Asset ids.AssetID
Target ids.NodeID
Type string
Field string
}
type Report struct {
Warnings []Diag
Unknown []string
Unresolved []Unresolved
}
func (r *Report) warn(path string, pos aetext.Pos, msg string) {
r.Warnings = append(r.Warnings, Diag{Path: path, Pos: pos, Msg: msg})
}
type loader struct {
scene *scene.Scene
reg *schema.Registry
res asset.Resolver
opt LoadOptions
report *Report
name string
nodeIDs map[ids.NodeID]scene.NodeHandle
deferred []deferredRef
created []scene.NodeHandle
}
type deferredRef struct {
node scene.NodeHandle
typeID schema.TypeID
field schema.FieldID
target ids.NodeID
name string
fname string
}
func Load(src []byte, name string, types *scene.Types, res asset.Resolver, opt LoadOptions) (*scene.Scene, *Report, error) {
s := scene.New(types, res, scene.Options{})
_, report, err := LoadInto(s, s.Root(), src, name, opt)
if err != nil {
return nil, report, err
}
return s, report, nil
}
func LoadInto(s *scene.Scene, parent scene.NodeHandle, src []byte, name string, opt LoadOptions) ([]scene.NodeHandle, *Report, error) {
report := &Report{}
l := &loader{
scene: s,
reg: s.Registry(),
res: s.Resolver(),
opt: opt,
report: report,
name: name,
nodeIDs: map[ids.NodeID]scene.NodeHandle{},
}
doc, err := parseDocument(src, name, report)
if err != nil {
return nil, report, err
}
for _, block := range doc.Blocks {
if block.Head != "node" {
continue
}
if err := l.loadNode(block, parent); err != nil {
return nil, report, err
}
}
if err := l.resolveDeferred(); err != nil {
return nil, report, err
}
s.Flush()
return l.created, report, nil
}
func parseDocument(src []byte, name string, report *Report) (*aetext.Node, error) {
doc, err := aetext.Parse(src, name)
if err != nil {
return nil, errs.Wrap(err, errs.SceneIOParse, "cannot parse the document")
}
if doc.Head != "scene" && doc.Head != "prefab" {
return nil, errs.New(errs.SceneIOParse, "unexpected document root",
log.F("path", name), log.F("head", doc.Head))
}
version := FormatVersion
if v, ok := doc.Attr("version"); ok {
version = int(v.AsInt())
}
if version > FormatVersion {
return nil, errs.New(errs.SceneIOVersion, "document was written by a newer engine",
log.F("path", name), log.F("version", version), log.F("supported", FormatVersion))
}
if version < FormatVersion {
migrated, err := Migrate(doc, version, FormatVersion)
if err != nil {
return nil, err
}
report.warn(name, doc.Pos, "document was migrated from version "+itoa(version))
doc = migrated
}
return doc, nil
}
func (l *loader) loadNode(n *aetext.Node, parent scene.NodeHandle) error {
var id ids.NodeID
if n.Label != "" && l.opt.KeepIDs {
parsed, err := ids.ParseNodeID(n.Label)
if err != nil {
if l.opt.Strict {
return errs.Wrap(err, errs.SceneIOParse, "malformed node id").With(log.F("path", l.name))
}
l.report.warn(l.name, n.Pos, "malformed node id, generating a new one")
} else {
id = parsed
}
}
name := "Node"
if v, ok := n.Attr("name"); ok {
name = v.Str
}
var h scene.NodeHandle
if id.IsZero() {
h = l.scene.Create(parent, name)
} else {
h = l.scene.CreateWithID(parent, name, id)
}
if h.IsZero() {
return errs.New(errs.SceneIOParse, "cannot create a node", log.F("path", l.name))
}
l.created = append(l.created, h)
l.nodeIDs[l.scene.ID(h)] = h
if v, ok := n.Attr("enabled"); ok {
l.scene.SetEnabled(h, v.Bool)
}
if v, ok := n.Attr("layers"); ok {
l.scene.SetLayers(h, uint64(v.AsInt()))
}
for _, block := range n.Blocks {
switch block.Head {
case "transform":
l.scene.SetLocal(h, readTransform(block))
case "component":
if err := l.loadComponent(h, block); err != nil {
return err
}
case "node":
if err := l.loadNode(block, h); err != nil {
return err
}
default:
l.report.warn(l.name, block.Pos, "unknown block "+block.Head)
}
}
return nil
}
func readTransform(n *aetext.Node) scene.Transform2D {
t := scene.IdentityTransform()
if v, ok := n.Field("position"); ok {
t.Position = readVec2(v, t.Position)
}
if v, ok := n.Field("rotation"); ok {
t.Rotation = float32(v.AsFloat())
}
if v, ok := n.Field("scale"); ok {
t.Scale = readVec2(v, t.Scale)
}
return t
}
func readVec2(v aetext.Value, def mathx.Vec2) mathx.Vec2 {
if v.Kind != aetext.VList || len(v.List) != 2 {
return def
}
return mathx.Vec2{X: float32(v.List[0].AsFloat()), Y: float32(v.List[1].AsFloat())}
}
func (l *loader) loadComponent(h scene.NodeHandle, n *aetext.Node) error {
typeName := n.Label
td, ok := l.reg.ByName(typeName)
if !ok {
if l.opt.Strict {
return errs.New(errs.SceneIOStrict, "unknown component type",
log.F("path", l.name), log.F("type", typeName))
}
l.storeUnknown(h, n)
l.report.Unknown = append(l.report.Unknown, typeName)
return nil
}
p, err := l.scene.AddByType(h, td.ID)
if err != nil {
return err
}
for _, f := range n.Fields {
fd, ok := td.Field(f.Name)
if !ok {
l.report.warn(l.name, f.Pos, "unknown field "+typeName+"."+f.Name)
continue
}
value, deferredTarget, err := l.convert(fd, f.Value, f.Pos)
if err != nil {
if l.opt.Strict {
return err
}
l.report.warn(l.name, f.Pos, err.Error())
continue
}
if !deferredTarget.IsZero() {
l.deferred = append(l.deferred, deferredRef{
node: h, typeID: td.ID, field: fd.ID, target: deferredTarget,
name: typeName, fname: fd.Name,
})
continue
}
if err := fd.Set(p, value); err != nil {
if l.opt.Strict {
return err
}
l.report.warn(l.name, f.Pos, err.Error())
}
}
return nil
}
func (l *loader) storeUnknown(h scene.NodeHandle, n *aetext.Node) {
u := scene.Add[Unknown](l.scene, h)
if u == nil {
return
}
version := 1
if v, ok := n.Attr("version"); ok {
version = int(v.AsInt())
}
u.Components = append(u.Components, UnknownComponent{
TypeName: n.Label,
Version: version,
Fields: n.Fields,
Blocks: n.Blocks,
})
}
func (l *loader) convert(fd *schema.FieldDescriptor, v aetext.Value, pos aetext.Pos) (schema.Value, ids.NodeID, error) {
switch fd.Kind {
case schema.KindBool:
return schema.Bool(v.Bool), ids.NodeID{}, nil
case schema.KindInt32:
return schema.Int32(int32(v.AsInt())), ids.NodeID{}, nil
case schema.KindInt64:
return schema.Int64(v.AsInt()), ids.NodeID{}, nil
case schema.KindFloat32:
return schema.Float32(float32(v.AsFloat())), ids.NodeID{}, nil
case schema.KindFloat64:
return schema.Float64(v.AsFloat()), ids.NodeID{}, nil
case schema.KindString:
return schema.String(v.Str), ids.NodeID{}, nil
case schema.KindVec2:
return schema.Vec2(readVec2(v, mathx.Vec2{})), ids.NodeID{}, nil
case schema.KindColor:
return schema.Color(unpackColor(v.Color)), ids.NodeID{}, nil
case schema.KindRect:
return schema.Rect(readRect(v)), ids.NodeID{}, nil
case schema.KindEnum:
if v.Kind == aetext.VIdent {
if n, ok := fd.EnumValueOf(v.Ident); ok {
return schema.Enum(n), ids.NodeID{}, nil
}
return schema.Value{}, ids.NodeID{}, errs.New(errs.SceneIOParse,
"unknown enum value "+v.Ident+" for field "+fd.Name)
}
return schema.Enum(v.AsInt()), ids.NodeID{}, nil
case schema.KindAssetRef:
if v.Str == "" {
return schema.Asset(ids.AssetID{}), ids.NodeID{}, nil
}
id, err := ids.ParseAssetID(v.Str)
if err != nil {
return schema.Value{}, ids.NodeID{}, err
}
if l.res != nil && !l.resolves(fd, id) {
l.report.Unresolved = append(l.report.Unresolved, Unresolved{
Asset: id, Type: fd.AssetKind, Field: fd.Name,
})
}
return schema.Asset(id), ids.NodeID{}, nil
case schema.KindNodeRef:
if v.Str == "" {
return schema.Node(ids.NodeID{}), ids.NodeID{}, nil
}
id, err := ids.ParseNodeID(v.Str)
if err != nil {
return schema.Value{}, ids.NodeID{}, err
}
return schema.Value{}, id, nil
case schema.KindList:
out := schema.Value{Kind: schema.KindList}
for _, item := range v.List {
elem := schema.FieldDescriptor{Kind: fd.Elem, Struct: fd.Struct, Enum: fd.Enum, Name: fd.Name}
ev, _, err := l.convert(&elem, item, pos)
if err != nil {
return schema.Value{}, ids.NodeID{}, err
}
out.List = append(out.List, ev)
}
return out, ids.NodeID{}, nil
case schema.KindStruct:
td, ok := l.reg.ByID(fd.Struct)
if !ok {
return schema.Value{}, ids.NodeID{}, errs.New(errs.SceneIOParse, "unregistered nested struct")
}
obj := &schema.Object{Type: td.ID, Fields: make([]schema.Value, len(td.Fields))}
for i := range td.Fields {
for _, f := range v.Obj {
if f.Name != td.Fields[i].Name {
continue
}
ev, _, err := l.convert(&td.Fields[i], f.Value, pos)
if err != nil {
return schema.Value{}, ids.NodeID{}, err
}
obj.Fields[i] = ev
}
}
return schema.Value{Kind: schema.KindStruct, Obj: obj}, ids.NodeID{}, nil
}
return schema.Value{}, ids.NodeID{}, errs.New(errs.SceneIOParse, "field kind cannot be read from text",
log.F("field", fd.Name))
}
func (l *loader) resolves(fd *schema.FieldDescriptor, id ids.AssetID) bool {
switch fd.AssetKind {
case "scene":
_, ok := l.res.Scene(id)
return ok
case "prefab":
_, ok := l.res.Prefab(id)
return ok
default:
_, ok := l.res.Texture(id)
return ok
}
}
func (l *loader) resolveDeferred() error {
for _, d := range l.deferred {
h, ok := l.nodeIDs[d.target]
if !ok {
h = l.scene.Handle(d.target)
}
if h.IsZero() {
if l.opt.Strict {
return errs.New(errs.SceneIOStrict, "node reference does not resolve",
log.F("path", l.name), log.F("field", d.name+"."+d.fname),
log.F("target", d.target.String()))
}
l.report.Unresolved = append(l.report.Unresolved, Unresolved{
Target: d.target, Type: d.name, Field: d.fname,
})
}
if err := l.scene.SetProperty(d.node, d.typeID, d.field, schema.Node(d.target)); err != nil {
l.report.warn(l.name, aetext.Pos{}, err.Error())
}
}
l.deferred = nil
return nil
}
func unpackColor(v uint32) mathx.Color {
return mathx.ColorRGBA8(
uint8(v>>24),
uint8(v>>16),
uint8(v>>8),
uint8(v),
)
}
func readRect(v aetext.Value) mathx.Rect {
if v.Kind != aetext.VList || len(v.List) != 4 {
return mathx.Rect{}
}
return mathx.Rect{
X: float32(v.List[0].AsFloat()),
Y: float32(v.List[1].AsFloat()),
W: float32(v.List[2].AsFloat()),
H: float32(v.List[3].AsFloat()),
}
}
func itoa(v int) string {
if v == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
neg := v < 0
if neg {
v = -v
}
for v > 0 {
i--
buf[i] = byte('0' + v%10)
v /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}