Files
sanspie 2744cff481
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 21m57s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
Stage 9.6c1: project refs and refactorings without the editor (D298)
2026-10-20 12:12:18 +03:00

447 lines
10 KiB
Go

package refs
import (
"time"
"aego/asset"
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/assetops"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/history"
"aego/editor/core/prefabs"
"aego/project/refactor"
prefs "aego/project/refs"
"aego/scene/prefab"
"aego/scene/scriptcomp"
"aego/schema"
"slices"
)
const RecoveryDir = refactor.RecoveryDir
type (
Ref = prefs.Ref
Place = prefs.Place
Missing = prefs.Missing
)
type Hit struct {
prefs.Hit
Open bool
}
type MissingNode struct {
prefs.MissingNode
Open bool
}
type PrefabInfo interface {
Info(d *document.SceneDocument, node ids.NodeID) (*prefabs.Info, bool)
}
type Service struct {
ctx *context.Editor
Prefabs PrefabInfo
Now func() time.Time
}
func New(ctx *context.Editor) *Service {
return &Service{ctx: ctx, Now: time.Now}
}
func (s *Service) source() prefs.Source {
if s.ctx.Pipeline == nil {
return nil
}
return s.ctx.Pipeline
}
func leafAny(v schema.Value, leaf func(schema.Value) bool) bool {
switch v.Kind {
case schema.KindAssetRef, schema.KindNodeRef:
return leaf(v)
case schema.KindList:
for _, it := range v.List {
if leafAny(it, leaf) {
return true
}
}
case schema.KindStruct:
if v.Obj != nil {
for _, f := range v.Obj.Fields {
if leafAny(f, leaf) {
return true
}
}
}
}
return false
}
func rewrite(v schema.Value, leaf func(schema.Value) (schema.Value, bool)) (schema.Value, bool) {
switch v.Kind {
case schema.KindAssetRef, schema.KindNodeRef:
return leaf(v)
case schema.KindList:
var out []schema.Value
for i, it := range v.List {
nv, ok := rewrite(it, leaf)
if !ok {
continue
}
if out == nil {
out = slices.Clone(v.List)
}
out[i] = nv
}
if out != nil {
v.List = out
return v, true
}
case schema.KindStruct:
if v.Obj == nil {
return v, false
}
var obj *schema.Object
for i, f := range v.Obj.Fields {
nv, ok := rewrite(f, leaf)
if !ok {
continue
}
if obj == nil {
obj = &schema.Object{Type: v.Obj.Type, Fields: slices.Clone(v.Obj.Fields)}
}
obj.Fields[i] = nv
}
if obj != nil {
v.Obj = obj
return v, true
}
}
return v, false
}
func isAsset(id ids.AssetID) func(schema.Value) bool {
return func(v schema.Value) bool { return v.Kind == schema.KindAssetRef && v.Asset == id }
}
func swapAsset(from, to ids.AssetID) func(schema.Value) (schema.Value, bool) {
return func(v schema.Value) (schema.Value, bool) {
if v.Kind == schema.KindAssetRef && v.Asset == from {
return schema.Asset(to), true
}
return v, false
}
}
type visitFn func(id ids.NodeID, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, owner string, v schema.Value)
func (s *Service) walk(d *document.SceneDocument, leaf func(schema.Value) bool, overrides bool, fn visitFn) {
sc := d.Scene()
reg := sc.Registry()
root := sc.Root()
unknown, hasUnknown := reg.ByName("engine.Unknown")
for _, h := range sc.PreOrder() {
if h == root || !sc.IsAlive(h) {
continue
}
id := sc.ID(h)
var info *prefabs.Info
if overrides && s.Prefabs != nil {
if inf, ok := s.Prefabs.Info(d, id); ok && !inf.Missing {
info = inf
}
}
visit := func(td *schema.TypeDescriptor, owner string) {
for i := range td.Fields {
fd := &td.Fields[i]
v, err := d.Props.Get(id, td.ID, fd.ID)
if err != nil || !leafAny(v, leaf) {
continue
}
if info != nil && !info.Added(id) && !info.Overridden(id, owner, fd.Name) {
continue
}
fn(id, td, fd, owner, v)
}
}
sc.ComponentsOf(h, func(t schema.TypeID) {
if (hasUnknown && t == unknown.ID) || t == scriptcomp.ScriptTypeID {
return
}
if td, ok := reg.ByID(t); ok {
visit(td, td.FullName)
}
})
if scs, ok := scriptcomp.ScriptsOf(sc, h); ok {
for i := range scs.Attachments {
name := scs.Attachments[i].TypeName
if td, ok := reg.ByName(name); ok && td.Kind == schema.TypeScript {
visit(td, prefab.ScriptOwner(name))
}
}
}
}
}
func (s *Service) livePlaces(d *document.SceneDocument, leaf func(schema.Value) bool) []Place {
var out []Place
sc := d.Scene()
s.walk(d, leaf, false, func(id ids.NodeID, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, _ string, _ schema.Value) {
out = append(out, Place{Node: id, Path: sc.Path(sc.Handle(id)), Owner: td.FullName, Field: fd.Name})
})
return out
}
func (s *Service) Referrers(target ids.AssetID) []Ref {
src := s.source()
out := prefs.Referrers(s.ctx.Project, src, target)
if src == nil {
return out
}
pl := s.ctx.Pipeline
seen := map[string]bool{}
for _, r := range out {
seen[r.Path] = true
}
for _, d := range s.ctx.Docs.Docs() {
if seen[d.Path] || !d.Dirty() || d.IsPlay() || len(s.livePlaces(d, isAsset(target))) == 0 {
continue
}
r := Ref{Path: d.Path, Kind: asset.ArtScene}
if d.IsPrefab() {
r.Kind = asset.ArtPrefab
}
if id, ok := pl.Lookup(d.Path); ok {
r.Asset = id
}
out = append(out, r)
}
prefs.Sort(out)
return out
}
func (s *Service) Find(target ids.AssetID) []Hit {
var out []Hit
for _, r := range s.Referrers(target) {
if !r.Project {
if d := s.ctx.Docs.Find(r.Path); d != nil {
out = append(out, Hit{Hit: prefs.Hit{Ref: r, Places: s.livePlaces(d, isAsset(target))}, Open: true})
continue
}
}
out = append(out, Hit{Hit: prefs.FileHit(s.source(), r, target)})
}
return out
}
type Job struct {
From ids.AssetID
To ids.AssetID
Docs []string
Files []string
Backup string
Project bool
plan *refactor.Replacement
root string
abs func(string) string
changed []string
}
func (s *Service) replaceLive(d *document.SceneDocument, from, to ids.AssetID) (int, error) {
type edit struct {
node ids.NodeID
t schema.TypeID
f schema.FieldID
v schema.Value
}
var edits []edit
s.walk(d, isAsset(from), true, func(id ids.NodeID, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, _ string, v schema.Value) {
if fd.Flags.Has(schema.FlagReadOnly) {
return
}
if nv, ok := rewrite(v, swapAsset(from, to)); ok {
edits = append(edits, edit{node: id, t: td.ID, f: fd.ID, v: nv})
}
})
if len(edits) == 0 {
return 0, nil
}
return len(edits), apply(d.History, "Replace References", func(tx *history.Transaction) error {
for _, e := range edits {
if err := d.Props.Set(tx, []ids.NodeID{e.node}, e.t, e.f, e.v); err != nil {
return err
}
}
return nil
})
}
func apply(h *history.History, label string, fn func(*history.Transaction) error) error {
tx := h.Begin(label)
if err := fn(tx); err != nil {
tx.Abort()
return err
}
tx.Commit()
return nil
}
func (s *Service) Begin(from, to ids.AssetID) (*Job, error) {
pl := s.ctx.Pipeline
if pl == nil || s.ctx.Project == nil {
return nil, errs.New(errs.EditorCommand, "the asset pipeline is not available")
}
if err := refactor.CheckReplace(pl, from, to); err != nil {
return nil, err
}
j := &Job{From: from, To: to, root: s.ctx.Project.Root, abs: pl.Abs, Backup: refactor.BackupDir(s.Now())}
for _, r := range s.Referrers(from) {
switch {
case r.Project:
j.Project = true
if !to.IsZero() {
if rel, ok := pl.PathOf(to); ok {
assetops.SetStartup(s.ctx, to, rel)
}
}
case s.ctx.Docs.Find(r.Path) != nil:
d := s.ctx.Docs.Find(r.Path)
if d.IsPlay() {
return nil, errs.New(errs.EditorCommand, "stop Play before replacing references")
}
if _, err := s.replaceLive(d, from, to); err != nil {
return nil, errs.Wrap(err, errs.EditorCommand, "cannot update "+r.Path)
}
j.Docs = append(j.Docs, r.Path)
default:
j.Files = append(j.Files, r.Path)
}
}
j.plan = &refactor.Replacement{From: from, To: to, Project: j.Project, Files: j.Files}
return j, nil
}
func (j *Job) Rewrite() error {
changed, err := j.plan.Write(j.root, j.abs, j.Backup)
j.changed = changed
return err
}
func (j *Job) Changed() []string {
return j.changed
}
func (s *Service) Finish(j *Job) error {
batch, err := refactor.Reimport(s.ctx.Pipeline, j.changed)
s.ctx.ApplyBatch(batch)
return err
}
func (s *Service) Replace(from, to ids.AssetID) (*Job, error) {
j, err := s.Begin(from, to)
if err != nil {
return nil, err
}
if err := j.Rewrite(); err != nil {
return j, err
}
return j, s.Finish(j)
}
func (s *Service) MissingAssets() []Missing {
src := s.source()
if src == nil {
return nil
}
return prefs.MissingAssets(s.ctx.Project, src)
}
func (s *Service) MissingNodes() []MissingNode {
pl := s.ctx.Pipeline
if pl == nil {
return nil
}
var out []MissingNode
for _, a := range pl.Assets() {
if a.Kind != asset.ArtScene && a.Kind != asset.ArtPrefab {
continue
}
if d := s.ctx.Docs.Find(a.Path); d != nil {
out = append(out, s.liveMissingNodes(d)...)
continue
}
for _, m := range prefs.FileMissingNodes(pl, a.Path) {
out = append(out, MissingNode{MissingNode: m})
}
}
return out
}
func (s *Service) liveMissingNodes(d *document.SceneDocument) []MissingNode {
sc := d.Scene()
dead := func(v schema.Value) bool {
if v.Kind != schema.KindNodeRef || v.Node.IsZero() {
return false
}
h := sc.Handle(v.Node)
return h.IsZero() || !sc.IsAlive(h)
}
var out []MissingNode
s.walk(d, dead, false, func(id ids.NodeID, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, _ string, v schema.Value) {
var target ids.NodeID
leafAny(v, func(x schema.Value) bool {
if dead(x) {
target = x.Node
return true
}
return false
})
m := prefs.MissingNode{Scene: d.Path, Node: id, Path: sc.Path(sc.Handle(id)), Owner: td.FullName, Field: fd.Name, Target: target}
out = append(out, MissingNode{MissingNode: m, Open: true})
})
return out
}
func (s *Service) ClearNode(m MissingNode) error {
d, err := s.ctx.Docs.Open(m.Scene)
if err != nil {
return err
}
is := func(v schema.Value) bool { return v.Kind == schema.KindNodeRef && v.Node == m.Target }
type edit struct {
node ids.NodeID
t schema.TypeID
f schema.FieldID
v schema.Value
}
var edits []edit
s.walk(d, is, true, func(id ids.NodeID, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, _ string, v schema.Value) {
if fd.Flags.Has(schema.FlagReadOnly) {
return
}
nv, ok := rewrite(v, func(x schema.Value) (schema.Value, bool) {
if is(x) {
return schema.Node(ids.NodeID{}), true
}
return x, false
})
if ok {
edits = append(edits, edit{node: id, t: td.ID, f: fd.ID, v: nv})
}
})
if len(edits) == 0 {
return nil
}
return apply(d.History, "Clear Missing Node Reference", func(tx *history.Transaction) error {
for _, e := range edits {
if err := d.Props.Set(tx, []ids.NodeID{e.node}, e.t, e.f, e.v); err != nil {
return err
}
}
return nil
})
}