Files
sanspie cffa66514b
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
Stage 9.6c2: prefab apply and data editing without the editor (D298)
2026-10-20 13:05:10 +03:00

355 lines
9.1 KiB
Go

package refactor
import (
"bytes"
"maps"
"os"
"slices"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/project"
"aego/project/refs"
"aego/scene"
"aego/scene/mutate"
"aego/scene/prefab"
"aego/scene/sceneio"
"aego/schema"
)
type PrefabChange struct {
Node ids.NodeID
Key prefab.Key
Level prefab.Level
Prop prefab.PropKey
Nearest bool
}
type ApplyPlan struct {
Root ids.NodeID
Prefab ids.AssetID
Target ids.AssetID
Models map[ids.AssetID]*prefab.Node
Before []byte
After []byte
}
type PrefabResult struct {
Edits []Edit
Result *mutate.Result
}
func PrefabLibrary(reg *schema.Registry, src refs.Source, res asset.Resolver) (*sceneio.Library, []error) {
lib := sceneio.NewLibrary(reg, res)
var bad []error
for _, a := range src.Assets() {
if a.Kind != asset.ArtPrefab {
continue
}
data, err := os.ReadFile(src.Abs(a.Path))
if err != nil {
bad = append(bad, errs.Wrap(err, errs.VFSIO, "cannot read "+a.Path))
continue
}
n, err := sceneio.ParsePrefab(data, a.Path, reg)
if err != nil {
bad = append(bad, err)
continue
}
lib.Set(a.ID, n)
}
return lib, bad
}
func Locate(s *scene.Scene, node ids.NodeID, key prefab.Key) (scene.NodeHandle, *sceneio.PrefabInstance, prefab.Key, error) {
h := s.Handle(node)
if h.IsZero() || !s.IsAlive(h) {
return scene.NodeHandle{}, nil, "", errs.New(errs.SceneDeadNode, "node is not in the scene")
}
root, pi, ok := sceneio.OwnerRoot(s, h)
if !ok {
return scene.NodeHandle{}, nil, "", errs.New(errs.EditorCommand, s.Name(h)+" is not part of a prefab instance")
}
if pi.Missing {
return scene.NodeHandle{}, nil, "", errs.New(errs.EditorCommand, "the prefab of "+s.Name(root)+" is missing")
}
if key == "" {
if k, ok := pi.Keys[node]; ok {
key = k
} else {
key = prefab.LocalKey(node)
}
}
return root, pi, key, nil
}
func addPrefix(base *prefab.Node, parent prefab.Key) string {
if n := base.Find(parent); n != nil && n != base && n.Origin != nil {
return string(parent)
}
return string(parent.Instance())
}
func PlanApply(s *scene.Scene, lib *sceneio.Library, c PrefabChange) (*ApplyPlan, error) {
if c.Level == prefab.LevelOrphan {
return nil, errs.New(errs.EditorCommand, "unused overrides cannot be applied")
}
root, pi, key, err := Locate(s, c.Node, c.Key)
if err != nil {
return nil, err
}
inst, err := sceneio.Overrides(s, root, lib)
if err != nil {
return nil, err
}
sel, rest := prefab.Pick{Level: c.Level, Key: key, Prop: c.Prop}.Split(inst.Overrides, pi.Keys[s.ID(root)])
if sel.Empty() {
return nil, mutate.ErrNothing
}
p, ok := lib.Prefab(pi.Prefab)
if !ok {
return nil, errs.New(errs.EditorCommand, "the prefab is not loaded")
}
base, _, err := prefab.Expand(lib, p, nil)
if err != nil {
return nil, err
}
b := newBaker(lib)
if err := b.bake(pi.Prefab, sel, c.Nearest); err != nil {
return nil, err
}
keep := maps.Clone(inst.IDs)
for _, a := range sel.AddNodes {
prefix := addPrefix(base, a.Parent)
a.Node.Walk(func(n, _ *prefab.Node) {
if n.Instance != nil {
return
}
k := prefab.LocalKey(n.Local)
if prefix != "" {
k = prefab.Key(prefix) + "/" + k
}
keep[k] = n.Local
})
}
plan := &ApplyPlan{
Root: s.ID(root),
Prefab: pi.Prefab,
Target: pi.Prefab,
Models: b.edits,
Before: sceneio.SaveSubtreeWith(s, root, lib),
After: sceneio.EncodeInstance(s.Registry(), s.ID(root), &prefab.Instance{Prefab: pi.Prefab, Overrides: rest, IDs: keep}),
}
if c.Nearest {
if id, ok := b.Nearest(pi.Prefab, key); ok {
plan.Target = id
}
}
return plan, nil
}
func (p *ApplyPlan) Edits(reg *schema.Registry, src refs.Source) ([]Edit, error) {
var out []Edit
for id, m := range p.Models {
rel, ok := src.PathOf(id)
if !ok {
return nil, errs.New(errs.EditorDocument, "prefab "+id.Short()+" is not in the project")
}
before, err := os.ReadFile(src.Abs(rel))
if err != nil {
return nil, errs.Wrap(err, errs.EditorDocument, "cannot read the prefab")
}
after := sceneio.EncodePrefab(reg, m)
if bytes.Equal(before, after) {
continue
}
out = append(out, Edit{ID: id, Path: rel, Before: before, After: after})
}
slices.SortFunc(out, func(a, b Edit) int { return strings.Compare(a.Path, b.Path) })
return out, nil
}
func instanceBlocks(s *scene.Scene, lib *sceneio.Library, skip ids.NodeID) (map[ids.NodeID][]byte, []ids.NodeID) {
blocks := map[ids.NodeID][]byte{}
var order []ids.NodeID
under := s.Handle(skip)
for _, h := range s.PreOrder() {
if h == s.Root() || !s.IsAlive(h) {
continue
}
inside := false
for q := h; !q.IsZero(); q = s.Parent(q) {
if q == under {
inside = true
break
}
}
if inside {
continue
}
if pi, ok := sceneio.InstanceOf(s, h); ok && pi.Owner && !pi.Missing {
id := s.ID(h)
blocks[id] = sceneio.SaveSubtreeWith(s, h, lib)
order = append(order, id)
}
}
return blocks, order
}
func ApplyPrefab(doc *mutate.Document, lib *sceneio.Library, src refs.Source, c PrefabChange) (*PrefabResult, error) {
plan, err := PlanApply(doc.Scene, lib, c)
if err != nil {
return nil, err
}
edits, err := plan.Edits(doc.Scene.Registry(), src)
if err != nil {
return nil, err
}
blocks, others := instanceBlocks(doc.Scene, lib, plan.Root)
prev := map[ids.AssetID]*prefab.Node{}
for id, m := range plan.Models {
if old, ok := lib.Prefab(id); ok {
prev[id] = old
}
lib.Set(id, m)
}
ops := []mutate.Op{mutate.Replace("#"+plan.Root.String(), string(plan.After))}
for _, id := range others {
ops = append(ops, mutate.Replace("#"+id.String(), string(blocks[id])))
}
res, err := doc.Apply(mutate.Tx{Label: "Apply Prefab", Ops: ops})
if err != nil {
for id := range plan.Models {
if old, ok := prev[id]; ok {
lib.Set(id, old)
} else {
lib.Unset(id)
}
}
return nil, err
}
n := len(res.Inverse.Ops)
res.Inverse.Ops[n-1].Fragment = string(plan.Before)
for i, id := range others {
res.Inverse.Ops[n-2-i].Fragment = string(blocks[id])
}
return &PrefabResult{Edits: edits, Result: res}, nil
}
func CheckCreate(s *scene.Scene, h scene.NodeHandle) error {
if h == s.Root() {
return errs.New(errs.EditorCommand, "the scene root cannot become a prefab")
}
if pi, ok := sceneio.InstanceOf(s, h); ok && pi.Owner {
return errs.New(errs.EditorCommand, s.Name(h)+" is already a prefab instance, use Create Variant")
}
if root, _, ok := sceneio.OwnerRoot(s, h); ok && root != h {
return errs.New(errs.EditorCommand, s.Name(h)+" belongs to a prefab instance, unpack it first")
}
return nil
}
func sceneIDOf(s *scene.Scene, k prefab.Key) (ids.NodeID, bool) {
head, rest, nested := strings.Cut(string(k), "/")
first, err := ids.ParseNodeID(head)
if err != nil {
return ids.NodeID{}, false
}
if !nested {
return first, !s.Handle(first).IsZero()
}
if pi, ok := sceneio.InstanceOf(s, s.Handle(first)); ok {
for sid, kk := range pi.Keys {
if string(kk) == rest {
return sid, true
}
}
}
if local, ok := k.Local(); ok && !s.Handle(local).IsZero() {
return local, true
}
return ids.NodeID{}, false
}
func LinkBlock(s *scene.Scene, h scene.NodeHandle, lib *sceneio.Library, id ids.AssetID) ([]byte, error) {
p, ok := lib.Prefab(id)
if !ok {
return nil, errs.New(errs.EditorCommand, "the new prefab did not import")
}
tree, _, err := prefab.Expand(lib, p, nil)
if err != nil {
return nil, err
}
inst := &prefab.Instance{Prefab: id, IDs: map[prefab.Key]ids.NodeID{}}
tree.Walk(func(n, _ *prefab.Node) {
if sid, ok := sceneIDOf(s, n.Key); ok {
inst.IDs[n.Key] = sid
}
})
return sceneio.EncodeInstance(s.Registry(), s.ID(h), inst), nil
}
func CreatePrefab(pl *pipeline.Pipeline, doc *mutate.Document, lib *sceneio.Library, nodeRef, rel string) (ids.AssetID, *mutate.Result, error) {
s := doc.Scene
h, err := doc.Node(nodeRef)
if err != nil {
return ids.AssetID{}, nil, err
}
if err := CheckCreate(s, h); err != nil {
return ids.AssetID{}, nil, err
}
abs := pl.Abs(rel)
if _, err := os.Stat(abs); err == nil {
return ids.AssetID{}, nil, errs.New(errs.VFSBadPath, rel+" already exists")
}
src := sceneio.SaveSubtree(s, h, true)
if err := project.WriteAtomic(abs, src); err != nil {
return ids.AssetID{}, nil, errs.Wrap(err, errs.VFSIO, "cannot write "+rel)
}
undo := func() {
os.Remove(abs)
os.Remove(abs + pipeline.MetaSuffix)
pl.Scan()
}
if _, _, err := pl.Scan(); err != nil {
undo()
return ids.AssetID{}, nil, err
}
id, ok := pl.Lookup(rel)
if !ok {
undo()
return ids.AssetID{}, nil, errs.New(errs.VFSBadPath, rel+" has no importer or is outside assets, scenes, scripts and prefabs", log.F("path", rel))
}
batch, err := pl.Import(id)
if err == nil && batch != nil {
if ferr, bad := batch.Failed[id]; bad {
err = ferr
}
}
if err != nil {
undo()
return ids.AssetID{}, nil, err
}
n, err := sceneio.ParsePrefab(src, rel, s.Registry())
if err != nil {
undo()
return ids.AssetID{}, nil, err
}
lib.Set(id, n)
after, err := LinkBlock(s, h, lib, id)
if err == nil {
var res *mutate.Result
res, err = doc.Apply(mutate.Tx{Label: "Create Prefab", Ops: []mutate.Op{mutate.Replace("#"+s.ID(h).String(), string(after))}})
if err == nil {
return id, res, nil
}
}
lib.Unset(id)
undo()
return ids.AssetID{}, nil, err
}