fix(coexist): 复审 r1 返工——帮助文案跟上新落点 + 「替换了清单」进人读通道 + 导入建实例绑到内容目录(M216 P2/P3/§5)
P2-1 ② 改了缺省落点,CLI 自己的文档还在写旧行为 - `cli/help.rs`「目标目录缺省 = 数据根的共享游戏目录(.minecraft/)」→ 新实例目录 `<数据根>/instances/<实例名>`(并登记实例); - `cli/model.rs` 的 `PackImportArgs::target` 字段文档同步。 - 用例 `help_describes_the_new_default_target`(断言帮助含新落点、不含旧落点)。 P2-2 ③ 的「显式说明」进不了任何人读通道 - CLI:`human_lines` 原只遍历 `plan.warnings`,而运行期告警(含「已替换版本清单 …旧的那份 备份在 …」)在 `report.warnings` 里 ⇒ `ull install` 人读输出从没说过这句话。现在有 report 就 报 report.warnings(它开头就是 plan 告警的副本),`manifest=Replaced` 再单独打一行备份路径。 - GUI:`install.rs` 新增 `Job::journal_report`(把 `report.warnings` 与 `manifest` 的备份路径写进 安装日志),`install/routes.rs` 两处 `ensure_complete` 之前各加一行调用(tower 批的两行)。 此前 `report.manifest` 全仓零消费者。 - 用例 `a_replaced_manifest_is_said_out_loud_in_the_human_output`(走真 `InstallPlan` 排版)。 P3-1 缺省目录被占的 hint 删掉不成立的 `--overwrite` 那半句(它绕过不了这条预检)。 P3-2 GUI 落点改成委托 core 的 `pack_dir::instance_dir`(此前 presenter 自己拼 `data()/instances/…`); `convert::dir_name` 缩成 `#[cfg(test)]` 的委托入口(生产侧已无调用点,留着继续钉归一化判据)。 P3-3 补 `the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it`:不带 `--target` 跑通整条 CLI,断言落点、内容、登记、以及实例设置里的 `isolation`/`gameDir`。 §5 导入建出来的实例原先「导入成功但内容永远不会被启动用到」 - 新增 `pack_dir::bind_instance_dir`(`InstanceConfig.isolation = Custom` + `game_dir = 导入目录`) 与 `pack_dir::bind_created_instance`(判据+文案,CLI 与 GUI 同一份); - CLI 在缺省建实例后、GUI 在 `import.rs::run` 里各调一次;只在「我们新建了实例目录且档案是新建」 时绑(显式 `--target` / `--no-register` / 复用已有档案都不动); - 运行目录 = 导入目录(saves/mods/logs/config),**versions/libraries/assets 仍按 D20 恒共享**; - 未改 `derive_game_dir`。 - 用例:正向 `binding_an_imported_instance_points_it_at_the_import_dir`、反向 `a_freshly_registered_instance_still_inherits_the_global_mode`。
This commit is contained in:
1 parent
96f15c79fb
commit
4e1323212b
10 files changed
+359
-45
No files matched your search
@@ -11,6 +11,8 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::instance::{self, InstanceConfig, Isolation};
|
||||
use crate::game::error::GameError;
|
||||
use crate::platform::layout::LauncherLayout;
|
||||
|
||||
/// 包文件名 → 实例候选名(`Better MC.mrpack` → `Better MC`)。
|
||||
@@ -89,6 +91,82 @@ pub fn import_target(
|
||||
Ok((target.to_path_buf(), name))
|
||||
}
|
||||
|
||||
/// 把「导入建出来的实例」绑到它的内容目录(`isolation=custom` + `game_dir`)。
|
||||
///
|
||||
/// # 为什么必须绑
|
||||
///
|
||||
/// 运行目录不是「导入目录」,而是从 `config/instances/<id>.json` 的 `isolation` 派生的
|
||||
/// ([`derive_game_dir`](crate::game::isolation::derive_game_dir)):缺省档
|
||||
/// `instances.mode=isolated` 给成 `<game>/versions/<versionId>`,`shared` 给成游戏根。
|
||||
/// 导入目标 `<data>/instances/<名>` **不在这条链上** ⇒ 不绑的话「导入成功」而那批
|
||||
/// mods/config 永远不会被那条实例的启动用到(M113 起 GUI 就有这个洞)。
|
||||
///
|
||||
/// # 绑成什么、不绑成什么
|
||||
///
|
||||
/// - 运行目录 = `dir`:`saves/` `mods/` `logs/` `config/` 落在这里(与别的启动器隔离);
|
||||
/// - **`versions/` `libraries/` `assets/` 仍按 D20 恒共享**在游戏根 —— 不是「完全隔离」,
|
||||
/// 版本本体只装一份,别人 `.minecraft` 里已下好的库也照旧复用。
|
||||
///
|
||||
/// # 回滚
|
||||
///
|
||||
/// 把 `config/instances/<id>.json` 的 `isolation` 删掉(或写成 `"inherit"`)即回到「跟随全局
|
||||
/// 档」;`gameDir` 随之失效。也可以整份删掉这一份实例设置文件(`register` 会重新补最小那份)。
|
||||
/// **不会**搬动任何已落盘的内容。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 读/写实例设置失败(JSON 坏、id 不合法、原子写失败)。
|
||||
pub fn bind_instance_dir(
|
||||
layout: &LauncherLayout,
|
||||
instance_id: &str,
|
||||
version_id: &str,
|
||||
dir: &Path,
|
||||
) -> Result<(), GameError> {
|
||||
let mut config = instance::read_instance_config(layout, instance_id)?
|
||||
.unwrap_or_else(|| InstanceConfig::new(instance_id, version_id));
|
||||
config.isolation = Some(Isolation::Custom);
|
||||
config.game_dir = Some(dir.to_path_buf());
|
||||
instance::write_instance_config(layout, &config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导入收尾:把「我们新建出来的实例」绑到它的内容目录,返回一句给人读的结论。
|
||||
///
|
||||
/// 判据(CLI 与 GUI 共用一份,别自己再写一遍):
|
||||
/// - `auto_bound == false` ⇒ 不绑(用户显式给了目标目录 / `--no-register`:那目录可能是别人的
|
||||
/// 运行目录,也可能被用户自己指过);
|
||||
/// - 没登记、或**复用了已有档案**(`registered.created == false`)⇒ 不绑;
|
||||
/// - 其余 ⇒ [`bind_instance_dir`]。
|
||||
///
|
||||
/// 参数直接吃 `addon::import::ImportReport`:`addon` 早就在调 `game::install::register`,
|
||||
/// 两者同属 core 内部;再投影一层 `struct` 只会让两侧各多写六行构造。
|
||||
#[must_use]
|
||||
pub fn bind_created_instance(
|
||||
layout: &LauncherLayout,
|
||||
target: &Path,
|
||||
auto_bound: bool,
|
||||
report: &crate::addon::import::ImportReport,
|
||||
) -> Option<String> {
|
||||
if !auto_bound {
|
||||
return None;
|
||||
}
|
||||
let registered = report.registered.as_ref().filter(|item| item.created)?;
|
||||
Some(
|
||||
match bind_instance_dir(layout, ®istered.id, ®istered.version_id, target) {
|
||||
Ok(()) => format!(
|
||||
"运行目录已指向 {}(实例 {})",
|
||||
target.display(),
|
||||
registered.name
|
||||
),
|
||||
// 内容与档案都已就位,只是目录没指过来 ⇒ 不把整次导入判失败,但必须说出来。
|
||||
Err(error) => format!(
|
||||
"实例已登记但没能绑到导入目录({error}):启动前请把该实例的运行目录指到 {}",
|
||||
target.display()
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 目录/文件名用的 slug(只留 ASCII 字母数字与 `-` / `_`,其余折成 `-`)。
|
||||
#[must_use]
|
||||
pub fn slug(text: &str) -> String {
|
||||
@@ -189,4 +267,47 @@ mod tests {
|
||||
assert_eq!((dir, name.as_str()), (explicit, "x"));
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
/// **§5 正向**:导入建出来的实例必须绑到内容目录,否则「导入成功」而内容永远不会被启动用到。
|
||||
///
|
||||
/// 抓得住:`bind_instance_dir` 没写 `isolation=custom` / `game_dir` 落空 / 写错了实例(读回来
|
||||
/// 仍是 `None`)。抓不住:`derive_game_dir` 真的把运行目录算成那个目录——那条由
|
||||
/// `isolation.rs` 的既有用例钉住,这里不重算(tower 要求不动 `derive_game_dir`)。
|
||||
#[test]
|
||||
fn binding_an_imported_instance_points_it_at_the_import_dir() {
|
||||
let root = std::env::temp_dir().join(format!("ull216-bind-{}", std::process::id()));
|
||||
let layout = LauncherLayout::new(root.join(".ULL"));
|
||||
let registration =
|
||||
crate::game::install::instance::register(&layout, "pack", "1.20.1", None)
|
||||
.expect("登记");
|
||||
let id = registration.profile.id.as_str().to_string();
|
||||
let dir = instance_dir(&layout, "pack");
|
||||
|
||||
bind_instance_dir(&layout, &id, "1.20.1", &dir).expect("绑定");
|
||||
let config = crate::game::install::instance::read_instance_config(&layout, &id)
|
||||
.expect("读设置")
|
||||
.expect("有设置文件");
|
||||
assert_eq!(config.isolation, Some(Isolation::Custom));
|
||||
assert_eq!(config.game_dir.as_deref(), Some(dir.as_path()));
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
/// **§5 反向**:没走绑定那条路的实例不许自己冒出 `custom`(否则上一条抓不住东西)。
|
||||
#[test]
|
||||
fn a_freshly_registered_instance_still_inherits_the_global_mode() {
|
||||
let root = std::env::temp_dir().join(format!("ull216-unbound-{}", std::process::id()));
|
||||
let layout = LauncherLayout::new(root.join(".ULL"));
|
||||
let registration =
|
||||
crate::game::install::instance::register(&layout, "plain", "1.20.1", None)
|
||||
.expect("登记");
|
||||
let config = crate::game::install::instance::read_instance_config(
|
||||
&layout,
|
||||
registration.profile.id.as_str(),
|
||||
)
|
||||
.expect("读设置")
|
||||
.expect("有设置文件");
|
||||
assert_eq!(config.isolation, None, "缺省必须跟随全局档");
|
||||
assert_eq!(config.game_dir, None);
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user