diff options
Diffstat (limited to 'tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs')
| -rw-r--r-- | tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs b/tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs new file mode 100644 index 0000000..e93392b --- /dev/null +++ b/tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs @@ -0,0 +1,185 @@ +use std::collections::BTreeMap; +use std::io; +use std::path::{Path, PathBuf}; + +use mingling::macros::routeify; +use mingling::{ + Grouped, + macros::{buffer, command, pack_err, r_println, renderer}, +}; + +use crate::{ + Next, + gd_res_structure::{TscnValue, parse}, + res::gd_proj_dir::ResGodotProjectDirectory, +}; + +pack_err!(ErrorGodotProjectNotFound); + +/// Build result +#[derive(Grouped)] +pub struct ResultCacheBuilt { + path: PathBuf, + len: usize, +} + +#[command(node = "build-uid-index", routeify)] +pub fn build_uid_index(proj: &ResGodotProjectDirectory) -> Next { + let Some(ref dir) = proj.dir else { + return ErrorGodotProjectNotFound::default().into(); + }; + + // 1. Scan uid → res:// paths + let map = scan_project(dir)?; + + // 2. Write .godot/tscn2bsn.uid-idx.json + let cache_path = dir.join(".godot").join("tscn2bsn.uid-idx.json"); + if let Some(parent) = cache_path.parent() { + std::fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(&map)?; + std::fs::write(&cache_path, json)?; + + ResultCacheBuilt { + path: cache_path, + len: map.len(), + } + .into() +} + +/// Recursively collect all files in the project, skipping `.godot` / `.git` +fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + let name = entry.file_name().to_string_lossy().into_owned(); + if name == ".godot" || name == ".git" { + continue; + } + collect_files(&path, out)?; + } else if path.is_file() { + out.push(path); + } + } + Ok(()) +} + +/// Project relative path → `res://` form (Godot's standard in-project path) +fn to_res_path(project_dir: &Path, file: &Path) -> String { + let rel = file.strip_prefix(project_dir).unwrap_or(file); + let rel = rel.to_string_lossy().replace('\\', "/"); + format!("res://{rel}") +} + +/// Scan all uid sources, aggregate the mapping +fn scan_project(project_dir: &Path) -> io::Result<BTreeMap<String, String>> { + let mut files = Vec::new(); + collect_files(project_dir, &mut files)?; + + let mut map = BTreeMap::new(); + for file in files { + let ext = file + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + match ext.as_str() { + // scenes/foo.glb.import → resource path res://scenes/foo.glb + "import" => { + if let Some(uid) = parse_import_uid(&file) { + let res_path = to_res_path(project_dir, &file); + insert_unique( + &mut map, + uid, + res_path.trim_end_matches(".import").to_string(), + ); + } + } + "tscn" | "tres" => { + if let Some(uid) = parse_scene_uid(&file) { + insert_unique(&mut map, uid, to_res_path(project_dir, &file)); + } + } + // foo.gd.uid → resource path res://foo.gd + "uid" => { + if let Some(uid) = parse_sidecar_uid(&file) { + let res_path = to_res_path(project_dir, &file); + insert_unique(&mut map, uid, res_path.trim_end_matches(".uid").to_string()); + } + } + _ => {} + } + } + Ok(map) +} + +fn insert_unique(map: &mut BTreeMap<String, String>, uid: String, res_path: String) { + match map.entry(uid.clone()) { + std::collections::btree_map::Entry::Vacant(v) => { + v.insert(res_path); + } + std::collections::btree_map::Entry::Occupied(o) => { + eprintln!("Warning: duplicate uid {uid}: {} vs {res_path}", o.get()); + } + } +} + +/// `.import` file: `uid="uid://..."` in the `[remap]` section (writer: editor_file_system.cpp L2693) +fn parse_import_uid(file: &Path) -> Option<String> { + let content = std::fs::read_to_string(file).ok()?; + let mut in_remap = false; + for line in content.lines() { + let line = line.trim(); + if line.starts_with('[') { + in_remap = line == "[remap]"; + continue; + } + if in_remap && let Some(v) = line.strip_prefix("uid=") { + let uid = v.trim().trim_matches('"'); + if uid.starts_with("uid://") { + return Some(uid.to_string()); + } + } + } + None +} + +/// `.tscn` / `.tres`: reuse the tscn parser, extract the `uid` field from the header tag +fn parse_scene_uid(file: &Path) -> Option<String> { + let content = std::fs::read_to_string(file).ok()?; + let doc = parse(&content).ok()?; + for (key, value) in &doc.header.fields { + if key == "uid" + && let TscnValue::Str(s) = value + { + return Some(s.clone()); + } + } + None +} + +/// `.uid` sidecar file: the first line is `uid://...` (writer: editor_file_system.cpp L939) +fn parse_sidecar_uid(file: &Path) -> Option<String> { + let content = std::fs::read_to_string(file).ok()?; + let first = content.lines().next()?.trim(); + if first.starts_with("uid://") { + Some(first.to_string()) + } else { + None + } +} + +#[renderer(buffer)] +pub fn render_cache_built(r: ResultCacheBuilt) { + r_println!("OK: wrote {} ({} uids)", r.path.display(), r.len); +} + +#[renderer(buffer)] +pub fn render_error_godot_project_not_found(_: ErrorGodotProjectNotFound) { + r_println!("Error: Godot project not found. "); + r_println!(""); + r_println!( + "Please specify the project directory with `/P` or run this command in the project directory!" + ) +} |
