From 8da042ccd37cdd5eadd9360be4278e2304189048 Mon Sep 17 00:00:00 2001 From: Blank038 Date: Sat, 29 Nov 2025 23:38:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=87=E5=87=86=E5=8C=96=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E7=AC=A6,=20=E5=A2=9E=E5=8A=A0=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 99 +++++++++ Cargo.toml | 4 +- examples/VARIABLES.md | 8 +- .../exampleScripts/modCommon/modConfig.py | 10 +- .../default/behavior_pack/pack_manifest.json | 4 +- .../default/resource_pack/pack_manifest.json | 4 +- examples/default/template.toml | 28 +++ examples/default/world_behavior_packs.json | 2 +- examples/default/world_resource_packs.json | 2 +- src/commands/create.rs | 135 ++++-------- src/error.rs | 10 + src/main.rs | 1 + src/template.rs | 198 ++++++++++++++++++ 13 files changed, 402 insertions(+), 103 deletions(-) create mode 100644 examples/default/template.toml create mode 100644 src/template.rs diff --git a/Cargo.lock b/Cargo.lock index 647df74..de41784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.18" @@ -399,9 +408,11 @@ dependencies = [ "anyhow", "clap", "dirs", + "regex", "reqwest", "serde", "serde_json", + "toml", "uuid", "walkdir", "zip", @@ -1180,6 +1191,35 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "reqwest" version = "0.12.11" @@ -1377,6 +1417,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1643,6 +1692,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -2065,6 +2155,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index e516454..a9d6ab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ reqwest = { version = "0.12", features = ["json", "blocking"] } uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +toml = "0.8" zip = "2.2.2" walkdir = "2" anyhow = "1.0.97" -dirs = "5.0" \ No newline at end of file +dirs = "5.0" +regex = "1.10" \ No newline at end of file diff --git a/examples/VARIABLES.md b/examples/VARIABLES.md index 2772a18..a943de8 100644 --- a/examples/VARIABLES.md +++ b/examples/VARIABLES.md @@ -6,5 +6,9 @@ |变量名|描述| |:----|:----| -|`__mod_name__`|项目名| -|`__mod_name_lower__`|项目名小写驼峰| +|`{{mod_name}}`|项目名| +|`{{mod_name_lower}}`|项目名小写驼峰| +|`{{behavior_pack_uuid}}`|行为包 UUID| +|`{{resource_pack_uuid}}`|资源包 UUID| +|`{{behavior_module_uuid}}`|行为包模块 UUID| +|`{{resource_module_uuid}}`|资源包模块 UUID| \ No newline at end of file diff --git a/examples/default/behavior_pack/exampleScripts/modCommon/modConfig.py b/examples/default/behavior_pack/exampleScripts/modCommon/modConfig.py index b6d4e87..de63623 100644 --- a/examples/default/behavior_pack/exampleScripts/modCommon/modConfig.py +++ b/examples/default/behavior_pack/exampleScripts/modCommon/modConfig.py @@ -1,7 +1,7 @@ -ProjectName = "Circus" +ProjectName = "{{mod_name}}" -ServerSystemName = "CircusServerSystem" -ServerSystemPath = "circusScripts.modServer.serverSystem.CircusServerSystem" +ServerSystemName = "{{mod_name}}ServerSystem" +ServerSystemPath = "{{mod_name_lower}}Scripts.modServer.serverSystem.{{mod_name}}ServerSystem" -ClientSystemName = "CircusClientSystem" -ClientSystemPath = "circusScripts.modClient.clientSystem.CircusClientSystem" \ No newline at end of file +ClientSystemName = "{{mod_name}}ClientSystem" +ClientSystemPath = "{{mod_name_lower}}Scripts.modClient.clientSystem.{{mod_name}}ClientSystem" \ No newline at end of file diff --git a/examples/default/behavior_pack/pack_manifest.json b/examples/default/behavior_pack/pack_manifest.json index 9cacfe7..c4cb4dd 100644 --- a/examples/default/behavior_pack/pack_manifest.json +++ b/examples/default/behavior_pack/pack_manifest.json @@ -3,7 +3,7 @@ { "description": "", "type": "data", - "uuid": "{behavior_module_uuid}", + "uuid": "{{behavior_module_uuid}}", "version": [ 0, 0, @@ -14,7 +14,7 @@ "header": { "description": "", "name": "behavior_pack", - "uuid": "{behavior_pack_uuid}", + "uuid": "{{behavior_pack_uuid}}", "version": [ 0, 0, diff --git a/examples/default/resource_pack/pack_manifest.json b/examples/default/resource_pack/pack_manifest.json index 7f67a0a..49ee718 100644 --- a/examples/default/resource_pack/pack_manifest.json +++ b/examples/default/resource_pack/pack_manifest.json @@ -8,7 +8,7 @@ 0 ], "name": "resource_pack", - "uuid": "{resource_pack_uuid}", + "uuid": "{{resource_pack_uuid}}", "version": [ 0, 0, @@ -19,7 +19,7 @@ { "description": "", "type": "resources", - "uuid": "{resource_module_uuid}", + "uuid": "{{resource_module_uuid}}", "version": [ 0, 0, diff --git a/examples/default/template.toml b/examples/default/template.toml new file mode 100644 index 0000000..79379ec --- /dev/null +++ b/examples/default/template.toml @@ -0,0 +1,28 @@ +[template] +name = "default" +description = "默认 emod 项目模板" + +[[renames]] +from = "behavior_pack/exampleScripts" +to = "behavior_pack/{{mod_name_lower}}Scripts" + +[[renames]] +from = "behavior_pack" +to = "behavior_pack_{{behavior_pack_uuid_short}}" + +[[renames]] +from = "resource_pack" +to = "resource_pack_{{resource_pack_uuid_short}}" + +[variables] +mod_name = { required = true, description = "项目名称" } +mod_name_lower = { required = true, description = "项目名称(小写驼峰)" } +behavior_pack_uuid = { required = true, description = "行为包 UUID" } +resource_pack_uuid = { required = true, description = "资源包 UUID" } +behavior_module_uuid = { required = true, description = "行为包模块 UUID" } +resource_module_uuid = { required = true, description = "资源包模块 UUID" } +behavior_pack_uuid_short = { required = true, description = "行为包 UUID 前8位" } +resource_pack_uuid_short = { required = true, description = "资源包 UUID 前8位" } + +[process] +file_extensions = ["json", "py", "lang", "txt"] diff --git a/examples/default/world_behavior_packs.json b/examples/default/world_behavior_packs.json index edf507d..67f98cb 100644 --- a/examples/default/world_behavior_packs.json +++ b/examples/default/world_behavior_packs.json @@ -1,6 +1,6 @@ [ { - "pack_id": "{behavior_pack_uuid}", + "pack_id": "{{behavior_pack_uuid}}", "type": "Addon", "version": [ 0, diff --git a/examples/default/world_resource_packs.json b/examples/default/world_resource_packs.json index 3dfd670..b27e331 100644 --- a/examples/default/world_resource_packs.json +++ b/examples/default/world_resource_packs.json @@ -1,6 +1,6 @@ [ { - "pack_id": "{resource_pack_uuid}", + "pack_id": "{{resource_pack_uuid}}", "type": "Addon", "version": [ 0, diff --git a/src/commands/create.rs b/src/commands/create.rs index 4e954b2..a66b759 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -1,6 +1,7 @@ use crate::{ config::Config, entity::project::ProjectInfo, + template::TemplateEngine, utils::{file, http::HttpClient}, }; use std::{fs, path::PathBuf}; @@ -26,9 +27,9 @@ fn create_project(name: &str, target: Option<&str>, temp_dir: &PathBuf) -> Resul let local_dir = PathBuf::from(format!("./{}", name)); fs::create_dir(&local_dir)?; - clone_and_copy_template(target, temp_dir, &local_dir)?; + let template_dir = clone_and_copy_template(target, temp_dir, &local_dir)?; - initialize_project(&local_dir, name)?; + initialize_project_with_template(&template_dir, &local_dir, name)?; Ok(()) } @@ -57,7 +58,11 @@ fn check_example_exists(target: &str) -> Result<()> { Ok(()) } -fn clone_and_copy_template(target: &str, temp_dir: &PathBuf, local_dir: &PathBuf) -> Result<()> { +fn clone_and_copy_template( + target: &str, + temp_dir: &PathBuf, + local_dir: &PathBuf, +) -> Result { let _ = fs::remove_dir_all(format!("{}/tmp", temp_dir.display())); let config = Config::load(); @@ -67,10 +72,14 @@ fn clone_and_copy_template(target: &str, temp_dir: &PathBuf, local_dir: &PathBuf let target_dir = PathBuf::from(format!("{}/tmp/examples/{}", temp_dir.display(), target)); file::copy_folder(&target_dir, local_dir)?; - Ok(()) + Ok(target_dir) } -fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> { +fn initialize_project_with_template( + template_dir: &PathBuf, + local_dir: &PathBuf, + name: &str, +) -> Result<()> { let lower_name = format!( "{}{}", name.chars().next().unwrap().to_lowercase(), @@ -80,14 +89,41 @@ fn initialize_project(local_dir: &PathBuf, name: &str) -> Result<()> { println!("项目名称: {}", name); println!("标识名称: {}", lower_name); - let scripts_dir = local_dir.join(format!("behavior_pack/{}Scripts", lower_name)); - fs::rename(local_dir.join("behavior_pack/exampleScripts"), &scripts_dir)?; - let project_info = generate_project_info(name, &lower_name); - apply_project_info(local_dir, &scripts_dir, &project_info)?; + let mut engine = TemplateEngine::load(template_dir)?; - rename_pack_folders(local_dir, &project_info)?; + engine.set_variable("mod_name".to_string(), project_info.name.clone()); + engine.set_variable( + "mod_name_lower".to_string(), + project_info.lower_name.clone(), + ); + engine.set_variable( + "behavior_pack_uuid".to_string(), + project_info.behavior_pack_uuid.clone(), + ); + engine.set_variable( + "resource_pack_uuid".to_string(), + project_info.resource_pack_uuid.clone(), + ); + engine.set_variable( + "behavior_module_uuid".to_string(), + project_info.behavior_module_uuid.clone(), + ); + engine.set_variable( + "resource_module_uuid".to_string(), + project_info.resource_module_uuid.clone(), + ); + engine.set_variable( + "behavior_pack_uuid_short".to_string(), + project_info.behavior_pack_uuid.chars().take(8).collect(), + ); + engine.set_variable( + "resource_pack_uuid_short".to_string(), + project_info.resource_pack_uuid.chars().take(8).collect(), + ); + + engine.process_directory(local_dir)?; Ok(()) } @@ -102,82 +138,3 @@ fn generate_project_info(name: &str, lower_name: &str) -> ProjectInfo { resource_module_uuid: Uuid::new_v4().to_string(), } } - -fn apply_project_info( - local_dir: &PathBuf, - scripts_dir: &PathBuf, - info: &ProjectInfo, -) -> Result<()> { - let manifest_files = vec![ - local_dir.join("world_behavior_packs.json"), - local_dir.join("world_resource_packs.json"), - local_dir.join("behavior_pack/pack_manifest.json"), - local_dir.join("resource_pack/pack_manifest.json"), - ]; - - for path in manifest_files { - apply_info_to_json(&path, info)?; - } - - process_python_files(scripts_dir, info)?; - - Ok(()) -} - -fn apply_info_to_json(path: &PathBuf, info: &ProjectInfo) -> Result<()> { - println!(" - 修改文件: {}", path.display()); - file::update_json_file(path, |json| { - let content = serde_json::to_string(json)?; - let updated = content - .replace("{behavior_pack_uuid}", &info.behavior_pack_uuid) - .replace("{resource_pack_uuid}", &info.resource_pack_uuid) - .replace("{behavior_module_uuid}", &info.behavior_module_uuid) - .replace("{resource_module_uuid}", &info.resource_module_uuid) - .replace("__mod_name__", &info.name) - .replace("__mod_name_lower__", &info.lower_name); - *json = serde_json::from_str(&updated)?; - Ok(()) - }) -} - -fn process_python_files(dir: &PathBuf, info: &ProjectInfo) -> Result<()> { - if !dir.exists() { - return Ok(()); - } - - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - process_python_files(&entry.path(), info)?; - } - } else if dir.extension().and_then(|s| s.to_str()) == Some("py") { - apply_info_to_python(dir, info)?; - } - - Ok(()) -} - -fn apply_info_to_python(path: &PathBuf, info: &ProjectInfo) -> Result<()> { - let content = fs::read_to_string(path)?; - let updated = content - .replace("__mod_name__", &info.name) - .replace("__mod_name_lower__", &info.lower_name); - fs::write(path, updated)?; - Ok(()) -} - -fn rename_pack_folders(local_dir: &PathBuf, info: &ProjectInfo) -> Result<()> { - let behavior_suffix: String = info.behavior_pack_uuid.chars().take(8).collect(); - let resource_suffix: String = info.resource_pack_uuid.chars().take(8).collect(); - - fs::rename( - local_dir.join("behavior_pack"), - local_dir.join(format!("behavior_pack_{}", behavior_suffix)), - )?; - fs::rename( - local_dir.join("resource_pack"), - local_dir.join(format!("resource_pack_{}", resource_suffix)), - )?; - - Ok(()) -} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 0c004ec..5dbf4d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,8 +11,10 @@ pub enum CliError { Zip(zip::result::ZipError), Walkdir(walkdir::Error), Parse(ParseIntError), + Toml(toml::de::Error), NotFound(String), InvalidData(String), + InvalidInput(String), } impl fmt::Display for CliError { @@ -25,8 +27,10 @@ impl fmt::Display for CliError { CliError::Zip(e) => write!(f, "压缩错误: {}", e), CliError::Walkdir(e) => write!(f, "目录遍历错误: {}", e), CliError::Parse(e) => write!(f, "解析错误: {}", e), + CliError::Toml(e) => write!(f, "TOML解析错误: {}", e), CliError::NotFound(msg) => write!(f, "未找到: {}", msg), CliError::InvalidData(msg) => write!(f, "无效数据: {}", msg), + CliError::InvalidInput(msg) => write!(f, "无效输入: {}", msg), } } } @@ -75,4 +79,10 @@ impl From for CliError { } } +impl From for CliError { + fn from(err: toml::de::Error) -> Self { + CliError::Toml(err) + } +} + pub type Result = std::result::Result; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 92865c3..99acb90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod entity; mod utils; mod error; mod config; +mod template; use crate::commands::{Cli, Commands}; use clap::Parser; diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..137b893 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,198 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use regex::Regex; +use walkdir::WalkDir; + +#[derive(Debug, Deserialize, Serialize)] +pub struct TemplateConfig { + pub template: TemplateInfo, + #[serde(default)] + pub renames: Vec, + pub variables: HashMap, + pub process: ProcessConfig, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TemplateInfo { + pub name: String, + pub description: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RenameRule { + pub from: String, + pub to: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct VariableConfig { + pub required: bool, + pub description: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProcessConfig { + pub file_extensions: Vec, +} + +pub struct TemplateEngine { + config: TemplateConfig, + variables: HashMap, +} + +impl TemplateEngine { + pub fn load(template_dir: &Path) -> crate::error::Result { + let config_path = template_dir.join("template.toml"); + let content = fs::read_to_string(&config_path)?; + let config: TemplateConfig = toml::from_str(&content)?; + + Ok(Self { + config, + variables: HashMap::new(), + }) + } + + pub fn set_variable(&mut self, key: String, value: String) { + self.variables.insert(key, value); + } + + pub fn validate_variables(&self) -> crate::error::Result<()> { + for (key, var_config) in &self.config.variables { + if var_config.required && !self.variables.contains_key(key) { + return Err(crate::error::CliError::InvalidInput(format!( + "缺少必需的变量: {} ({})", + key, var_config.description + ))); + } + } + Ok(()) + } + + pub fn process_directory(&self, dir: &Path) -> crate::error::Result<()> { + self.validate_variables()?; + + self.replace_in_files(dir)?; + + self.apply_renames(dir)?; + + self.verify_no_placeholders(dir)?; + + Ok(()) + } + + fn replace_in_files(&self, dir: &Path) -> crate::error::Result<()> { + let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap(); + + for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + + if !path.is_file() { + continue; + } + + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + if !self.config.process.file_extensions.contains(&ext.to_string()) { + continue; + } + } else { + continue; + } + + let content = fs::read_to_string(path)?; + let mut updated = content.clone(); + + for cap in placeholder_regex.captures_iter(&content) { + let placeholder = &cap[0]; + let var_name = &cap[1]; + + if let Some(value) = self.variables.get(var_name) { + updated = updated.replace(placeholder, value); + } + } + + if updated != content { + fs::write(path, updated)?; + println!(" - 处理文件: {}", path.display()); + } + } + + Ok(()) + } + + fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> { + for rule in self.config.renames.iter().rev() { + let from_path = dir.join(&rule.from); + + if !from_path.exists() { + continue; + } + + let to_path_str = self.replace_placeholders(&rule.to); + let to_path = dir.join(&to_path_str); + + if let Some(parent) = to_path.parent() { + fs::create_dir_all(parent)?; + } + + fs::rename(&from_path, &to_path)?; + println!(" - 重命名: {} -> {}", rule.from, to_path_str); + } + + Ok(()) + } + + fn replace_placeholders(&self, text: &str) -> String { + let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap(); + let mut result = text.to_string(); + + for cap in placeholder_regex.captures_iter(text) { + let placeholder = &cap[0]; + let var_name = &cap[1]; + + if let Some(value) = self.variables.get(var_name) { + result = result.replace(placeholder, value); + } + } + + result + } + + fn verify_no_placeholders(&self, dir: &Path) -> crate::error::Result<()> { + let placeholder_regex = Regex::new(r"\{\{(\w+)\}\}").unwrap(); + let mut found_placeholders = Vec::new(); + + for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + + if !path.is_file() { + continue; + } + + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + if !self.config.process.file_extensions.contains(&ext.to_string()) { + continue; + } + } else { + continue; + } + + let content = fs::read_to_string(path)?; + + for cap in placeholder_regex.captures_iter(&content) { + let var_name = &cap[1]; + found_placeholders.push(format!("{}:{}", path.display(), var_name)); + } + } + + if !found_placeholders.is_empty() { + eprintln!("警告: 发现未替换的占位符:"); + for placeholder in found_placeholders { + eprintln!(" - {}", placeholder); + } + } + + Ok(()) + } +} \ No newline at end of file