feat(boss-engine): 增加项目派生命令
新增 fork-boss-engine,从 AbyssBossEngine 源项目生成独立 BOSS 模组,并重建 UUID、命名空间和脚本包。 校验项目名称与命名空间,同时阻止输出目录落入源目录,避免路径穿越和递归复制。
This commit is contained in:
35
src/boss_engine/copy.rs
Normal file
35
src/boss_engine/copy.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use crate::error::Result;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// 递归复制项目目录,排除指定的目录名和文件名。用于 fork 的项目复制。
|
||||||
|
pub fn copy_dir_filtered(
|
||||||
|
src: &Path,
|
||||||
|
dst: &Path,
|
||||||
|
exclude_dirs: &[&str],
|
||||||
|
exclude_files: &[&str],
|
||||||
|
) -> Result<()> {
|
||||||
|
std::fs::create_dir_all(dst)?;
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(src)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let name_str = file_name.to_string_lossy();
|
||||||
|
|
||||||
|
if entry_path.is_dir() {
|
||||||
|
if exclude_dirs.contains(&name_str.as_ref()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dest_path = dst.join(&file_name);
|
||||||
|
copy_dir_filtered(&entry_path, &dest_path, exclude_dirs, exclude_files)?;
|
||||||
|
} else if entry_path.is_file() {
|
||||||
|
if exclude_files.contains(&name_str.as_ref()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dest_path = dst.join(&file_name);
|
||||||
|
std::fs::copy(&entry_path, &dest_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
114
src/boss_engine/mod.rs
Normal file
114
src/boss_engine/mod.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
pub mod copy;
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::error::{CliError, Result};
|
||||||
|
|
||||||
|
/// 发现项目布局: BP/RP 目录 + scripts package 目录。
|
||||||
|
/// 逻辑与 AbyssBossEngine tests/conftest.py 的自动发现一致。
|
||||||
|
pub fn discover_layout(project_dir: &Path) -> Result<Layout> {
|
||||||
|
let behavior_pack = find_single_dir(project_dir, "behavior_pack_")?;
|
||||||
|
let resource_pack = find_single_dir(project_dir, "resource_pack_")?;
|
||||||
|
let scripts_package = find_scripts_package(&behavior_pack)?;
|
||||||
|
let scripts_package_name = scripts_package
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CliError::InvalidData(format!(
|
||||||
|
"scripts package 目录名非 UTF-8: '{}'",
|
||||||
|
scripts_package.display()
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(Layout {
|
||||||
|
behavior_pack,
|
||||||
|
resource_pack,
|
||||||
|
scripts_package,
|
||||||
|
scripts_package_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Layout {
|
||||||
|
pub behavior_pack: PathBuf,
|
||||||
|
pub resource_pack: PathBuf,
|
||||||
|
pub scripts_package: PathBuf,
|
||||||
|
pub scripts_package_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_single_dir(parent: &Path, prefix: &str) -> Result<PathBuf> {
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(parent)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if name.starts_with(prefix) {
|
||||||
|
matches.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match matches.len() {
|
||||||
|
0 => Err(CliError::NotFound(format!(
|
||||||
|
"在 '{}' 下未找到 {}* 目录",
|
||||||
|
parent.display(),
|
||||||
|
prefix
|
||||||
|
))),
|
||||||
|
1 => Ok(matches.into_iter().next().unwrap()),
|
||||||
|
_ => Err(CliError::InvalidData(format!(
|
||||||
|
"在 '{}' 下找到多个 {}* 目录",
|
||||||
|
parent.display(),
|
||||||
|
prefix
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_scripts_package(behavior_pack: &Path) -> Result<PathBuf> {
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(behavior_pack)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() && path.join("modMain.py").is_file() {
|
||||||
|
matches.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match matches.len() {
|
||||||
|
0 => Err(CliError::NotFound(format!(
|
||||||
|
"在 '{}' 下未找到含 modMain.py 的 scripts package",
|
||||||
|
behavior_pack.display()
|
||||||
|
))),
|
||||||
|
1 => Ok(matches.into_iter().next().unwrap()),
|
||||||
|
_ => Err(CliError::InvalidData(format!(
|
||||||
|
"在 '{}' 下找到多个含 modMain.py 的目录",
|
||||||
|
behavior_pack.display()
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PascalCase → snake_case: AbyssBossEngine → abyss_boss_engine
|
||||||
|
pub fn pascal_to_snake(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
for (i, ch) in s.chars().enumerate() {
|
||||||
|
if ch.is_uppercase() && i > 0 {
|
||||||
|
result.push('_');
|
||||||
|
}
|
||||||
|
result.extend(ch.to_lowercase());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 首字母小写: AbyssBossEngine → abyssBossEngine
|
||||||
|
pub fn lower_first_char(s: &str) -> Result<String> {
|
||||||
|
let mut chars = s.chars();
|
||||||
|
let first = chars
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| CliError::InvalidInput("name 不能为空".to_string()))?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}{}",
|
||||||
|
first.to_lowercase().collect::<String>(),
|
||||||
|
chars.as_str()
|
||||||
|
))
|
||||||
|
}
|
||||||
486
src/commands/fork_boss_engine.rs
Normal file
486
src/commands/fork_boss_engine.rs
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::boss_engine::{self, copy};
|
||||||
|
use crate::commands::ForkBossEngineArgs;
|
||||||
|
use crate::error::{CliError, Result};
|
||||||
|
use crate::utils::file;
|
||||||
|
|
||||||
|
const BOSS_ENGINE_PATH_ENV: &str = "ABYSS_BOSS_ENGINE_PATH";
|
||||||
|
const OLD_NAMESPACE: &str = "abyss_boss_engine";
|
||||||
|
|
||||||
|
const EXCLUDE_DIRS: &[&str] = &[
|
||||||
|
".git",
|
||||||
|
"target",
|
||||||
|
"__pycache__",
|
||||||
|
".pytest_cache",
|
||||||
|
".codewiki",
|
||||||
|
".omo",
|
||||||
|
".idea",
|
||||||
|
".opencode",
|
||||||
|
".agents",
|
||||||
|
".claude",
|
||||||
|
".qoder",
|
||||||
|
"openspec",
|
||||||
|
".emod-cli",
|
||||||
|
];
|
||||||
|
|
||||||
|
const EXCLUDE_FILES: &[&str] = &[".mcdev.json", "AGENTS.md"];
|
||||||
|
|
||||||
|
pub fn execute(args: &ForkBossEngineArgs) {
|
||||||
|
if let Err(e) = run_fork(args) {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("Success: boss engine forked.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_fork(args: &ForkBossEngineArgs) -> Result<()> {
|
||||||
|
validate_project_name(&args.name)?;
|
||||||
|
|
||||||
|
let from_str = resolve_from(&args.from)?;
|
||||||
|
let from_dir = file::find_project_dir(&Some(from_str))?;
|
||||||
|
validate_engine_source(&from_dir)?;
|
||||||
|
|
||||||
|
let src_layout = boss_engine::discover_layout(&from_dir)?;
|
||||||
|
let old_scripts_name = src_layout.scripts_package_name.clone();
|
||||||
|
let old_bp_dir_name = src_layout
|
||||||
|
.behavior_pack
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.ok_or_else(|| CliError::InvalidData("BP 目录名非 UTF-8".into()))?
|
||||||
|
.to_string();
|
||||||
|
let old_rp_dir_name = src_layout
|
||||||
|
.resource_pack
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.ok_or_else(|| CliError::InvalidData("RP 目录名非 UTF-8".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let namespace = args
|
||||||
|
.namespace
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| boss_engine::pascal_to_snake(&args.name));
|
||||||
|
validate_namespace(&namespace)?;
|
||||||
|
let new_scripts_name = format!("{}Scripts", boss_engine::lower_first_char(&args.name)?);
|
||||||
|
|
||||||
|
let output_dir = PathBuf::from(format!("./{}", args.name));
|
||||||
|
if output_dir.exists() {
|
||||||
|
return Err(CliError::InvalidInput(format!(
|
||||||
|
"输出目录已存在: '{}'",
|
||||||
|
output_dir.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let source_dir = from_dir.canonicalize()?;
|
||||||
|
let output_dir_abs = env::current_dir()?.canonicalize()?.join(&args.name);
|
||||||
|
if output_dir_abs.starts_with(&source_dir) {
|
||||||
|
return Err(CliError::InvalidInput(format!(
|
||||||
|
"输出目录不能位于框架源目录内: '{}'",
|
||||||
|
output_dir_abs.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Forking boss engine:");
|
||||||
|
println!(" from: {}", from_dir.display());
|
||||||
|
println!(" to: {}", output_dir.display());
|
||||||
|
println!(" name: {}", args.name);
|
||||||
|
println!(" ns: {}", namespace);
|
||||||
|
|
||||||
|
copy::copy_dir_filtered(&from_dir, &output_dir, EXCLUDE_DIRS, EXCLUDE_FILES)?;
|
||||||
|
|
||||||
|
let uuids = generate_uuids();
|
||||||
|
update_manifests_and_packs(&output_dir, &uuids)?;
|
||||||
|
|
||||||
|
let new_bp_dir_name = format!("behavior_pack_{}", uuids.bp_short);
|
||||||
|
let new_rp_dir_name = format!("resource_pack_{}", uuids.rp_short);
|
||||||
|
rename_dir(&output_dir, &old_bp_dir_name, &new_bp_dir_name)?;
|
||||||
|
rename_dir(&output_dir, &old_rp_dir_name, &new_rp_dir_name)?;
|
||||||
|
|
||||||
|
let new_bp_dir = output_dir.join(&new_bp_dir_name);
|
||||||
|
rename_dir(&new_bp_dir, &old_scripts_name, &new_scripts_name)?;
|
||||||
|
|
||||||
|
let new_scripts_dir = new_bp_dir.join(&new_scripts_name);
|
||||||
|
|
||||||
|
if args.keep_example {
|
||||||
|
replace_namespace_everywhere(&output_dir, OLD_NAMESPACE, &namespace)?;
|
||||||
|
} else {
|
||||||
|
let new_rp_dir = output_dir.join(&new_rp_dir_name);
|
||||||
|
remove_example_assets(&new_bp_dir, &new_rp_dir, &new_scripts_dir)?;
|
||||||
|
write_clean_modconfig(&new_scripts_dir, &namespace)?;
|
||||||
|
write_clean_server_system(&new_scripts_dir)?;
|
||||||
|
clean_lang_file(&new_rp_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary(&uuids, &old_scripts_name, &new_scripts_name, &args.name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_project_name(name: &str) -> Result<()> {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let valid = chars
|
||||||
|
.next()
|
||||||
|
.map(|first| first.is_ascii_uppercase())
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& chars.all(|ch| ch.is_ascii_alphanumeric());
|
||||||
|
if !valid {
|
||||||
|
return Err(CliError::InvalidInput(
|
||||||
|
"name 必须是仅含 ASCII 字母和数字的 PascalCase 名称".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_namespace(namespace: &str) -> Result<()> {
|
||||||
|
let mut chars = namespace.chars();
|
||||||
|
let valid = chars
|
||||||
|
.next()
|
||||||
|
.map(|first| first.is_ascii_lowercase())
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_');
|
||||||
|
if !valid {
|
||||||
|
return Err(CliError::InvalidInput(
|
||||||
|
"namespace 必须以小写 ASCII 字母开头,且仅包含小写字母、数字和下划线".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_engine_source(project_dir: &Path) -> Result<()> {
|
||||||
|
let layout = boss_engine::discover_layout(project_dir)?;
|
||||||
|
let marker = layout
|
||||||
|
.scripts_package
|
||||||
|
.join("boss_engine")
|
||||||
|
.join("server")
|
||||||
|
.join("manager.py");
|
||||||
|
if !marker.is_file() {
|
||||||
|
return Err(CliError::InvalidInput(format!(
|
||||||
|
"'{}' 不是有效的 AbyssBossEngine 项目: 缺少 boss_engine/server/manager.py",
|
||||||
|
project_dir.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NewUuids {
|
||||||
|
bp_header: String,
|
||||||
|
bp_module: String,
|
||||||
|
rp_header: String,
|
||||||
|
rp_module: String,
|
||||||
|
bp_short: String,
|
||||||
|
rp_short: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_uuids() -> NewUuids {
|
||||||
|
let bp_header = Uuid::new_v4().to_string();
|
||||||
|
let rp_header = Uuid::new_v4().to_string();
|
||||||
|
NewUuids {
|
||||||
|
bp_short: bp_header.chars().take(8).collect(),
|
||||||
|
rp_short: rp_header.chars().take(8).collect(),
|
||||||
|
bp_module: Uuid::new_v4().to_string(),
|
||||||
|
rp_module: Uuid::new_v4().to_string(),
|
||||||
|
bp_header,
|
||||||
|
rp_header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_manifests_and_packs(output_dir: &Path, uuids: &NewUuids) -> Result<()> {
|
||||||
|
let bp_dir = find_pack_dir(output_dir, "behavior_pack_")?;
|
||||||
|
let rp_dir = find_pack_dir(output_dir, "resource_pack_")?;
|
||||||
|
|
||||||
|
file::update_json_file(&bp_dir.join("pack_manifest.json"), |json| {
|
||||||
|
json["header"]["uuid"] = uuids.bp_header.clone().into();
|
||||||
|
json["modules"][0]["uuid"] = uuids.bp_module.clone().into();
|
||||||
|
json["dependencies"][0]["uuid"] = uuids.rp_header.clone().into();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
file::update_json_file(&rp_dir.join("pack_manifest.json"), |json| {
|
||||||
|
json["header"]["uuid"] = uuids.rp_header.clone().into();
|
||||||
|
json["modules"][0]["uuid"] = uuids.rp_module.clone().into();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
file::update_json_file(&output_dir.join("world_behavior_packs.json"), |json| {
|
||||||
|
json[0]["pack_id"] = uuids.bp_header.clone().into();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
file::update_json_file(&output_dir.join("world_resource_packs.json"), |json| {
|
||||||
|
json[0]["pack_id"] = uuids.rp_header.clone().into();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_pack_dir(parent: &Path, prefix: &str) -> Result<PathBuf> {
|
||||||
|
for entry in fs::read_dir(parent)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if name.starts_with(prefix) {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(CliError::NotFound(format!(
|
||||||
|
"在 '{}' 下未找到 {}* 目录",
|
||||||
|
parent.display(),
|
||||||
|
prefix
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_dir(parent: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
||||||
|
let old_path = parent.join(old_name);
|
||||||
|
let new_path = parent.join(new_name);
|
||||||
|
if !old_path.exists() {
|
||||||
|
return Err(CliError::NotFound(format!(
|
||||||
|
"待重命名目录不存在: '{}'",
|
||||||
|
old_path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
fs::rename(&old_path, &new_path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── example 处理 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
fn remove_example_assets(bp_dir: &Path, rp_dir: &Path, scripts_dir: &Path) -> Result<()> {
|
||||||
|
let example_prefix = format!("{}_example", OLD_NAMESPACE);
|
||||||
|
|
||||||
|
let mut deleted = 0usize;
|
||||||
|
for root in [bp_dir, rp_dir] {
|
||||||
|
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let file_name = match path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if file_name.starts_with(&example_prefix) {
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let example_scripts_dir = scripts_dir.join("modServer").join("example");
|
||||||
|
if example_scripts_dir.is_dir() {
|
||||||
|
fs::remove_dir_all(&example_scripts_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" removed {} example asset files", deleted);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_clean_modconfig(scripts_dir: &Path, namespace: &str) -> Result<()> {
|
||||||
|
let content = format!(
|
||||||
|
r#"# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
MOD_ID = "{ns}"
|
||||||
|
MOD_VERSION = "1.0.0"
|
||||||
|
SCRIPT_PACKAGE = __name__.split(".")[0]
|
||||||
|
|
||||||
|
PROJECT_NAME = "AiyoBoss_%s" % MOD_ID
|
||||||
|
SERVER_SYSTEM_NAME = "AiyoBossServer_%s" % MOD_ID
|
||||||
|
CLIENT_SYSTEM_NAME = "AiyoBossClient_%s" % MOD_ID
|
||||||
|
|
||||||
|
SERVER_SYSTEM_PATH = "%s.modServer.serverSystem.BossModServerSystem" % SCRIPT_PACKAGE
|
||||||
|
CLIENT_SYSTEM_PATH = "%s.modClient.clientSystem.BossModClientSystem" % SCRIPT_PACKAGE
|
||||||
|
BROKER_SYSTEM_PATH = "%s.boss_engine.client.broker_system.BossClientBroker" % SCRIPT_PACKAGE
|
||||||
|
|
||||||
|
BOSS_REGISTRY_EVENT = "AiyoBossRegistryV1"
|
||||||
|
BGM_SNAPSHOT_EVENT = "AiyoBossBgmSnapshotV1"
|
||||||
|
CLIENT_READY_EVENT = "AiyoBossClientReadyV1"
|
||||||
|
|
||||||
|
MAX_SUMMONS_PER_BOSS = 8
|
||||||
|
SUMMON_TARGET_SYNC_INTERVAL = 0.5
|
||||||
|
"#,
|
||||||
|
ns = namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = scripts_dir.join("modCommon").join("modConfig.py");
|
||||||
|
fs::write(&path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_clean_server_system(scripts_dir: &Path) -> Result<()> {
|
||||||
|
let content = r#"# -*- coding: utf-8 -*-
|
||||||
|
import mod.server.extraServerApi as serverApi
|
||||||
|
|
||||||
|
from ..boss_engine.server import BossManager
|
||||||
|
from ..modCommon import modConfig
|
||||||
|
|
||||||
|
|
||||||
|
ServerSystem = serverApi.GetServerSystemCls()
|
||||||
|
|
||||||
|
|
||||||
|
class BossModServerSystem(ServerSystem):
|
||||||
|
def __init__(self, namespace, system_name):
|
||||||
|
ServerSystem.__init__(self, namespace, system_name)
|
||||||
|
self.boss_manager = BossManager(
|
||||||
|
self,
|
||||||
|
modConfig.MOD_ID,
|
||||||
|
modConfig.PROJECT_NAME,
|
||||||
|
modConfig.CLIENT_SYSTEM_NAME,
|
||||||
|
modConfig.BOSS_REGISTRY_EVENT,
|
||||||
|
modConfig.BGM_SNAPSHOT_EVENT,
|
||||||
|
modConfig.CLIENT_READY_EVENT,
|
||||||
|
modConfig.MAX_SUMMONS_PER_BOSS,
|
||||||
|
modConfig.SUMMON_TARGET_SYNC_INTERVAL,
|
||||||
|
)
|
||||||
|
self.boss_manager.start()
|
||||||
|
print("[AbyssBossEngine][%s] server ready" % modConfig.MOD_ID)
|
||||||
|
|
||||||
|
def Destroy(self):
|
||||||
|
self.boss_manager.destroy()
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let path = scripts_dir.join("modServer").join("serverSystem.py");
|
||||||
|
fs::write(&path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_lang_file(rp_dir: &Path) -> Result<()> {
|
||||||
|
let lang_path = rp_dir.join("texts").join("zh_CN.lang");
|
||||||
|
if !lang_path.is_file() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&lang_path)?;
|
||||||
|
let filtered: String = content
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.contains(OLD_NAMESPACE))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
fs::write(&lang_path, filtered + "\n")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── keep-example 模式: namespace 全局替换 ──────────────────
|
||||||
|
|
||||||
|
fn replace_namespace_everywhere(output_dir: &Path, old_ns: &str, new_ns: &str) -> Result<()> {
|
||||||
|
let bp_dir = find_pack_dir(output_dir, "behavior_pack_")?;
|
||||||
|
let rp_dir = find_pack_dir(output_dir, "resource_pack_")?;
|
||||||
|
|
||||||
|
let mut replaced_files = 0usize;
|
||||||
|
for root in [&bp_dir, &rp_dir] {
|
||||||
|
replaced_files += replace_in_text_files(root, old_ns, new_ns)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut renamed_files = 0usize;
|
||||||
|
for root in [&bp_dir, &rp_dir] {
|
||||||
|
renamed_files += rename_files_with_namespace(root, old_ns, new_ns)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" namespace replaced: {} -> {} ({} files updated, {} renamed)",
|
||||||
|
old_ns, new_ns, replaced_files, renamed_files
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_in_text_files(root: &Path, old_ns: &str, new_ns: &str) -> Result<usize> {
|
||||||
|
let mut count = 0usize;
|
||||||
|
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = match fs::read_to_string(path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if !content.contains(old_ns) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let updated = content.replace(old_ns, new_ns);
|
||||||
|
fs::write(path, updated)?;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_files_with_namespace(root: &Path, old_ns: &str, new_ns: &str) -> Result<usize> {
|
||||||
|
let mut to_rename = Vec::new();
|
||||||
|
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let file_name = match path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if file_name.contains(old_ns) {
|
||||||
|
to_rename.push(path.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in &to_rename {
|
||||||
|
let dir = path.parent().unwrap();
|
||||||
|
let old_name = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
let new_name = old_name.replace(old_ns, new_ns);
|
||||||
|
fs::rename(path, dir.join(new_name))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(to_rename.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 输出 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn print_summary(uuids: &NewUuids, old_scripts: &str, new_scripts: &str, project_name: &str) {
|
||||||
|
println!("\nUUID changes:");
|
||||||
|
println!(" BP header: {}", uuids.bp_header);
|
||||||
|
println!(" BP module: {}", uuids.bp_module);
|
||||||
|
println!(" RP header: {}", uuids.rp_header);
|
||||||
|
println!(" RP module: {}", uuids.rp_module);
|
||||||
|
println!("\nScripts package: {} -> {}", old_scripts, new_scripts);
|
||||||
|
println!("\nNext steps:");
|
||||||
|
println!(" cd {}", project_name);
|
||||||
|
println!(" python -m pytest tests -q");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_from(from: &Option<String>) -> Result<String> {
|
||||||
|
if let Some(path) = from {
|
||||||
|
if !path.is_empty() {
|
||||||
|
return Ok(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match env::var(BOSS_ENGINE_PATH_ENV) {
|
||||||
|
Ok(value) if !value.is_empty() => {
|
||||||
|
println!(" (from env ${}: {})", BOSS_ENGINE_PATH_ENV, value);
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
_ => Err(CliError::InvalidInput(format!(
|
||||||
|
"未指定框架源路径: 请使用 --from 或设置环境变量 {}",
|
||||||
|
BOSS_ENGINE_PATH_ENV
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{validate_namespace, validate_project_name};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_project_names_and_namespaces() {
|
||||||
|
assert!(validate_project_name("ShadowBoss2").is_ok());
|
||||||
|
assert!(validate_namespace("shadow_boss2").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_paths_and_invalid_namespaces() {
|
||||||
|
assert!(validate_project_name("../ShadowBoss").is_err());
|
||||||
|
assert!(validate_project_name("shadowBoss").is_err());
|
||||||
|
assert!(validate_namespace("ShadowBoss").is_err());
|
||||||
|
assert!(validate_namespace("2shadow_boss").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod bbmodel;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
|
pub mod fork_boss_engine;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod release;
|
pub mod release;
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ pub enum Commands {
|
|||||||
Debug(DebugArgs),
|
Debug(DebugArgs),
|
||||||
/// Convert Blockbench .bbmodel to NetEase block geometry
|
/// Convert Blockbench .bbmodel to NetEase block geometry
|
||||||
Bbmodel(BbmodelArgs),
|
Bbmodel(BbmodelArgs),
|
||||||
|
/// Fork AbyssBossEngine into a new boss mod project
|
||||||
|
ForkBossEngine(ForkBossEngineArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@@ -118,3 +121,20 @@ pub struct DebugArgs {
|
|||||||
#[arg(short = 'n', long = "new")]
|
#[arg(short = 'n', long = "new")]
|
||||||
pub new_world: bool,
|
pub new_world: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ForkBossEngineArgs {
|
||||||
|
/// Path to the AbyssBossEngine source project.
|
||||||
|
/// Falls back to env ABYSS_BOSS_ENGINE_PATH when omitted.
|
||||||
|
#[arg(long)]
|
||||||
|
pub from: Option<String>,
|
||||||
|
/// New project name (PascalCase, e.g. MyShadowBoss)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub name: String,
|
||||||
|
/// Override the entity namespace (default: snake_case of --name)
|
||||||
|
#[arg(long)]
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
/// Keep example boss as reference (default: remove)
|
||||||
|
#[arg(long)]
|
||||||
|
pub keep_example: bool,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod boss_engine;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod debug;
|
mod debug;
|
||||||
mod entity;
|
mod entity;
|
||||||
@@ -17,5 +18,6 @@ fn main() {
|
|||||||
Commands::Components(args) => commands::components::execute(args),
|
Commands::Components(args) => commands::components::execute(args),
|
||||||
Commands::Debug(args) => commands::debug::execute(args),
|
Commands::Debug(args) => commands::debug::execute(args),
|
||||||
Commands::Bbmodel(args) => commands::bbmodel::execute(args),
|
Commands::Bbmodel(args) => commands::bbmodel::execute(args),
|
||||||
|
Commands::ForkBossEngine(args) => commands::fork_boss_engine::execute(args),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user