From 2a6320e39485fb494b827d14d9e76895fa4f58b2 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sun, 9 Aug 2026 01:39:05 +0800 Subject: chore: rename build-cache command to build-uid-index --- tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs | 185 ----------------------- tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs | 185 +++++++++++++++++++++++ tools/tscn-to-bsn/src/cmd/mod.rs | 11 +- tools/tscn-to-bsn/src/helps/help.txt | 4 +- 4 files changed, 190 insertions(+), 195 deletions(-) delete mode 100644 tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs create mode 100644 tools/tscn-to-bsn/src/cmd/cmd_build_uid_index.rs diff --git a/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs b/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs deleted file mode 100644 index 660caf1..0000000 --- a/tools/tscn-to-bsn/src/cmd/cmd_build_cache.rs +++ /dev/null @@ -1,185 +0,0 @@ -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-cache", routeify)] -pub fn build_cache(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.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)] -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!" - ) -} 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) -> 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!" + ) +} diff --git a/tools/tscn-to-bsn/src/cmd/mod.rs b/tools/tscn-to-bsn/src/cmd/mod.rs index 7d71273..a393940 100644 --- a/tools/tscn-to-bsn/src/cmd/mod.rs +++ b/tools/tscn-to-bsn/src/cmd/mod.rs @@ -1,15 +1,12 @@ use mingling::{Program, macros::program_setup}; -use crate::{ - ThisProgram, - cmd::{cmd_ast::CMDAst, cmd_build_cache::CMDBuildCache}, -}; +use crate::ThisProgram; pub mod cmd_ast; -pub mod cmd_build_cache; +pub mod cmd_build_uid_index; #[program_setup] pub fn commands_setup(p: &mut Program) { - p.with_dispatcher(CMDAst); - p.with_dispatcher(CMDBuildCache); + p.with_dispatcher(cmd_ast::CMDAst); + p.with_dispatcher(cmd_build_uid_index::CMDBuildUidIndex); } diff --git a/tools/tscn-to-bsn/src/helps/help.txt b/tools/tscn-to-bsn/src/helps/help.txt index 99ed118..4eed086 100644 --- a/tools/tscn-to-bsn/src/helps/help.txt +++ b/tools/tscn-to-bsn/src/helps/help.txt @@ -9,6 +9,4 @@ Flags: Commands: ast Analyze the AST of a *.tscn file, output as JSON - - build-cache Collect information about the current - project and build a cache for analysis. + build-uid-index Build the uid index for the current Godot project -- cgit