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

347 lines
7.0 KiB
Go

package refs
import (
"os"
"path"
"slices"
"sort"
"strconv"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/ids"
"aego/format/aetext"
"aego/project"
"aego/scene/mutate"
)
var (
vAsset = aetext.AssetRef("").Kind
vNode = aetext.NodeRef("").Kind
)
type Source interface {
Assets() []pipeline.AssetInfo
Entry(id ids.AssetID) (asset.Entry, bool)
PathOf(id ids.AssetID) (string, bool)
Lookup(path string) (ids.AssetID, bool)
Grave(id ids.AssetID) (pipeline.Grave, bool)
Abs(rel string) string
}
type Ref struct {
Asset ids.AssetID
Path string
Kind asset.ArtifactKind
Project bool
}
type Place struct {
Node ids.NodeID
Path string
Owner string
Field string
}
func (p Place) String() string {
var parts []string
for _, s := range []string{p.Path, p.Owner, p.Field} {
if s != "" {
parts = append(parts, s)
}
}
return strings.Join(parts, " · ")
}
type Hit struct {
Ref Ref
Places []Place
Err error
}
func Token(id ids.AssetID) []byte {
return []byte(mutate.FormatValue(aetext.AssetRef(id.String())))
}
func Sort(list []Ref) {
sort.SliceStable(list, func(i, j int) bool {
if list[i].Project != list[j].Project {
return list[i].Project
}
return list[i].Path < list[j].Path
})
}
func Referrers(p *project.Project, src Source, target ids.AssetID) []Ref {
var out []Ref
if p != nil && !target.IsZero() && p.MainScene == target {
out = append(out, Ref{Project: true, Path: project.FileName})
}
if src == nil {
return out
}
for _, a := range src.Assets() {
if a.ID == target {
continue
}
if e, ok := src.Entry(a.ID); ok && slices.Contains(e.Deps, target) {
out = append(out, Ref{Asset: a.ID, Path: a.Path, Kind: a.Kind})
}
}
Sort(out)
return out
}
func Places(data []byte, name string, target ids.AssetID) ([]Place, error) {
doc, err := aetext.Parse(data, name)
if err != nil {
return nil, err
}
return textPlaces(doc, vAsset, target.String()), nil
}
func FileHit(src Source, r Ref, target ids.AssetID) Hit {
h := Hit{Ref: r}
if r.Project {
h.Places = []Place{{Field: "mainScene"}}
return h
}
data, err := os.ReadFile(src.Abs(r.Path))
if err != nil {
h.Err = err
return h
}
h.Places, h.Err = Places(data, r.Path, target)
return h
}
func Find(p *project.Project, src Source, target ids.AssetID) []Hit {
var out []Hit
for _, r := range Referrers(p, src, target) {
out = append(out, FileHit(src, r, target))
}
return out
}
func textPlaces(doc *aetext.Node, kind aetext.ValueKind, target string) []Place {
var out []Place
var visit func(n *aetext.Node, pl Place)
var value func(v aetext.Value, field string, pl Place)
value = func(v aetext.Value, field string, pl Place) {
switch {
case v.Kind == kind && v.Str == target:
pl.Field = field
out = append(out, pl)
case v.Kind == aetext.VList:
for i, it := range v.List {
value(it, field+"["+strconv.Itoa(i)+"]", pl)
}
case v.Kind == aetext.VStruct:
for _, f := range v.Obj {
value(f.Value, field+"."+f.Name, pl)
}
}
}
visit = func(n *aetext.Node, pl Place) {
switch n.Head {
case "node":
if id, err := ids.ParseNodeID(n.Label); err == nil {
pl.Node = id
}
name := n.Label
if v, ok := n.Attr("name"); ok {
name = v.Str
}
pl.Path = path.Join(pl.Path, name)
pl.Owner = ""
case "component", "script":
pl.Owner = n.Label
case "override":
if v, ok := n.Attr("prop"); ok {
pl.Owner = v.Str
}
}
for _, f := range n.Attrs {
value(f.Value, f.Name, pl)
}
for _, f := range n.Fields {
value(f.Value, f.Name, pl)
}
for _, b := range n.Blocks {
visit(b, pl)
}
}
visit(doc, Place{})
return out
}
func Known(src Source, id ids.AssetID) bool {
if id.IsZero() {
return true
}
if _, ok := asset.LookupBuiltin(id); ok {
return true
}
if _, ok := src.PathOf(id); ok {
return true
}
if _, buried := src.Grave(id); buried {
return false
}
_, ok := src.Entry(id)
return ok
}
type Missing struct {
ID ids.AssetID
Grave pipeline.Grave
Buried bool
Refs []Ref
Suggest ids.AssetID
Path string
}
func (m Missing) Label() string {
if m.Buried && m.Grave.Path != "" {
return m.Grave.Path
}
return m.ID.String()
}
func MissingAssets(p *project.Project, src Source) []Missing {
by := map[ids.AssetID]*Missing{}
var order []ids.AssetID
add := func(id ids.AssetID, r Ref) {
m := by[id]
if m == nil {
m = &Missing{ID: id}
by[id] = m
order = append(order, id)
}
m.Refs = append(m.Refs, r)
}
if p != nil && !Known(src, p.MainScene) {
add(p.MainScene, Ref{Project: true, Path: project.FileName})
}
assets := src.Assets()
for _, a := range assets {
e, ok := src.Entry(a.ID)
if !ok {
continue
}
for _, d := range e.Deps {
if !Known(src, d) {
add(d, Ref{Asset: a.ID, Path: a.Path, Kind: a.Kind})
}
}
}
out := make([]Missing, 0, len(order))
for _, id := range order {
m := by[id]
if g, ok := src.Grave(id); ok {
m.Grave, m.Buried = g, true
base := strings.ToLower(path.Base(g.Path))
for _, a := range assets {
if a.Kind == g.Kind && strings.ToLower(path.Base(a.Path)) == base {
m.Suggest, m.Path = a.ID, a.Path
break
}
}
}
out = append(out, *m)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].Label() < out[j].Label() })
return out
}
type MissingNode struct {
Scene string
Node ids.NodeID
Path string
Owner string
Field string
Target ids.NodeID
}
func FileMissingNodes(src Source, rel string) []MissingNode {
data, err := os.ReadFile(src.Abs(rel))
if err != nil {
return nil
}
doc, err := aetext.Parse(data, rel)
if err != nil {
return nil
}
return MissingNodesIn(doc, rel)
}
func MissingNodes(src Source) []MissingNode {
var out []MissingNode
for _, a := range src.Assets() {
if a.Kind == asset.ArtScene || a.Kind == asset.ArtPrefab {
out = append(out, FileMissingNodes(src, a.Path)...)
}
}
return out
}
func MissingNodesIn(doc *aetext.Node, scene string) []MissingNode {
defined := map[string]bool{}
var collect func(n *aetext.Node)
collect = func(n *aetext.Node) {
if n.Head == "node" {
defined[n.Label] = true
}
for _, b := range n.Blocks {
collect(b)
}
}
collect(doc)
var refs []string
var scan func(v aetext.Value)
scan = func(v aetext.Value) {
switch v.Kind {
case vNode:
refs = append(refs, v.Str)
case aetext.VList:
for _, it := range v.List {
scan(it)
}
case aetext.VStruct:
for _, f := range v.Obj {
scan(f.Value)
}
}
}
var walk func(n *aetext.Node)
walk = func(n *aetext.Node) {
for _, f := range n.Fields {
scan(f.Value)
}
for _, f := range n.Attrs {
scan(f.Value)
}
for _, b := range n.Blocks {
walk(b)
}
}
walk(doc)
var out []MissingNode
seen := map[string]bool{}
for _, id := range refs {
if id == "" || defined[id] || seen[id] {
continue
}
nid, err := ids.ParseNodeID(id)
if err != nil || nid.IsZero() {
continue
}
seen[id] = true
for _, pl := range textPlaces(doc, vNode, id) {
out = append(out, MissingNode{Scene: scene, Node: pl.Node, Path: pl.Path, Owner: pl.Owner, Field: pl.Field, Target: nid})
}
}
return out
}