From fd79a99477a21bf84f78d0ad67032742289c9aad Mon Sep 17 00:00:00 2001 From: Blank038 Date: Sat, 16 May 2026 17:24:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(debug):=20=E6=94=AF=E6=8C=81=20--new=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=88=9B=E5=BB=BA=E5=85=A8=E6=96=B0=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E5=AD=98=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 选项,启动调试时自动生成带时间戳的新存档目录 (格式 MC_DEV_WORLD_YYYYMMDD_HHMMSS)并持久化到 .mcdev.json。 - 新增 local_timestamp_compact() 跨平台本地时间格式化 - 新增 allocate_new_world() 更新配置并落盘 - 补充单元测试验证存档名格式与持久化正确性 --- Cargo.toml | 1 + src/commands/debug.rs | 2 +- src/commands/mod.rs | 3 ++ src/debug/config.rs | 71 ++++++++++++++++++++++++++++++++++++++++++- src/debug/env.rs | 58 +++++++++++++++++++++++++++++++++++ src/debug/mod.rs | 8 ++++- 6 files changed, 140 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69a11c0..ae9771d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", ] } diff --git a/src/commands/debug.rs b/src/commands/debug.rs index adbbd6f..2e25205 100644 --- a/src/commands/debug.rs +++ b/src/commands/debug.rs @@ -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); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b47676b..532d1c0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -92,4 +92,7 @@ pub struct DebugArgs { /// The path of the project (default: current directory) #[arg(short, long)] pub path: Option, + /// Create a fresh debug world and persist it in .mcdev.json + #[arg(short = 'n', long = "new")] + pub new_world: bool, } diff --git a/src/debug/config.rs b/src/debug/config.rs index f589acd..64e02f0 100644 --- a/src/debug/config.rs +++ b/src/debug/config.rs @@ -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(); diff --git a/src/debug/env.rs b/src/debug/env.rs index 4218c8a..19a254b 100644 --- a/src/debug/env.rs +++ b/src/debug/env.rs @@ -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") } diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 5da44ba..49ebdfd 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -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)?;