From dfbc80098c2b46adb14a571947fb7ac8f0169224 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sat, 8 Aug 2026 19:47:17 +0800 Subject: feat(build-cache): write uid map to JSON cache file --- tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs | 170 ++++++++++++++++++++++++++- tools/tscn-to-bsn/src/err/json.rs | 8 ++ tools/tscn-to-bsn/src/err/mod.rs | 1 + 3 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 tools/tscn-to-bsn/src/err/json.rs (limited to 'tools/tscn-to-bsn/src') diff --git a/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs b/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs index 28e6334..660caf1 100644 --- a/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs +++ b/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs @@ -1,16 +1,178 @@ -use mingling::macros::{buffer, command, empty_result, pack_err, r_println, renderer}; +use std::collections::BTreeMap; +use std::io; +use std::path::{Path, PathBuf}; -use crate::{Next, res::gd_proj_dir::ResGodotProjectDirectory}; +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); -#[command(node = "build-cache")] +/// Build result +#[derive(Grouped)] +pub struct ResultCacheBuilt { + path: PathBuf, + len: usize, +} + +#[command(node = "build-cache", routeify)] pub fn build_cache(proj: &ResGodotProjectDirectory) -> Next { let Some(ref dir) = proj.dir else { return ErrorGodotProjectNotFound::default().into(); }; - empty_result!() + // 1. Scan uid → res:// paths + let map = scan_project(dir)?; + + // 2. Write .godot/tscn2bsn.cache.json + let cache_path = dir.join(".godot").join("tscn2bsn.cache.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)] diff --git a/tools/tscn-to-bsn/src/err/json.rs b/tools/tscn-to-bsn/src/err/json.rs new file mode 100644 index 0000000..b0f240e --- /dev/null +++ b/tools/tscn-to-bsn/src/err/json.rs @@ -0,0 +1,8 @@ +use mingling::macros::{buffer, group, r_println, renderer}; + +group!(ErrorJson = serde_json::Error); + +#[renderer(buffer)] +pub fn handle_error_json(err: ErrorJson) { + r_println!("{}", err.to_string()) +} diff --git a/tools/tscn-to-bsn/src/err/mod.rs b/tools/tscn-to-bsn/src/err/mod.rs index 29f6fd0..4721ea1 100644 --- a/tools/tscn-to-bsn/src/err/mod.rs +++ b/tools/tscn-to-bsn/src/err/mod.rs @@ -1,3 +1,4 @@ pub mod io; +pub mod json; pub mod ron; pub mod tscn_parse; -- cgit