use gdscript_base::TextRange;
use rustc_hash::FxHashMap;
use smol_str::SmolStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeIdx(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtId(pub SmolStr);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SceneKind {
Scene,
Resource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SceneModel {
pub kind: SceneKind,
pub format: Option<u8>,
pub uid: Option<SmolStr>,
pub script_class: Option<SmolStr>,
pub resource_type: Option<SmolStr>,
pub ext_resources: FxHashMap<ExtId, ExtResource>,
pub sub_resources: FxHashMap<ExtId, SubResource>,
pub nodes: Vec<SceneNode>,
pub root: Option<NodeIdx>,
pub by_path: FxHashMap<SmolStr, NodeIdx>,
pub unique_nodes: FxHashMap<SmolStr, NodeIdx>,
pub problems: Vec<SceneProblem>,
child_index: FxHashMap<(NodeIdx, SmolStr), NodeIdx>,
children: FxHashMap<NodeIdx, Vec<NodeIdx>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SceneNode {
pub name: SmolStr,
pub decl_type: Option<SmolStr>,
pub parent_path: Option<SmolStr>,
pub parent_idx: Option<NodeIdx>,
pub script: Option<ExtId>,
pub instance: Option<ExtId>,
pub instance_is_inherited_root: bool,
pub instance_placeholder: bool,
pub unique_name_in_owner: bool,
pub header_span: TextRange,
pub name_span: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtResource {
pub res_type: SmolStr,
pub path: Option<SmolStr>,
pub uid: Option<SmolStr>,
pub span: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubResource {
pub res_type: SmolStr,
pub span: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SceneProblem {
BinaryResource,
UnknownTag {
at: TextRange,
},
MalformedHeader {
at: TextRange,
},
MissingExtField {
at: TextRange,
},
UnknownExtResource {
id: ExtId,
at: TextRange,
},
MultipleRoots {
roots: Vec<NodeIdx>,
},
NoRoot,
DanglingParent {
node: NodeIdx,
parent_path: SmolStr,
},
}
impl SceneModel {
#[must_use]
pub(crate) fn empty(kind: SceneKind) -> Self {
Self {
kind,
format: None,
uid: None,
script_class: None,
resource_type: None,
ext_resources: FxHashMap::default(),
sub_resources: FxHashMap::default(),
nodes: Vec::new(),
root: None,
by_path: FxHashMap::default(),
unique_nodes: FxHashMap::default(),
problems: Vec::new(),
child_index: FxHashMap::default(),
children: FxHashMap::default(),
}
}
pub(crate) fn set_indices(
&mut self,
child_index: FxHashMap<(NodeIdx, SmolStr), NodeIdx>,
children: FxHashMap<NodeIdx, Vec<NodeIdx>>,
) {
self.child_index = child_index;
self.children = children;
}
#[must_use]
pub fn node(&self, idx: NodeIdx) -> Option<&SceneNode> {
self.nodes.get(idx.0 as usize)
}
#[must_use]
pub fn resolve_path(&self, path: &str) -> Option<NodeIdx> {
self.resolve_path_from(self.root?, path)
}
#[must_use]
pub fn resolve_path_from(&self, base: NodeIdx, path: &str) -> Option<NodeIdx> {
let p = path.trim();
if p.is_empty() || p == "." {
return Some(base);
}
if p.starts_with('/') {
return None; }
let mut cur = base;
for seg in p.split('/') {
if seg.is_empty() || seg == "." {
continue;
}
if seg == ".." {
return None; }
cur = self.step_segment(cur, seg)?;
}
Some(cur)
}
fn step_segment(&self, cur: NodeIdx, seg: &str) -> Option<NodeIdx> {
if let Some(unique) = seg.strip_prefix('%') {
self.unique_nodes.get(unique).copied()
} else {
self.child_index.get(&(cur, SmolStr::new(seg))).copied()
}
}
#[must_use]
pub fn resolve_unique(&self, path: &str) -> Option<NodeIdx> {
self.resolve_path_from(self.root?, &Self::with_unique_head(path))
}
fn with_unique_head(path: &str) -> String {
if path.starts_with('%') {
path.to_owned()
} else {
format!("%{path}")
}
}
#[must_use]
pub fn node_with_script(&self, script_path: &str) -> Option<NodeIdx> {
self.nodes.iter().enumerate().find_map(|(i, n)| {
let ext = self.ext_resources.get(n.script.as_ref()?)?;
(ext.path.as_deref() == Some(script_path))
.then(|| NodeIdx(u32::try_from(i).unwrap_or(u32::MAX)))
})
}
pub fn children_of(&self, idx: Option<NodeIdx>) -> impl Iterator<Item = (NodeIdx, &SceneNode)> {
idx.or(self.root)
.and_then(|t| self.children.get(&t))
.into_iter()
.flatten()
.filter_map(move |&c| self.node(c).map(|n| (c, n)))
}
#[must_use]
pub fn classify_path_from(&self, base: NodeIdx, path: &str) -> NodePathResolution {
let p = path.trim();
if p.is_empty() || p == "." {
return NodePathResolution::Resolved(base);
}
if p.starts_with('/') {
return NodePathResolution::Escaped; }
let mut cur = base;
for seg in p.split('/') {
if seg.is_empty() || seg == "." {
continue;
}
if seg == ".." {
return NodePathResolution::Escaped;
}
match self.step_segment(cur, seg) {
Some(next) => cur = next,
None => {
return if !seg.starts_with('%') && self.descends_from_instance(Some(cur)) {
NodePathResolution::IntoInstance
} else {
NodePathResolution::Missing
};
}
}
}
NodePathResolution::Resolved(cur)
}
#[must_use]
pub fn classify_unique(&self, path: &str) -> NodePathResolution {
match self.root {
Some(root) => self.classify_path_from(root, &Self::with_unique_head(path)),
None => NodePathResolution::Missing,
}
}
pub(crate) fn descends_from_instance(&self, start: Option<NodeIdx>) -> bool {
let mut cur = start;
let mut guard = 0u32;
while let Some(c) = cur {
let Some(node) = self.nodes.get(c.0 as usize) else {
break;
};
if node.instance.is_some()
|| node.instance_placeholder
|| node.instance_is_inherited_root
{
return true;
}
cur = node.parent_idx;
guard += 1;
if guard > 4096 {
break;
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodePathResolution {
Resolved(NodeIdx),
Escaped,
IntoInstance,
Missing,
}