use gdscript_base::TextRange;
use rustc_hash::FxHashMap;
use smol_str::SmolStr;
use crate::model::{
ExtId, ExtResource, NodeIdx, SceneKind, SceneModel, SceneNode, SceneProblem, SubResource,
};
#[must_use]
pub fn parse_scene(text: &str) -> SceneModel {
if binary_magic(text) {
let mut m = SceneModel::empty(SceneKind::Scene);
m.problems.push(SceneProblem::BinaryResource);
return m;
}
let mut p = Parser::new(text);
p.run();
p.build_tree();
p.model
}
fn binary_magic(text: &str) -> bool {
let b = text.as_bytes();
let mut i = 0;
while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\r' | b'\n') {
i += 1;
}
let rest = &b[i..];
rest.starts_with(b"RSRC") || rest.starts_with(b"RSCC")
}
type Span = (usize, usize);
#[derive(Default)]
struct HeaderAttrs {
name: Option<Span>,
typ: Option<Span>,
parent: Option<Span>,
instance: Option<Span>,
instance_placeholder: Option<Span>,
format: Option<Span>,
uid: Option<Span>,
script_class: Option<Span>,
id: Option<Span>,
path: Option<Span>,
}
impl HeaderAttrs {
fn set(&mut self, key: &str, value: Span) {
let slot = match key {
"name" => &mut self.name,
"type" => &mut self.typ,
"parent" => &mut self.parent,
"instance" => &mut self.instance,
"instance_placeholder" => &mut self.instance_placeholder,
"format" => &mut self.format,
"uid" => &mut self.uid,
"script_class" => &mut self.script_class,
"id" => &mut self.id,
"path" => &mut self.path,
_ => return, };
*slot = Some(value);
}
}
struct Parser<'a> {
src: &'a str,
bytes: &'a [u8],
pos: usize,
model: SceneModel,
}
impl<'a> Parser<'a> {
fn new(src: &'a str) -> Self {
Self {
src,
bytes: src.as_bytes(),
pos: 0,
model: SceneModel::empty(SceneKind::Scene),
}
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn bump(&mut self) {
self.pos += 1;
}
fn at_eof(&self) -> bool {
self.pos >= self.bytes.len()
}
fn skip_inline_ws(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t')) {
self.bump();
}
}
fn skip_trivia(&mut self) {
loop {
match self.peek() {
Some(b' ' | b'\t' | b'\r' | b'\n') => self.bump(),
Some(b';') => self.skip_to_eol(),
_ => break,
}
}
}
fn skip_to_eol(&mut self) {
while !matches!(self.peek(), None | Some(b'\n')) {
self.bump();
}
if self.peek() == Some(b'\n') {
self.bump();
}
}
fn read_ident(&mut self) -> Option<SmolStr> {
let start = self.pos;
while matches!(self.peek(), Some(b) if b.is_ascii_alphanumeric() || b == b'_' || b == b'/')
{
self.bump();
}
if self.pos == start {
None
} else {
self.src.get(start..self.pos).map(SmolStr::new)
}
}
fn consume_value(&mut self) -> Span {
self.skip_inline_ws();
let start = self.pos;
match self.peek() {
Some(b'"') => self.consume_quoted(),
Some(b'&' | b'@') => {
self.bump();
if self.peek() == Some(b'"') {
self.consume_quoted();
} else {
self.consume_bare();
}
}
Some(b'[' | b'{' | b'(') => self.consume_balanced(),
Some(b'#') => self.consume_color(),
Some(_) => {
self.consume_bare();
while matches!(self.peek(), Some(b'(' | b'[')) {
self.consume_balanced();
}
}
None => {}
}
(start, self.pos)
}
fn consume_quoted(&mut self) {
self.bump(); loop {
match self.peek() {
None => break,
Some(b'\\') => {
self.bump();
self.bump(); }
Some(b'"') => {
self.bump();
break;
}
Some(_) => self.bump(),
}
}
}
fn consume_balanced(&mut self) {
let mut depth: u32 = 0;
loop {
match self.peek() {
None => break,
Some(b'"') => self.consume_quoted(),
Some(b'#') => self.consume_color(), Some(b';') => self.skip_to_eol(),
Some(b'(' | b'[' | b'{') => {
depth += 1;
self.bump();
}
Some(b')' | b']' | b'}') => {
self.bump();
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
Some(_) => self.bump(),
}
}
}
fn consume_color(&mut self) {
self.bump(); while matches!(self.peek(), Some(b) if b.is_ascii_hexdigit()) {
self.bump();
}
}
fn consume_bare(&mut self) {
while matches!(
self.peek(),
Some(b) if b.is_ascii_alphanumeric() || matches!(b, b'_' | b'+' | b'-' | b'.')
) {
self.bump();
}
}
fn read_header(&mut self) -> (Option<SmolStr>, HeaderAttrs, bool) {
self.bump(); self.skip_inline_ws();
let tag = self.read_ident();
let mut attrs = HeaderAttrs::default();
let mut closed = false;
loop {
self.skip_inline_ws();
match self.peek() {
Some(b']') => {
self.bump();
closed = true;
break;
}
None | Some(b'\n') => break,
Some(_) => {
let Some(key) = self.read_ident() else {
self.bump(); continue;
};
self.skip_inline_ws();
if self.peek() != Some(b'=') {
continue; }
self.bump(); let value = self.consume_value();
attrs.set(&key, value);
}
}
}
(tag, attrs, closed)
}
fn consume_body(&mut self, is_node: bool) -> (Option<ExtId>, bool) {
let mut script = None;
let mut unique = false;
loop {
self.skip_trivia();
match self.peek() {
None | Some(b'[') => break, Some(_) => {}
}
let Some(key) = self.read_ident() else {
self.skip_to_eol(); continue;
};
self.skip_inline_ws();
if self.peek() != Some(b'=') {
self.skip_to_eol();
continue;
}
self.bump(); let (vs, ve) = self.consume_value();
if is_node {
match key.as_str() {
"script" => script = self.extract_ext_id(vs, ve),
"unique_name_in_owner" => {
unique = self.src.get(vs..ve).is_some_and(|v| v.trim() == "true");
}
_ => {}
}
}
self.skip_to_eol();
}
(script, unique)
}
fn extract_string(&self, span: Span) -> Option<SmolStr> {
let raw = self.src.get(span.0..span.1)?.trim();
if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
Some(SmolStr::new(unescape(&raw[1..raw.len() - 1])))
} else if raw.is_empty() {
None
} else {
Some(SmolStr::new(raw))
}
}
fn extract_u8(&self, span: Span) -> Option<u8> {
self.extract_string(span)?.trim().parse().ok()
}
fn extract_ext_id(&self, start: usize, end: usize) -> Option<ExtId> {
let v = self.src.get(start..end)?;
let open = v.find('(')?;
if v.get(..open)?.trim() != "ExtResource" {
return None;
}
let close = v.rfind(')')?;
if close <= open {
return None;
}
let inner = v.get(open + 1..close)?.trim().trim_matches('"').trim();
(!inner.is_empty()).then(|| ExtId(SmolStr::new(inner)))
}
fn run(&mut self) {
loop {
self.skip_trivia();
if self.at_eof() {
break;
}
if self.peek() == Some(b'[') {
self.section();
} else {
self.skip_to_eol(); }
}
}
fn section(&mut self) {
let start = self.pos;
let (tag, attrs, closed) = self.read_header();
let header_span = TextRange::new(to_u32(start), to_u32(self.pos));
if !closed {
self.model
.problems
.push(SceneProblem::MalformedHeader { at: header_span });
}
match tag.as_deref() {
Some("gd_scene") => {
self.model.kind = SceneKind::Scene;
self.read_scene_header(&attrs);
self.consume_body(false);
}
Some("gd_resource") => {
self.model.kind = SceneKind::Resource;
self.read_resource_header(&attrs);
self.consume_body(false);
}
Some("ext_resource") => {
self.add_ext_resource(&attrs, header_span);
self.consume_body(false);
}
Some("sub_resource") => {
self.add_sub_resource(&attrs, header_span);
self.consume_body(false);
}
Some("node") => self.add_node(&attrs, header_span),
Some("connection" | "editable" | "resource") => {
self.consume_body(false); }
Some(_) => {
self.model
.problems
.push(SceneProblem::UnknownTag { at: header_span });
self.consume_body(false);
}
None => {
self.model
.problems
.push(SceneProblem::MalformedHeader { at: header_span });
self.consume_body(false);
}
}
}
fn read_scene_header(&mut self, a: &HeaderAttrs) {
self.model.format = a.format.and_then(|s| self.extract_u8(s));
self.model.uid = a.uid.and_then(|s| self.extract_string(s));
self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
}
fn read_resource_header(&mut self, a: &HeaderAttrs) {
self.model.format = a.format.and_then(|s| self.extract_u8(s));
self.model.uid = a.uid.and_then(|s| self.extract_string(s));
self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
self.model.resource_type = a.typ.and_then(|s| self.extract_string(s));
}
fn add_ext_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
let res_type = a.typ.and_then(|s| self.extract_string(s));
let path = a.path.and_then(|s| self.extract_string(s));
let uid = a.uid.and_then(|s| self.extract_string(s));
let id = a.id.and_then(|s| self.extract_string(s));
match id {
Some(id) => {
if res_type.is_none() || path.is_none() {
self.model
.problems
.push(SceneProblem::MissingExtField { at: span });
}
self.model.ext_resources.insert(
ExtId(id),
ExtResource {
res_type: res_type.unwrap_or_default(),
path,
uid,
span,
},
);
}
None => self
.model
.problems
.push(SceneProblem::MissingExtField { at: span }),
}
}
fn add_sub_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
let res_type = a
.typ
.and_then(|s| self.extract_string(s))
.unwrap_or_default();
if let Some(id) = a.id.and_then(|s| self.extract_string(s)) {
self.model
.sub_resources
.insert(ExtId(id), SubResource { res_type, span });
}
}
fn add_node(&mut self, a: &HeaderAttrs, header_span: TextRange) {
let name = a
.name
.and_then(|s| self.extract_string(s))
.unwrap_or_default();
let name_span = a
.name
.map_or(header_span, |(s, e)| TextRange::new(to_u32(s), to_u32(e)));
let decl_type = a.typ.and_then(|s| self.extract_string(s));
let parent_path = a.parent.and_then(|s| self.extract_string(s));
let instance = a.instance.and_then(|(s, e)| self.extract_ext_id(s, e));
let instance_placeholder = a.instance_placeholder.is_some();
let (script, unique_name_in_owner) = self.consume_body(true);
self.model.nodes.push(SceneNode {
name,
decl_type,
parent_path,
parent_idx: None,
script,
instance,
instance_is_inherited_root: false,
instance_placeholder,
unique_name_in_owner,
header_span,
name_span,
});
}
fn build_tree(&mut self) {
let n = self.model.nodes.len();
if n == 0 {
return;
}
let roots: Vec<NodeIdx> = (0..n)
.filter(|&i| self.model.nodes[i].parent_path.is_none())
.map(|i| NodeIdx(to_u32(i)))
.collect();
self.model.root = roots.first().copied();
if roots.len() > 1 {
self.model.problems.push(SceneProblem::MultipleRoots {
roots: roots.clone(),
});
} else if roots.is_empty() {
self.model.problems.push(SceneProblem::NoRoot);
}
let root = self.model.root;
let mut child_index: FxHashMap<(NodeIdx, SmolStr), NodeIdx> = FxHashMap::default();
let mut children: FxHashMap<NodeIdx, Vec<NodeIdx>> = FxHashMap::default();
let mut full_paths: Vec<SmolStr> = vec![SmolStr::default(); n];
for i in 0..n {
let idx = NodeIdx(to_u32(i));
let parent_path = self.model.nodes[i].parent_path.clone();
let name = self.model.nodes[i].name.clone();
if Some(idx) == root && self.model.nodes[i].instance.is_some() {
self.model.nodes[i].instance_is_inherited_root = true;
}
let parent_idx = match parent_path.as_deref() {
None => None,
Some(".") => root,
Some(p) => match walk_path(root, p, &child_index) {
Walk::Resolved(found) => Some(found),
Walk::Escaped => None,
Walk::Missed(deepest) => {
if !self.model.descends_from_instance(deepest) {
self.model.problems.push(SceneProblem::DanglingParent {
node: idx,
parent_path: SmolStr::new(p),
});
}
None
}
},
};
self.model.nodes[i].parent_idx = parent_idx;
if let Some(p) = parent_idx {
child_index.entry((p, name.clone())).or_insert(idx);
children.entry(p).or_default().push(idx);
let pfp = &full_paths[p.0 as usize];
let fp = if pfp.is_empty() {
name
} else {
SmolStr::new(format!("{pfp}/{name}"))
};
full_paths[i] = fp.clone();
self.model.by_path.entry(fp).or_insert(idx);
}
}
for i in 0..n {
if self.model.nodes[i].unique_name_in_owner {
self.model
.unique_nodes
.entry(self.model.nodes[i].name.clone())
.or_insert(NodeIdx(to_u32(i)));
}
}
for i in 0..n {
let span = self.model.nodes[i].header_span;
let refs = [
self.model.nodes[i].script.clone(),
self.model.nodes[i].instance.clone(),
];
for id in refs.into_iter().flatten() {
if !self.model.ext_resources.contains_key(&id) {
self.model
.problems
.push(SceneProblem::UnknownExtResource { id, at: span });
}
}
}
self.model.set_indices(child_index, children);
}
}
enum Walk {
Resolved(NodeIdx),
Escaped,
Missed(Option<NodeIdx>),
}
fn walk_path(
root: Option<NodeIdx>,
path: &str,
child_index: &FxHashMap<(NodeIdx, SmolStr), NodeIdx>,
) -> Walk {
if path.starts_with('/') {
return Walk::Escaped; }
let Some(mut cur) = root else {
return Walk::Missed(None);
};
for seg in path.split('/') {
if seg.is_empty() || seg == "." {
continue;
}
if seg == ".." {
return Walk::Escaped; }
match child_index.get(&(cur, SmolStr::new(seg))) {
Some(&next) => cur = next,
None => return Walk::Missed(Some(cur)),
}
}
Walk::Resolved(cur)
}
fn unescape(s: &str) -> String {
if !s.contains('\\') {
return s.to_owned();
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('r') => out.push('\r'),
Some(other) => out.push(other), None => out.push('\\'),
}
}
out
}
fn to_u32(v: usize) -> u32 {
u32::try_from(v).unwrap_or(u32::MAX)
}