1use gdscript_base::TextRange;
14use rustc_hash::FxHashMap;
15use smol_str::SmolStr;
16
17use crate::model::{
18 ExtId, ExtResource, NodeIdx, SceneKind, SceneModel, SceneNode, SceneProblem, SubResource,
19};
20
21#[must_use]
23pub fn parse_scene(text: &str) -> SceneModel {
24 if binary_magic(text) {
25 let mut m = SceneModel::empty(SceneKind::Scene);
26 m.problems.push(SceneProblem::BinaryResource);
27 return m;
28 }
29 let mut p = Parser::new(text);
30 p.run();
31 p.build_tree();
32 p.model
33}
34
35fn binary_magic(text: &str) -> bool {
37 let b = text.as_bytes();
38 let mut i = 0;
39 while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\r' | b'\n') {
40 i += 1;
41 }
42 let rest = &b[i..];
43 rest.starts_with(b"RSRC") || rest.starts_with(b"RSCC")
44}
45
46type Span = (usize, usize);
48
49#[derive(Default)]
51struct HeaderAttrs {
52 name: Option<Span>,
53 typ: Option<Span>,
54 parent: Option<Span>,
55 instance: Option<Span>,
56 instance_placeholder: Option<Span>,
57 format: Option<Span>,
58 uid: Option<Span>,
59 script_class: Option<Span>,
60 id: Option<Span>,
61 path: Option<Span>,
62}
63
64impl HeaderAttrs {
65 fn set(&mut self, key: &str, value: Span) {
66 let slot = match key {
67 "name" => &mut self.name,
68 "type" => &mut self.typ,
69 "parent" => &mut self.parent,
70 "instance" => &mut self.instance,
71 "instance_placeholder" => &mut self.instance_placeholder,
72 "format" => &mut self.format,
73 "uid" => &mut self.uid,
74 "script_class" => &mut self.script_class,
75 "id" => &mut self.id,
76 "path" => &mut self.path,
77 _ => return, };
79 *slot = Some(value);
80 }
81}
82
83struct Parser<'a> {
84 src: &'a str,
85 bytes: &'a [u8],
86 pos: usize,
87 model: SceneModel,
88}
89
90impl<'a> Parser<'a> {
91 fn new(src: &'a str) -> Self {
92 Self {
93 src,
94 bytes: src.as_bytes(),
95 pos: 0,
96 model: SceneModel::empty(SceneKind::Scene),
97 }
98 }
99
100 fn peek(&self) -> Option<u8> {
103 self.bytes.get(self.pos).copied()
104 }
105
106 fn bump(&mut self) {
107 self.pos += 1;
108 }
109
110 fn at_eof(&self) -> bool {
111 self.pos >= self.bytes.len()
112 }
113
114 fn skip_inline_ws(&mut self) {
115 while matches!(self.peek(), Some(b' ' | b'\t')) {
116 self.bump();
117 }
118 }
119
120 fn skip_trivia(&mut self) {
122 loop {
123 match self.peek() {
124 Some(b' ' | b'\t' | b'\r' | b'\n') => self.bump(),
125 Some(b';') => self.skip_to_eol(),
126 _ => break,
127 }
128 }
129 }
130
131 fn skip_to_eol(&mut self) {
132 while !matches!(self.peek(), None | Some(b'\n')) {
133 self.bump();
134 }
135 if self.peek() == Some(b'\n') {
136 self.bump();
137 }
138 }
139
140 fn read_ident(&mut self) -> Option<SmolStr> {
142 let start = self.pos;
143 while matches!(self.peek(), Some(b) if b.is_ascii_alphanumeric() || b == b'_' || b == b'/')
144 {
145 self.bump();
146 }
147 if self.pos == start {
148 None
149 } else {
150 self.src.get(start..self.pos).map(SmolStr::new)
151 }
152 }
153
154 fn consume_value(&mut self) -> Span {
159 self.skip_inline_ws();
160 let start = self.pos;
161 match self.peek() {
162 Some(b'"') => self.consume_quoted(),
163 Some(b'&' | b'@') => {
164 self.bump();
165 if self.peek() == Some(b'"') {
166 self.consume_quoted();
167 } else {
168 self.consume_bare();
169 }
170 }
171 Some(b'[' | b'{' | b'(') => self.consume_balanced(),
172 Some(b'#') => self.consume_color(),
173 Some(_) => {
174 self.consume_bare();
175 while matches!(self.peek(), Some(b'(' | b'[')) {
177 self.consume_balanced();
178 }
179 }
180 None => {}
181 }
182 (start, self.pos)
183 }
184
185 fn consume_quoted(&mut self) {
187 self.bump(); loop {
189 match self.peek() {
190 None => break,
191 Some(b'\\') => {
192 self.bump();
193 self.bump(); }
195 Some(b'"') => {
196 self.bump();
197 break;
198 }
199 Some(_) => self.bump(),
200 }
201 }
202 }
203
204 fn consume_balanced(&mut self) {
207 let mut depth: u32 = 0;
208 loop {
209 match self.peek() {
210 None => break,
211 Some(b'"') => self.consume_quoted(),
212 Some(b'#') => self.consume_color(), Some(b';') => self.skip_to_eol(),
214 Some(b'(' | b'[' | b'{') => {
215 depth += 1;
216 self.bump();
217 }
218 Some(b')' | b']' | b'}') => {
219 self.bump();
220 depth = depth.saturating_sub(1);
221 if depth == 0 {
222 break;
223 }
224 }
225 Some(_) => self.bump(),
226 }
227 }
228 }
229
230 fn consume_color(&mut self) {
232 self.bump(); while matches!(self.peek(), Some(b) if b.is_ascii_hexdigit()) {
234 self.bump();
235 }
236 }
237
238 fn consume_bare(&mut self) {
240 while matches!(
241 self.peek(),
242 Some(b) if b.is_ascii_alphanumeric() || matches!(b, b'_' | b'+' | b'-' | b'.')
243 ) {
244 self.bump();
245 }
246 }
247
248 fn read_header(&mut self) -> (Option<SmolStr>, HeaderAttrs, bool) {
253 self.bump(); self.skip_inline_ws();
255 let tag = self.read_ident();
256 let mut attrs = HeaderAttrs::default();
257 let mut closed = false;
258 loop {
259 self.skip_inline_ws();
260 match self.peek() {
261 Some(b']') => {
262 self.bump();
263 closed = true;
264 break;
265 }
266 None | Some(b'\n') => break,
268 Some(_) => {
269 let Some(key) = self.read_ident() else {
270 self.bump(); continue;
272 };
273 self.skip_inline_ws();
274 if self.peek() != Some(b'=') {
275 continue; }
277 self.bump(); let value = self.consume_value();
279 attrs.set(&key, value);
280 }
281 }
282 }
283 (tag, attrs, closed)
284 }
285
286 fn consume_body(&mut self, is_node: bool) -> (Option<ExtId>, bool) {
290 let mut script = None;
291 let mut unique = false;
292 loop {
293 self.skip_trivia();
294 match self.peek() {
295 None | Some(b'[') => break, Some(_) => {}
297 }
298 let Some(key) = self.read_ident() else {
299 self.skip_to_eol(); continue;
301 };
302 self.skip_inline_ws();
303 if self.peek() != Some(b'=') {
304 self.skip_to_eol();
305 continue;
306 }
307 self.bump(); let (vs, ve) = self.consume_value();
309 if is_node {
310 match key.as_str() {
311 "script" => script = self.extract_ext_id(vs, ve),
312 "unique_name_in_owner" => {
313 unique = self.src.get(vs..ve).is_some_and(|v| v.trim() == "true");
314 }
315 _ => {}
316 }
317 }
318 self.skip_to_eol();
319 }
320 (script, unique)
321 }
322
323 fn extract_string(&self, span: Span) -> Option<SmolStr> {
328 let raw = self.src.get(span.0..span.1)?.trim();
329 if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
330 Some(SmolStr::new(unescape(&raw[1..raw.len() - 1])))
331 } else if raw.is_empty() {
332 None
333 } else {
334 Some(SmolStr::new(raw))
335 }
336 }
337
338 fn extract_u8(&self, span: Span) -> Option<u8> {
340 self.extract_string(span)?.trim().parse().ok()
341 }
342
343 fn extract_ext_id(&self, start: usize, end: usize) -> Option<ExtId> {
348 let v = self.src.get(start..end)?;
349 let open = v.find('(')?;
350 if v.get(..open)?.trim() != "ExtResource" {
351 return None;
352 }
353 let close = v.rfind(')')?;
354 if close <= open {
355 return None;
356 }
357 let inner = v.get(open + 1..close)?.trim().trim_matches('"').trim();
358 (!inner.is_empty()).then(|| ExtId(SmolStr::new(inner)))
359 }
360
361 fn run(&mut self) {
364 loop {
365 self.skip_trivia();
366 if self.at_eof() {
367 break;
368 }
369 if self.peek() == Some(b'[') {
370 self.section();
371 } else {
372 self.skip_to_eol(); }
374 }
375 }
376
377 fn section(&mut self) {
378 let start = self.pos;
379 let (tag, attrs, closed) = self.read_header();
380 let header_span = TextRange::new(to_u32(start), to_u32(self.pos));
381 if !closed {
382 self.model
383 .problems
384 .push(SceneProblem::MalformedHeader { at: header_span });
385 }
387 match tag.as_deref() {
388 Some("gd_scene") => {
389 self.model.kind = SceneKind::Scene;
390 self.read_scene_header(&attrs);
391 self.consume_body(false);
392 }
393 Some("gd_resource") => {
394 self.model.kind = SceneKind::Resource;
395 self.read_resource_header(&attrs);
396 self.consume_body(false);
397 }
398 Some("ext_resource") => {
399 self.add_ext_resource(&attrs, header_span);
400 self.consume_body(false);
401 }
402 Some("sub_resource") => {
403 self.add_sub_resource(&attrs, header_span);
404 self.consume_body(false);
405 }
406 Some("node") => self.add_node(&attrs, header_span),
407 Some("connection" | "editable" | "resource") => {
408 self.consume_body(false); }
410 Some(_) => {
411 self.model
412 .problems
413 .push(SceneProblem::UnknownTag { at: header_span });
414 self.consume_body(false);
415 }
416 None => {
417 self.model
418 .problems
419 .push(SceneProblem::MalformedHeader { at: header_span });
420 self.consume_body(false);
421 }
422 }
423 }
424
425 fn read_scene_header(&mut self, a: &HeaderAttrs) {
426 self.model.format = a.format.and_then(|s| self.extract_u8(s));
427 self.model.uid = a.uid.and_then(|s| self.extract_string(s));
428 self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
429 }
430
431 fn read_resource_header(&mut self, a: &HeaderAttrs) {
432 self.model.format = a.format.and_then(|s| self.extract_u8(s));
433 self.model.uid = a.uid.and_then(|s| self.extract_string(s));
434 self.model.script_class = a.script_class.and_then(|s| self.extract_string(s));
435 self.model.resource_type = a.typ.and_then(|s| self.extract_string(s));
436 }
437
438 fn add_ext_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
439 let res_type = a.typ.and_then(|s| self.extract_string(s));
440 let path = a.path.and_then(|s| self.extract_string(s));
441 let uid = a.uid.and_then(|s| self.extract_string(s));
442 let id = a.id.and_then(|s| self.extract_string(s));
443 match id {
444 Some(id) => {
445 if res_type.is_none() || path.is_none() {
446 self.model
447 .problems
448 .push(SceneProblem::MissingExtField { at: span });
449 }
450 self.model.ext_resources.insert(
451 ExtId(id),
452 ExtResource {
453 res_type: res_type.unwrap_or_default(),
454 path,
455 uid,
456 span,
457 },
458 );
459 }
460 None => self
461 .model
462 .problems
463 .push(SceneProblem::MissingExtField { at: span }),
464 }
465 }
466
467 fn add_sub_resource(&mut self, a: &HeaderAttrs, span: TextRange) {
468 let res_type = a
469 .typ
470 .and_then(|s| self.extract_string(s))
471 .unwrap_or_default();
472 if let Some(id) = a.id.and_then(|s| self.extract_string(s)) {
473 self.model
474 .sub_resources
475 .insert(ExtId(id), SubResource { res_type, span });
476 }
477 }
478
479 fn add_node(&mut self, a: &HeaderAttrs, header_span: TextRange) {
480 let name = a
481 .name
482 .and_then(|s| self.extract_string(s))
483 .unwrap_or_default();
484 let name_span = a
485 .name
486 .map_or(header_span, |(s, e)| TextRange::new(to_u32(s), to_u32(e)));
487 let decl_type = a.typ.and_then(|s| self.extract_string(s));
488 let parent_path = a.parent.and_then(|s| self.extract_string(s));
489 let instance = a.instance.and_then(|(s, e)| self.extract_ext_id(s, e));
490 let instance_placeholder = a.instance_placeholder.is_some();
491 let (script, unique_name_in_owner) = self.consume_body(true);
492 self.model.nodes.push(SceneNode {
493 name,
494 decl_type,
495 parent_path,
496 parent_idx: None,
497 script,
498 instance,
499 instance_is_inherited_root: false,
500 instance_placeholder,
501 unique_name_in_owner,
502 header_span,
503 name_span,
504 });
505 }
506
507 fn build_tree(&mut self) {
510 let n = self.model.nodes.len();
511 if n == 0 {
512 return;
513 }
514 let roots: Vec<NodeIdx> = (0..n)
516 .filter(|&i| self.model.nodes[i].parent_path.is_none())
517 .map(|i| NodeIdx(to_u32(i)))
518 .collect();
519 self.model.root = roots.first().copied();
520 if roots.len() > 1 {
521 self.model.problems.push(SceneProblem::MultipleRoots {
522 roots: roots.clone(),
523 });
524 } else if roots.is_empty() {
525 self.model.problems.push(SceneProblem::NoRoot);
526 }
527 let root = self.model.root;
528
529 let mut child_index: FxHashMap<(NodeIdx, SmolStr), NodeIdx> = FxHashMap::default();
532 let mut children: FxHashMap<NodeIdx, Vec<NodeIdx>> = FxHashMap::default();
533 let mut full_paths: Vec<SmolStr> = vec![SmolStr::default(); n];
534
535 for i in 0..n {
536 let idx = NodeIdx(to_u32(i));
537 let parent_path = self.model.nodes[i].parent_path.clone();
538 let name = self.model.nodes[i].name.clone();
539
540 if Some(idx) == root && self.model.nodes[i].instance.is_some() {
544 self.model.nodes[i].instance_is_inherited_root = true;
545 }
546
547 let parent_idx = match parent_path.as_deref() {
548 None => None,
549 Some(".") => root,
550 Some(p) => match walk_path(root, p, &child_index) {
551 Walk::Resolved(found) => Some(found),
552 Walk::Escaped => None,
555 Walk::Missed(deepest) => {
556 if !self.model.descends_from_instance(deepest) {
562 self.model.problems.push(SceneProblem::DanglingParent {
563 node: idx,
564 parent_path: SmolStr::new(p),
565 });
566 }
567 None
568 }
569 },
570 };
571 self.model.nodes[i].parent_idx = parent_idx;
572
573 if let Some(p) = parent_idx {
574 child_index.entry((p, name.clone())).or_insert(idx);
577 children.entry(p).or_default().push(idx);
578 let pfp = &full_paths[p.0 as usize];
579 let fp = if pfp.is_empty() {
580 name
581 } else {
582 SmolStr::new(format!("{pfp}/{name}"))
583 };
584 full_paths[i] = fp.clone();
585 self.model.by_path.entry(fp).or_insert(idx);
586 }
587 }
588
589 for i in 0..n {
591 if self.model.nodes[i].unique_name_in_owner {
592 self.model
593 .unique_nodes
594 .entry(self.model.nodes[i].name.clone())
595 .or_insert(NodeIdx(to_u32(i)));
596 }
597 }
598
599 for i in 0..n {
601 let span = self.model.nodes[i].header_span;
602 let refs = [
603 self.model.nodes[i].script.clone(),
604 self.model.nodes[i].instance.clone(),
605 ];
606 for id in refs.into_iter().flatten() {
607 if !self.model.ext_resources.contains_key(&id) {
608 self.model
609 .problems
610 .push(SceneProblem::UnknownExtResource { id, at: span });
611 }
612 }
613 }
614
615 self.model.set_indices(child_index, children);
616 }
617}
618
619enum Walk {
621 Resolved(NodeIdx),
623 Escaped,
626 Missed(Option<NodeIdx>),
629}
630
631fn walk_path(
634 root: Option<NodeIdx>,
635 path: &str,
636 child_index: &FxHashMap<(NodeIdx, SmolStr), NodeIdx>,
637) -> Walk {
638 if path.starts_with('/') {
639 return Walk::Escaped; }
641 let Some(mut cur) = root else {
642 return Walk::Missed(None);
643 };
644 for seg in path.split('/') {
645 if seg.is_empty() || seg == "." {
646 continue;
647 }
648 if seg == ".." {
649 return Walk::Escaped; }
651 match child_index.get(&(cur, SmolStr::new(seg))) {
652 Some(&next) => cur = next,
653 None => return Walk::Missed(Some(cur)),
654 }
655 }
656 Walk::Resolved(cur)
657}
658
659fn unescape(s: &str) -> String {
663 if !s.contains('\\') {
664 return s.to_owned();
665 }
666 let mut out = String::with_capacity(s.len());
667 let mut chars = s.chars();
668 while let Some(c) = chars.next() {
669 if c != '\\' {
670 out.push(c);
671 continue;
672 }
673 match chars.next() {
674 Some('n') => out.push('\n'),
675 Some('t') => out.push('\t'),
676 Some('r') => out.push('\r'),
677 Some(other) => out.push(other), None => out.push('\\'),
679 }
680 }
681 out
682}
683
684fn to_u32(v: usize) -> u32 {
686 u32::try_from(v).unwrap_or(u32::MAX)
687}