feat(debug): 支持 --new 参数创建全新调试存档
添加 选项,启动调试时自动生成带时间戳的新存档目录 (格式 MC_DEV_WORLD_YYYYMMDD_HHMMSS)并持久化到 .mcdev.json。 - 新增 local_timestamp_compact() 跨平台本地时间格式化 - 新增 allocate_new_world() 更新配置并落盘 - 补充单元测试验证存档名格式与持久化正确性
This commit is contained in:
@@ -27,6 +27,7 @@ windows-sys = { version = "0.59", features = [
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
@@ -9,7 +9,7 @@ pub fn execute(args: &DebugArgs) {
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
|
||||
if let Err(err) = debug::run(&project_dir) {
|
||||
if let Err(err) = debug::run(&project_dir, args.new_world) {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,7 @@ pub struct DebugArgs {
|
||||
/// The path of the project (default: current directory)
|
||||
#[arg(short, long)]
|
||||
pub path: Option<String>,
|
||||
/// Create a fresh debug world and persist it in .mcdev.json
|
||||
#[arg(short = 'n', long = "new")]
|
||||
pub new_world: bool,
|
||||
}
|
||||
|
||||
@@ -179,6 +179,17 @@ pub fn write_config(project_dir: &Path, config: &DebugConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn allocate_new_world(project_dir: &Path, config: &mut DebugConfig) -> Result<()> {
|
||||
let world_name = format!(
|
||||
"MC_DEV_WORLD_{}",
|
||||
crate::debug::env::local_timestamp_compact()
|
||||
);
|
||||
|
||||
config.world_folder_name = world_name.clone();
|
||||
config.world_name = world_name;
|
||||
write_config(project_dir, config)
|
||||
}
|
||||
|
||||
fn create_default_config() -> DebugConfig {
|
||||
DebugConfig {
|
||||
included_mod_dirs: default_included_mod_dirs(),
|
||||
@@ -333,7 +344,39 @@ pub fn strip_json_comments(input: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_mod_dir, strip_json_comments};
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use super::{allocate_new_world, resolve_mod_dir, strip_json_comments};
|
||||
|
||||
struct TempProject {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempProject {
|
||||
fn new() -> Self {
|
||||
let nonce = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"emod-cli-debug-config-test-{}-{nonce}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempProject {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_jsonc_comments_without_touching_strings() {
|
||||
@@ -345,6 +388,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_new_world_updates_folder_and_name() {
|
||||
let project = TempProject::new();
|
||||
let mut config = super::create_default_config();
|
||||
|
||||
allocate_new_world(&project.path, &mut config).unwrap();
|
||||
|
||||
let suffix = config
|
||||
.world_folder_name
|
||||
.strip_prefix("MC_DEV_WORLD_")
|
||||
.expect("world folder should use debug prefix");
|
||||
assert_eq!(suffix.len(), "YYYYMMDD_HHMMSS".len());
|
||||
assert_eq!(suffix.as_bytes()[8], b'_');
|
||||
assert!(
|
||||
suffix
|
||||
.bytes()
|
||||
.enumerate()
|
||||
.all(|(index, byte)| index == 8 || byte.is_ascii_digit())
|
||||
);
|
||||
assert_eq!(config.world_name, config.world_folder_name);
|
||||
|
||||
let persisted = super::load_or_create(&project.path).unwrap();
|
||||
assert_eq!(persisted.world_folder_name, config.world_folder_name);
|
||||
assert_eq!(persisted.world_name, config.world_folder_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_current_project_resolves_to_absolute_cwd() {
|
||||
let resolved = resolve_mod_dir(std::path::Path::new("."), "./", true, true).unwrap();
|
||||
|
||||
@@ -20,6 +20,64 @@ pub fn worlds_path() -> PathBuf {
|
||||
minecraft_data_path().join("minecraftWorlds")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn local_timestamp_compact() -> String {
|
||||
use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::GetLocalTime};
|
||||
|
||||
let mut now = SYSTEMTIME {
|
||||
wYear: 0,
|
||||
wMonth: 0,
|
||||
wDayOfWeek: 0,
|
||||
wDay: 0,
|
||||
wHour: 0,
|
||||
wMinute: 0,
|
||||
wSecond: 0,
|
||||
wMilliseconds: 0,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
GetLocalTime(&mut now);
|
||||
}
|
||||
|
||||
format!(
|
||||
"{:04}{:02}{:02}_{:02}{:02}{:02}",
|
||||
now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn local_timestamp_compact() -> String {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let days = (now / 86_400) as i64;
|
||||
let seconds_of_day = now % 86_400;
|
||||
let (year, month, day) = civil_from_days(days);
|
||||
let hour = seconds_of_day / 3_600;
|
||||
let minute = (seconds_of_day % 3_600) / 60;
|
||||
let second = seconds_of_day % 60;
|
||||
|
||||
format!("{year:04}{month:02}{day:02}_{hour:02}{minute:02}{second:02}")
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn civil_from_days(days_since_unix_epoch: i64) -> (i32, u32, u32) {
|
||||
let z = days_since_unix_epoch + 719_468;
|
||||
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||
let day_of_era = z - era * 146_097;
|
||||
let year_of_era =
|
||||
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
||||
let year = year_of_era + era * 400;
|
||||
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
||||
let month_part = (5 * day_of_year + 2) / 153;
|
||||
let day = day_of_year - (153 * month_part + 2) / 5 + 1;
|
||||
let month = month_part + if month_part < 10 { 3 } else { -9 };
|
||||
let year = year + if month <= 2 { 1 } else { 0 };
|
||||
|
||||
(year as i32, month as u32, day as u32)
|
||||
}
|
||||
|
||||
pub fn behavior_packs_path() -> PathBuf {
|
||||
games_com_netease_path().join("behavior_packs")
|
||||
}
|
||||
|
||||
@@ -13,8 +13,14 @@ use std::path::Path;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn run(project_dir: &Path) -> Result<()> {
|
||||
pub fn run(project_dir: &Path, new_world: bool) -> Result<()> {
|
||||
let mut config = config::load_or_create(project_dir)?;
|
||||
|
||||
if new_world && !config::env_is_subprocess_mode() {
|
||||
config::allocate_new_world(project_dir, &mut config)?;
|
||||
println!("[MCDK] 创建新存档:{}", config.world_folder_name);
|
||||
}
|
||||
|
||||
config::ensure_game_executable(project_dir, &mut config)?;
|
||||
|
||||
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
||||
|
||||
Reference in New Issue
Block a user