r1 复审的两条 P2。
## P2-1:外部实例不再一律 isolated
`versions/<id>/` 里出现 `mods` / `saves` / `config` / `options.txt` 任一 ⇒ `isolated`
(gameDir = `versions/<id>`);否则 ⇒ `shared`(gameDir = 游戏根 `.minecraft`)。
判据只 `exists()`,只读,不改盘;写盘仍然只在用户第一次改设置时(`ensure_profile`)。
- 判据收在 `host/game/discovered.rs::base_mode()`,签名 `(env, id, version_id)`:
**有档案的实例返回全局 `instances.mode`**(D20 原口径,一字不变),自动发现的按痕迹判。
- 派生运行目录的五处(启动 / mods / saves / addon install / addon pack)从传
`global_mode` 改成传 `base_mode` ⇒ 「读到的存档/模组在哪」与磁贴上的 ISOLATION 同一份判据。
- `isolation_of(env, id, version_id)` 多收一个版本 id:实例文件优先,缺省 `base_mode`。
于是懒创建档案(只写 launch 段、isolation 仍为空)之后,磁贴不会从「共用目录」翻回「独立目录」。
- 磁贴/详情:自动发现的实例多一句「运行目录由盘上已有的文件决定」(i18n 中英);
按启动时再弹一条说清**这一次**用哪一边(`pages::instances::discovered_run_toast`,
同文案在屏不重复叠加)。
- `dto::DISCOVERED_ID_PREFIX` / `is_discovered_instance` 成为「自动发现」的公开判据
(不给 `InstanceSummary` 加字段:那要改 24 处结构体字面量,跨 agent / cli)。
## P2-2:空态用例的覆盖面
r1 复审实测:把 `!block.rows.is_empty()` 改成 `block.rows.is_empty()`(0 实例时反而没按钮),
当时三条用例全绿。现在:
- `detail_empty` 拆成 `empty_not_selected` / `empty_no_instances` 两个函数,分支各自可断言;
- `the_cta_lives_only_in_the_no_instances_branch`:CTA 只在「一个都没有」那一支、
「没选中」那一支必须是 `Vec::new()`,且分派必须写成 `if has_instances {`(取反即红);
- `the_page_asks_the_empty_state_with_the_negated_row_count`:两条调用点(宽窗 + 窄窗)
都必须传 `!block.rows.is_empty()`,条数也钉住(多一条即红)。
- 两个变异已自证:传参取反 ⇒ 后一条红;CTA 挪支 ⇒ 前一条红(红原文见交付报告)。
## P3
- `java::target_major()` 改读并集快照:只有自动发现实例的用户原来会拿到默认 JDK17 而不是
该版本要求的 JDK(`profiles.json` 是空的)。有档案时顺序与旧写法一致。
- `instances()` 全量扫仍在 UI 线程——本轮不动,刷新时机与代价写进报告(塔另开一条)。
## 结构
`host/game.rs` 394 → 286 行:自动发现那一簇(痕迹判据 / 扫出来的版本 / 合成视图)搬到
`host/game/discovered.rs`(138 行),只为守 400 行硬线;`game::base_mode` 仍是同一条路径。
## 门禁
app-iced lib 973 passed / presenter all-targets 386 passed / i18n 36 passed /
fmt 0 / clippy -D warnings 0。
199 lines
9.1 KiB
Rust
199 lines
9.1 KiB
Rust
use super::*;
|
||
|
||
/// 版本 id → DTO 加载器的接口面(解析在 core,这里只验翻译没走样)。
|
||
///
|
||
/// 抓得住:显示路径把某个种类翻错/翻塌(例如 Quilt 写成 Forge)——`dto_kind` 的委派与
|
||
/// `dto` 的 `from_core` 都落在这一条上;以及 core 解析端退回老口径(`fabric-loader-…` 才认、
|
||
/// 自家 `1.20.4-fabric-0.16.5` 认不出)——后两条正是 M122 的 bug 面。
|
||
/// 抓不住:core 解析规则本身(真值表在 `unlucky_core::game::loader` 的测试里,那边才是
|
||
/// 真相源);也抓不住「版本号为空时不显示品牌名」那条(在 `LoaderInfo::badge` 与
|
||
/// app-iced 的 `loader_badge`,M120 已修,别在这里重复)。
|
||
#[test]
|
||
fn loader_parsing_covers_all_shapes() {
|
||
let cases = [
|
||
("1.20.4", LoaderKind::Vanilla, ""),
|
||
// 自家口径(生成端 `LoaderKind::version_id`):`<mc>-<kind>-<loader>`
|
||
("1.20.4-fabric-0.16.5", LoaderKind::Fabric, "0.16.5"),
|
||
("1.20.4-quilt-0.27.0", LoaderKind::Quilt, "0.27.0"),
|
||
("1.20.1-forge-47.2.0", LoaderKind::Forge, "47.2.0"),
|
||
("1.21.1-neoforge-21.1.80", LoaderKind::NeoForge, "21.1.80"),
|
||
// 老 HMCL 口径(存量实例还会带着它们)
|
||
("fabric-loader-0.16.5-1.20.1", LoaderKind::Fabric, "0.16.5"),
|
||
("quilt-loader-0.24.0-1.20.1", LoaderKind::Quilt, "0.24.0"),
|
||
// 第三方口径
|
||
("1.13.2-OptiFine_HD_U_E6", LoaderKind::OptiFine, "HD_U_E6"),
|
||
("1.12.2-LiteLoader", LoaderKind::LiteLoader, ""),
|
||
];
|
||
for (id, kind, version) in cases {
|
||
let info = loader_from_id(id);
|
||
assert_eq!(info.kind, kind, "{id}");
|
||
assert_eq!(info.version, version, "{id}");
|
||
assert!(!info.badge().is_empty());
|
||
}
|
||
}
|
||
|
||
/// 显示路径的**全量**锚:core 的每一种加载器,从自家版本 id 走到磁贴时仍然是**它自己**。
|
||
///
|
||
/// 清单取自 [`unlucky_core::game::LoaderKind::ALL`],**不手抄**:M126 r3 复审实测出的缺口正是
|
||
/// 「手抄的清单跟着漏」——core 加第八家、契约与 help 都补齐、只在 `dto_kind` 那一支写
|
||
/// `=> Vanilla`,三个 crate 全绿而磁贴静默显示「原版」。`ALL` 由 core 的宏表长出来,加一家
|
||
/// 自动进圈,测试作者不需要(也没法)记得补一行。
|
||
///
|
||
/// 抓得住:显示路径任何一环把种类翻塌成原版——`dto_kind` 又变回手写映射、`dto` 的
|
||
/// `from_core` 那一支接错、`label()` 被写成「原版」;**包括新加的那种**(那时 `ALL` 先变长)。
|
||
/// 抓不住:core 的 id 解析规则本身(真值表在 `unlucky_core::game::loader`);版本号为空的
|
||
/// 徽标判据(M120 的 `loader_badge`,本 crate 不渲染)。
|
||
#[test]
|
||
fn every_core_loader_kind_reaches_the_tile_as_itself() {
|
||
use unlucky_core::game::LoaderKind as Core;
|
||
for kind in Core::ALL {
|
||
let info = loader_from_id(&format!("1.20.4-{}-0.16.5", kind.as_str()));
|
||
assert_eq!(
|
||
info.kind.as_str(),
|
||
kind.as_str(),
|
||
"core 的 {kind:?} 走到磁贴时变成了 `{}`(徽标 `{}`)",
|
||
info.kind.as_str(),
|
||
info.badge()
|
||
);
|
||
assert_eq!(
|
||
info.badge().starts_with(LoaderKind::Vanilla.label()),
|
||
kind == Core::Vanilla,
|
||
"core 的 {kind:?} 在磁贴上的徽标是 `{}`",
|
||
info.badge()
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 端到端(数据层):真装出来的四个加载器实例,走 [`instances`] 的快照各自带对加载器。
|
||
///
|
||
/// 造的是本启动器**真实产物**的形状:`profiles.json` 里 `versionId` 是自家 id,
|
||
/// `versions/<id>/<id>.json` 里 `inheritsFrom` 指向原版层(生成端见
|
||
/// `unlucky_core::addon::loader`)。原版版本号由此取自继承链根,不是从 id 里切的。
|
||
///
|
||
/// 抓得住:`describe_version` ↔ `loader_from_id` ↔ `instances` 这条链上任何一环把加载器
|
||
/// 丢掉(磁贴会显示「原版」——M122 的用户可见症状);以及继承链根取错(磁贴的版本号)。
|
||
/// 抓不住:实例页真出图(走 app-iced 的视图模型与 `loader_badge`,本 crate 不渲染);
|
||
/// 也抓不住「真下载安装了 Fabric/Quilt/Forge/NeoForge」——这里只伪造落盘结果。
|
||
#[test]
|
||
fn own_installed_instances_snapshot_carries_their_loader() {
|
||
let dir = std::env::temp_dir().join(format!("ull-host-game-own-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let env = HostEnv::at_root(&dir);
|
||
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
|
||
|
||
let cases = [
|
||
(
|
||
"1.20.4-fabric-0.16.5",
|
||
"1.20.4",
|
||
LoaderKind::Fabric,
|
||
"Fabric 0.16.5",
|
||
),
|
||
(
|
||
"1.20.4-quilt-0.27.0",
|
||
"1.20.4",
|
||
LoaderKind::Quilt,
|
||
"Quilt 0.27.0",
|
||
),
|
||
(
|
||
"1.20.1-forge-47.2.0",
|
||
"1.20.1",
|
||
LoaderKind::Forge,
|
||
"Forge 47.2.0",
|
||
),
|
||
(
|
||
"1.21.1-neoforge-21.1.80",
|
||
"1.21.1",
|
||
LoaderKind::NeoForge,
|
||
"NeoForge 21.1.80",
|
||
),
|
||
];
|
||
for (version_id, mc_version, _, _) in cases {
|
||
for (id, inherits) in [(mc_version, None), (version_id, Some(mc_version))] {
|
||
let json = match inherits {
|
||
Some(parent) => format!(r#"{{"id":"{id}","inheritsFrom":"{parent}"}}"#),
|
||
None => format!(r#"{{"id":"{id}"}}"#),
|
||
};
|
||
let path = env.game.version_json(id);
|
||
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("版本目录");
|
||
std::fs::write(&path, json).expect("写版本 JSON");
|
||
}
|
||
}
|
||
|
||
let profiles = ProfileList {
|
||
profiles: cases
|
||
.iter()
|
||
.map(
|
||
|(version_id, _, _, _)| unlucky_core::game::InstanceProfile {
|
||
id: unlucky_core::game::ProfileId::generate(),
|
||
name: format!("装好的 {version_id}"),
|
||
version_id: (*version_id).to_string(),
|
||
game_dir: None,
|
||
running_dir: None,
|
||
java_path: None,
|
||
created_at: None,
|
||
last_played_at: None,
|
||
extra: BTreeMap::new(),
|
||
},
|
||
)
|
||
.collect(),
|
||
..ProfileList::default()
|
||
};
|
||
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles).expect("写清单");
|
||
|
||
let snapshot = instances(&env);
|
||
// M273 之后 `versions/*` 也是实例来源:这个夹具为了造 `inheritsFrom` 还写了三个父版本目录
|
||
// (`1.20.4` / `1.20.1` / `1.21.1`)而**没有**给它们建档案,于是它们会作为「外部实例」出现。
|
||
// 这条用例守的是「这条链把加载器/版本号翻对」——所以只看它自己那四个版本,条数也在这一层断言;
|
||
// 「盘上有版本就出现在列表里」由 `tests_union.rs` 独占。
|
||
let snapshot: Vec<_> = snapshot
|
||
.into_iter()
|
||
.filter(|tile| cases.iter().any(|case| case.0 == tile.version_id))
|
||
.collect();
|
||
assert_eq!(snapshot.len(), cases.len());
|
||
for ((version_id, mc_version, kind, badge), tile) in cases.iter().zip(&snapshot) {
|
||
assert_eq!(tile.version_id.as_str(), *version_id);
|
||
assert_eq!(
|
||
tile.mc_version.as_str(),
|
||
*mc_version,
|
||
"{version_id} 的版本号取错"
|
||
);
|
||
assert_eq!(tile.loader.kind, *kind, "{version_id} 的加载器");
|
||
assert_eq!(tile.loader.badge().as_str(), *badge, "{version_id} 的徽标");
|
||
assert!(tile.version_present, "{version_id} 的载荷应当存在");
|
||
}
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
#[test]
|
||
fn isolation_write_then_read_round_trips() {
|
||
let dir = std::env::temp_dir().join(format!("ull-host-game-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let env = HostEnv::at_root(&dir);
|
||
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
|
||
|
||
// 直接建一份 profiles.json(register 需要版本本体,这里只测隔离写读)
|
||
let profiles = ProfileList {
|
||
profiles: vec![unlucky_core::game::InstanceProfile {
|
||
id: unlucky_core::game::ProfileId::generate(),
|
||
name: "demo".to_string(),
|
||
version_id: "1.20.1".to_string(),
|
||
game_dir: None,
|
||
running_dir: None,
|
||
java_path: None,
|
||
created_at: None,
|
||
last_played_at: None,
|
||
extra: BTreeMap::new(),
|
||
}],
|
||
..ProfileList::default()
|
||
};
|
||
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles).expect("写清单");
|
||
let id = profiles.profiles[0].id.as_str().to_string();
|
||
|
||
// 有档案的实例:基底档 = 全局档(`isolated`),与 M273 之前一字不差。
|
||
assert_eq!(isolation_of(&env, &id, "1.20.1"), "isolated");
|
||
set_isolation(&env, &id, "shared").expect("切档位");
|
||
assert_eq!(isolation_of(&env, &id, "1.20.1"), "shared");
|
||
assert!(set_isolation(&env, &id, "nope").is_err());
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|