From 905e905ecaa4c2eb66d3d4e224cfe615c5ed8589 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sat, 19 Sep 2026 19:37:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20SLS=20=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E5=88=A0=E9=99=A4=E6=AE=8B=E7=95=99=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=B8=8E=E9=87=8D=E8=A3=85=E5=85=B1=E7=94=A8?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除实例残留外部数据: - remove_instance 原先只在 game_dir_override 指向 versions/ 隔离目录时才删除外部目录;SLS(hosted)实例使用版本共享布局 (外部目录直接作为游戏根),删除时落到托管目录分支,外部 mods/ 存档/配置残留。 - 现在 game_dir_override 只要指向实例独占目录(非共享 .minecraft 根)就一并删除;新增 is_shared_minecraft_root 判据:含 libraries/ 或 assets/ 的目录视为共享游戏根,删除实例时保留。 重装共用同一文件夹: - hosted::create 生成 game_dir_override 时直接拼接 `/`,无冲突处理;同一整合包安装两次会指向同一 目录,两个实例共用一份游戏数据。 - 新增 unique_game_dir:目标目录已存在时依次尝试 ` (1)`、 ` (2)` …,与 create_instance::resolve_instance_path 的 实例目录去重逻辑保持一致。 --- packages/app-lib/src/api/pack/hosted.rs | 44 ++++++++++++++++--- .../instances/commands/remove_instance.rs | 24 +++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/app-lib/src/api/pack/hosted.rs b/packages/app-lib/src/api/pack/hosted.rs index ad1cb13..1bc6023 100644 --- a/packages/app-lib/src/api/pack/hosted.rs +++ b/packages/app-lib/src/api/pack/hosted.rs @@ -436,16 +436,23 @@ pub async fn create( // e.g. `/`. Avoid a `versions/` layout: that shape // is reserved for externally linked launcher instances and would make the // launcher expect a Minecraft version JSON beside the pack. - let game_dir_override = game_dir_root + let game_dir_override = match game_dir_root .as_deref() .map(str::trim) .filter(|root| !root.is_empty()) - .map(|root| { - Path::new(root) - .join(&publication.manifest.name) - .to_string_lossy() - .into_owned() - }); + { + Some(root) => { + // The pack's game files live in their own folder under the chosen + // root, e.g. `/`. If that folder already exists + // (a previous install of the same pack, or a name clash), pick a + // suffixed sibling instead of sharing the folder with another + // instance. + let base = Path::new(root).join(&publication.manifest.name); + let resolved = unique_game_dir(&base); + Some(resolved.to_string_lossy().into_owned()) + } + None => None, + }; let instance = crate::state::create_instance( crate::state::CreateInstance { name: publication.manifest.name.clone(), @@ -480,6 +487,29 @@ pub async fn create( Ok(instance.id) } +/// Returns `base` when its directory does not exist yet; otherwise returns the +/// first `base (n)` (n = 1, 2, …) whose directory is still free. Mirrors the +/// instance-folder de-duplication in `create_instance::resolve_instance_path`, +/// so re-installing the same hosted pack no longer makes two instances share a +/// single game folder. +fn unique_game_dir(base: &Path) -> PathBuf { + if !base.exists() { + return base.to_path_buf(); + } + let parent = base.parent().unwrap_or_else(|| Path::new("")); + let name = base + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "instance".to_string()); + let mut which = 1u32; + loop { + let candidate = parent.join(format!("{name} ({which})")); + if !candidate.exists() { + return candidate; + } + which += 1; + } +} pub async fn binding(instance_id: &str) -> crate::Result> { read_json(&crate::instance::get_full_path(instance_id).await?, BINDING) .await diff --git a/packages/app-lib/src/state/instances/commands/remove_instance.rs b/packages/app-lib/src/state/instances/commands/remove_instance.rs index d2d8993..4b18b0b 100644 --- a/packages/app-lib/src/state/instances/commands/remove_instance.rs +++ b/packages/app-lib/src/state/instances/commands/remove_instance.rs @@ -32,12 +32,17 @@ pub(crate) async fn remove_instance( .game_dir_override .as_deref() .map(PathBuf::from) - .filter(|path| is_version_isolated_game_dir(path)) + .filter(|path| { + // Delete the external game directory when the instance owns it. + // A version-isolated `versions/` folder is obviously + // exclusive; a plain `/` folder is also owned by + // this instance. A shared `.minecraft` root, however, holds the + // game's libraries/assets and must never be deleted with one + // instance. + is_version_isolated_game_dir(path) + || !is_shared_minecraft_root(path) + }) { - // New instances created against a configured `.minecraft` root use - // a private `versions/` directory. Remove that external - // directory when the instance is deleted, while preserving shared - // (non-isolated) overrides for backwards compatibility. game_dir_override } else { state.directories.instances_dir().join(&instance.path) @@ -69,3 +74,12 @@ fn is_version_isolated_game_dir(path: &Path) -> bool { .and_then(|name| name.to_str()) == Some("versions") } + +/// Heuristic: a shared `.minecraft` root holds the game's libraries and +/// assets, which must survive the removal of any single instance that points +/// at it. Instance-owned external folders (e.g. a hosted modpack's +/// `/` directory) contain only mods/saves/config and no such +/// shared game body. +fn is_shared_minecraft_root(path: &Path) -> bool { + path.join("libraries").is_dir() || path.join("assets").is_dir() +}