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) -> 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> { 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, 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 { 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 { 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 { 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!" ) }