feat(debug): 支持 --new 参数创建全新调试存档

添加  选项,启动调试时自动生成带时间戳的新存档目录
(格式 MC_DEV_WORLD_YYYYMMDD_HHMMSS)并持久化到 .mcdev.json。

- 新增 local_timestamp_compact() 跨平台本地时间格式化
- 新增 allocate_new_world() 更新配置并落盘
- 补充单元测试验证存档名格式与持久化正确性
This commit is contained in:
2026-05-16 17:24:13 +08:00
parent 011b59c948
commit fd79a99477
6 changed files with 140 additions and 3 deletions

View File

@@ -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",
] }

View File

@@ -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);
}
}

View File

@@ -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,
}

View File

@@ -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();

View File

@@ -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")
}

View File

@@ -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)?;