326 lines
6.9 KiB
Go
326 lines
6.9 KiB
Go
package prefabs
|
|
|
|
import (
|
|
"os"
|
|
"slices"
|
|
|
|
"aego/asset"
|
|
"aego/asset/pipeline"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/history"
|
|
"aego/editor/core/history/commands"
|
|
"aego/project/refactor"
|
|
"aego/scene"
|
|
"aego/scene/prefab"
|
|
"aego/scene/sceneio"
|
|
)
|
|
|
|
type fileChange struct {
|
|
id ids.AssetID
|
|
rel string
|
|
before []byte
|
|
after []byte
|
|
}
|
|
|
|
type source struct {
|
|
doc document.ID
|
|
node ids.NodeID
|
|
before []byte
|
|
after []byte
|
|
}
|
|
|
|
type target struct {
|
|
doc *document.SceneDocument
|
|
node ids.NodeID
|
|
parent ids.NodeID
|
|
index int
|
|
block []byte
|
|
}
|
|
|
|
type group struct {
|
|
svc *Service
|
|
label string
|
|
files []fileChange
|
|
src *source
|
|
applied bool
|
|
}
|
|
|
|
type groupCmd struct {
|
|
g *group
|
|
}
|
|
|
|
func (c *groupCmd) Label() string {
|
|
return c.g.label
|
|
}
|
|
|
|
func (c *groupCmd) Do(history.Target) error {
|
|
if c.g.applied {
|
|
return nil
|
|
}
|
|
return c.g.run(true)
|
|
}
|
|
|
|
func (c *groupCmd) Undo(history.Target) error {
|
|
if !c.g.applied {
|
|
return nil
|
|
}
|
|
return c.g.run(false)
|
|
}
|
|
|
|
func (c *groupCmd) Merge(history.Command) bool {
|
|
return false
|
|
}
|
|
|
|
func readFile(abs string) ([]byte, error) {
|
|
data, err := os.ReadFile(abs)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.EditorDocument, "cannot read the prefab")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (s *Service) files(plan *refactor.ApplyPlan) ([]fileChange, error) {
|
|
if s.ctx.Pipeline == nil {
|
|
return nil, errs.New(errs.EditorCommand, "the asset pipeline is not available")
|
|
}
|
|
edits, err := plan.Edits(s.ctx.Types.Schema(), s.ctx.Pipeline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]fileChange, len(edits))
|
|
for i, e := range edits {
|
|
out[i] = fileChange{id: e.ID, rel: e.Path, before: e.Before, after: e.After}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) reimport(list []ids.AssetID) error {
|
|
if len(list) == 0 {
|
|
return nil
|
|
}
|
|
s.writing = true
|
|
defer func() { s.writing = false }()
|
|
batch, err := s.ctx.Pipeline.Reimport(list...)
|
|
s.ctx.ApplyBatch(batch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, id := range list {
|
|
if ferr, bad := batch.Failed[id]; bad {
|
|
return ferr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) dependsOn(lib *sceneio.Library, id ids.AssetID, changed map[ids.AssetID]bool) bool {
|
|
seen := map[ids.AssetID]bool{}
|
|
var visit func(ids.AssetID) bool
|
|
var walk func(*prefab.Node) bool
|
|
visit = func(id ids.AssetID) bool {
|
|
if changed[id] {
|
|
return true
|
|
}
|
|
if seen[id] {
|
|
return false
|
|
}
|
|
seen[id] = true
|
|
p, ok := lib.Prefab(id)
|
|
return ok && walk(p)
|
|
}
|
|
walk = func(n *prefab.Node) bool {
|
|
if n.Instance != nil {
|
|
if visit(n.Instance.Prefab) {
|
|
return true
|
|
}
|
|
for _, a := range n.Instance.Overrides.AddNodes {
|
|
if walk(a.Node) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for _, c := range n.Children {
|
|
if walk(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return visit(id)
|
|
}
|
|
|
|
func covered(sc *scene.Scene, h scene.NodeHandle, taken []scene.NodeHandle) bool {
|
|
for p := sc.Parent(h); !p.IsZero(); p = sc.Parent(p) {
|
|
if slices.Contains(taken, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *Service) affected(lib *sceneio.Library, changed map[ids.AssetID]bool, blocks bool) []target {
|
|
var out []target
|
|
for _, d := range s.ctx.Docs.Docs() {
|
|
sc := d.Scene()
|
|
var taken []scene.NodeHandle
|
|
for _, h := range sc.PreOrder() {
|
|
pi, ok := sceneio.InstanceOf(sc, h)
|
|
if !ok || !pi.Owner || !sc.IsAlive(h) || covered(sc, h, taken) || !s.dependsOn(lib, pi.Prefab, changed) {
|
|
continue
|
|
}
|
|
taken = append(taken, h)
|
|
t := target{doc: d, node: sc.ID(h), parent: sc.ID(sc.Parent(h)), index: commands.ChildIndex(sc, h)}
|
|
if blocks {
|
|
t.block = sceneio.SaveSubtreeWith(sc, h, lib)
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (g *group) changed() map[ids.AssetID]bool {
|
|
out := make(map[ids.AssetID]bool, len(g.files))
|
|
for _, f := range g.files {
|
|
out[f.id] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (g *group) ids() []ids.AssetID {
|
|
out := make([]ids.AssetID, len(g.files))
|
|
for i, f := range g.files {
|
|
out[i] = f.id
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (g *group) write(forward bool) error {
|
|
pl := g.svc.ctx.Pipeline
|
|
for i, f := range g.files {
|
|
data := f.after
|
|
if !forward {
|
|
data = f.before
|
|
}
|
|
if err := document.WriteAtomic(pl.Abs(f.rel), data); err != nil {
|
|
for _, w := range g.files[:i] {
|
|
back := w.before
|
|
if !forward {
|
|
back = w.after
|
|
}
|
|
document.WriteAtomic(pl.Abs(w.rel), back)
|
|
}
|
|
g.svc.reimport(g.ids())
|
|
return errs.Wrap(err, errs.EditorDocument, "cannot write "+f.rel)
|
|
}
|
|
}
|
|
if err := g.svc.reimport(g.ids()); err != nil {
|
|
for _, f := range g.files {
|
|
back := f.before
|
|
if !forward {
|
|
back = f.after
|
|
}
|
|
document.WriteAtomic(pl.Abs(f.rel), back)
|
|
}
|
|
g.svc.reimport(g.ids())
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *group) run(forward bool) error {
|
|
svc := g.svc
|
|
from := svc.lib()
|
|
plan := svc.affected(from, g.changed(), true)
|
|
if err := g.write(forward); err != nil {
|
|
return err
|
|
}
|
|
to := svc.lib()
|
|
var done []target
|
|
for _, t := range plan {
|
|
data := t.block
|
|
if g.src != nil && t.doc.ID == g.src.doc && t.node == g.src.node {
|
|
data = g.src.after
|
|
if !forward {
|
|
data = g.src.before
|
|
}
|
|
}
|
|
if _, err := commands.ReplaceNode(t.doc.Scene(), t.node, t.parent, t.index, data, to); err != nil {
|
|
g.write(!forward)
|
|
back := svc.lib()
|
|
for _, r := range done {
|
|
commands.ReplaceNode(r.doc.Scene(), r.node, r.parent, r.index, r.block, back)
|
|
}
|
|
return err
|
|
}
|
|
done = append(done, t)
|
|
}
|
|
g.applied = forward
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) commit(label string, files []fileChange, src *source) error {
|
|
if len(files) == 0 {
|
|
s.ctx.Notify(context.LevelInfo, "The prefab already matches")
|
|
return nil
|
|
}
|
|
g := &group{svc: s, label: label, files: files, src: src}
|
|
var hosts []*document.SceneDocument
|
|
for _, t := range s.affected(s.lib(), g.changed(), false) {
|
|
if !slices.Contains(hosts, t.doc) {
|
|
hosts = append(hosts, t.doc)
|
|
}
|
|
}
|
|
if src != nil {
|
|
if d := s.ctx.Docs.Get(src.doc); d != nil && !slices.Contains(hosts, d) {
|
|
hosts = append([]*document.SceneDocument{d}, hosts...)
|
|
}
|
|
}
|
|
for _, d := range hosts {
|
|
if reason, locked := d.History.Locked(); locked {
|
|
return errs.New(errs.EditorHistory, reason)
|
|
}
|
|
}
|
|
if len(hosts) == 0 {
|
|
return g.run(true)
|
|
}
|
|
for _, d := range hosts {
|
|
if err := d.History.Exec(&groupCmd{g: g}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) External(b *pipeline.Batch) func() {
|
|
if s.writing || s.ctx.Docs.InPlay() {
|
|
return nil
|
|
}
|
|
changed := map[ids.AssetID]bool{}
|
|
for _, e := range b.Imported {
|
|
if e.Kind == asset.ArtPrefab {
|
|
changed[e.ID] = true
|
|
}
|
|
}
|
|
if len(changed) == 0 {
|
|
return nil
|
|
}
|
|
plan := s.affected(s.lib(), changed, true)
|
|
if len(plan) == 0 {
|
|
return nil
|
|
}
|
|
return func() {
|
|
lib := s.lib()
|
|
for _, t := range plan {
|
|
if _, err := commands.ReplaceNode(t.doc.Scene(), t.node, t.parent, t.index, t.block, lib); err != nil {
|
|
s.ctx.Notify(context.LevelError, "cannot refresh the prefab instances in "+t.doc.Path+": "+err.Error())
|
|
}
|
|
}
|
|
clear(s.infos)
|
|
}
|
|
}
|