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_IO",
|
||||||
"Win32_System_Pipes",
|
"Win32_System_Pipes",
|
||||||
"Win32_System_SystemServices",
|
"Win32_System_SystemServices",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub fn execute(args: &DebugArgs) {
|
|||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|| 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);
|
eprintln!("Error: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,4 +92,7 @@ pub struct DebugArgs {
|
|||||||
/// The path of the project (default: current directory)
|
/// The path of the project (default: current directory)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub path: Option<String>,
|
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(())
|
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 {
|
fn create_default_config() -> DebugConfig {
|
||||||
DebugConfig {
|
DebugConfig {
|
||||||
included_mod_dirs: default_included_mod_dirs(),
|
included_mod_dirs: default_included_mod_dirs(),
|
||||||
@@ -333,7 +344,39 @@ pub fn strip_json_comments(input: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
#[test]
|
||||||
fn strips_jsonc_comments_without_touching_strings() {
|
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]
|
#[test]
|
||||||
fn default_current_project_resolves_to_absolute_cwd() {
|
fn default_current_project_resolves_to_absolute_cwd() {
|
||||||
let resolved = resolve_mod_dir(std::path::Path::new("."), "./", true, true).unwrap();
|
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")
|
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 {
|
pub fn behavior_packs_path() -> PathBuf {
|
||||||
games_com_netease_path().join("behavior_packs")
|
games_com_netease_path().join("behavior_packs")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ use std::path::Path;
|
|||||||
|
|
||||||
use crate::error::Result;
|
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)?;
|
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)?;
|
config::ensure_game_executable(project_dir, &mut config)?;
|
||||||
|
|
||||||
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user