fix(presenter): 自动发现实例按盘上痕迹选运行目录 + 空态用例补上分支/传参(M273 r1)
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。
This commit is contained in:
1 parent
5479f25068
commit
c5009442c5
21 files changed
+494
-157
No files matched your search
@@ -26,3 +26,18 @@ pub use super::launch_failure::{LaunchFailureCause, LaunchFailureStage, LaunchFa
|
||||
|
||||
/// 实例标识:core::game 生成的稳定 id,app 视为不透明字符串。
|
||||
pub type InstanceId = String;
|
||||
|
||||
/// **自动发现**实例的 id 前缀(M273):`versions/<id>` 里扫出来、没有档案的版本用
|
||||
/// `ext-<hash>` 作实例 id。
|
||||
///
|
||||
/// 为什么让它是公开常量而不给 [`InstanceSummary`] 加字段:加字段要改 24 处结构体字面量
|
||||
/// (横跨 presenter / agent / cli / app-iced),而「这个实例是不是自动发现的」在界面上的
|
||||
/// 用途只有一处——详情页那句「为什么它按独立目录 / 共用目录跑」。前缀是**稳定契约**,
|
||||
/// 由 `host::game` 的 `external_instance_id` 唯一生产(M274 多根时前缀不变,哈希里加根)。
|
||||
pub const DISCOVERED_ID_PREFIX: &str = "ext-";
|
||||
|
||||
/// 这个实例是不是「从盘上自动发现」的(没有用户 / 向导建过的档案)。
|
||||
#[must_use]
|
||||
pub fn is_discovered_instance(id: &str) -> bool {
|
||||
id.starts_with(DISCOVERED_ID_PREFIX)
|
||||
}
|
||||
@@ -61,8 +61,9 @@ pub use install::{
|
||||
InstallStage,
|
||||
};
|
||||
pub use instances::{
|
||||
CreateRequest, InstanceId, InstanceRuntime, InstanceSummary, LaunchFailureCause,
|
||||
LaunchFailureStage, LaunchFailureView, LaunchStage, LoaderInfo, LoaderKind, Progress,
|
||||
is_discovered_instance, CreateRequest, InstanceId, InstanceRuntime, InstanceSummary,
|
||||
LaunchFailureCause, LaunchFailureStage, LaunchFailureView, LaunchStage, LoaderInfo, LoaderKind,
|
||||
Progress, DISCOVERED_ID_PREFIX,
|
||||
};
|
||||
pub use java::{JavaChainOutcome, JavaChainStep, JavaPageView, JavaRuntimeView, JavaVendorView};
|
||||
pub use launch_args::{LaunchArgsPatch, LaunchArgsView};
|
||||
|
||||
@@ -288,20 +288,14 @@ fn find_instance(env: &HostEnv, id: &str) -> Result<InstanceSummary, ContentFail
|
||||
.ok_or_else(|| local(format!("找不到实例 {id}")))
|
||||
}
|
||||
|
||||
/// 实例的运行目录(D20 覆盖链:实例 `isolation` > 全局 `instances.mode`)。
|
||||
/// 实例的运行目录(D20 覆盖链:实例 `isolation` > 基底档,见 `game::base_mode`)。
|
||||
fn game_dir_of(env: &HostEnv, instance: &InstanceSummary) -> Result<PathBuf, ContentFailure> {
|
||||
let global = super::super::game::global_mode(env);
|
||||
let base = super::super::game::base_mode(env, &instance.id, &instance.version_id);
|
||||
let config = read_instance_config(&env.layout, &instance.id)
|
||||
.map_err(|error| local(format!("读实例设置失败:{error}")))?;
|
||||
derive_game_dir(
|
||||
&env.game,
|
||||
&instance.version_id,
|
||||
global,
|
||||
config.as_ref(),
|
||||
None,
|
||||
)
|
||||
.map(|derivation| derivation.game_dir)
|
||||
.map_err(|error| local(format!("隔离目录派生失败:{error}")))
|
||||
derive_game_dir(&env.game, &instance.version_id, base, config.as_ref(), None)
|
||||
.map(|derivation| derivation.game_dir)
|
||||
.map_err(|error| local(format!("隔离目录派生失败:{error}")))
|
||||
}
|
||||
|
||||
/// 只留文件名本身:站点给的名字可能带路径分隔符,落盘前必须砍掉
|
||||
|
||||
@@ -280,24 +280,18 @@ pub(super) fn send(tx: &JobSender, epoch: u64, event: PackEvent) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 实例 → 运行目录(与 `addon/install.rs` 同一条派生:实例隔离档 > 全局档)。
|
||||
/// 实例 → 运行目录(与 `addon/install.rs` 同一条派生:实例隔离档 > 基底档)。
|
||||
pub(super) fn instance_root(env: &HostEnv, id: &InstanceId) -> Result<PathBuf, String> {
|
||||
let instance = super::super::game::instances(env)
|
||||
.into_iter()
|
||||
.find(|instance| &instance.id == id)
|
||||
.ok_or_else(|| format!("instance not found: {id}"))?;
|
||||
let global = super::super::game::global_mode(env);
|
||||
let base = super::super::game::base_mode(env, &instance.id, &instance.version_id);
|
||||
let config = read_instance_config(&env.layout, &instance.id)
|
||||
.map_err(|error| format!("read instance config failed: {error}"))?;
|
||||
derive_game_dir(
|
||||
&env.game,
|
||||
&instance.version_id,
|
||||
global,
|
||||
config.as_ref(),
|
||||
None,
|
||||
)
|
||||
.map(|derivation| derivation.game_dir)
|
||||
.map_err(|error| format!("derive game dir failed: {error}"))
|
||||
derive_game_dir(&env.game, &instance.version_id, base, config.as_ref(), None)
|
||||
.map(|derivation| derivation.game_dir)
|
||||
.map_err(|error| format!("derive game dir failed: {error}"))
|
||||
}
|
||||
|
||||
/// 实例快照(清单的 `game` + `dependencies` 从它来)。读不出来就返回 `None`,
|
||||
|
||||
@@ -22,6 +22,12 @@ use crate::dto::{
|
||||
|
||||
use super::env::HostEnv;
|
||||
|
||||
mod discovered;
|
||||
|
||||
pub(super) use discovered::base_mode;
|
||||
|
||||
use discovered::{external_summary, external_version_of, external_versions};
|
||||
|
||||
/// 全局默认隔离档位(`instances.mode`;缺键/坏值按 D20 默认 `isolated`)。
|
||||
pub fn global_mode(env: &HostEnv) -> IsolationMode {
|
||||
env.store()
|
||||
@@ -108,12 +114,12 @@ fn dto_kind(kind: unlucky_core::game::LoaderKind) -> LoaderKind {
|
||||
})
|
||||
}
|
||||
|
||||
/// 实例的隔离档位(实例文件优先,缺省显示全局档位)。
|
||||
pub fn isolation_of(env: &HostEnv, id: &str) -> String {
|
||||
let global = global_mode(env);
|
||||
/// 实例的隔离档位(实例文件优先,缺省 = [`base_mode`])。
|
||||
pub fn isolation_of(env: &HostEnv, id: &str, version_id: &str) -> String {
|
||||
let base = base_mode(env, id, version_id);
|
||||
match read_instance_config(&env.layout, id) {
|
||||
Ok(Some(config)) => config.effective_isolation(global).as_str().to_string(),
|
||||
_ => global.as_str().to_string(),
|
||||
Ok(Some(config)) => config.effective_isolation(base).as_str().to_string(),
|
||||
_ => base.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,78 +179,13 @@ fn profile_summary(
|
||||
last_played: played.get(id).cloned(),
|
||||
runtime: InstanceRuntime::Idle,
|
||||
icon: None,
|
||||
isolation: isolation_of(env, id),
|
||||
isolation: isolation_of(env, id, &version_id),
|
||||
version_present: env.game.is_version_installed(&version_id),
|
||||
launch: super::launch_args::view(env, id, &version_id),
|
||||
version_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 盘上有版本、但没有档案绑定的那些版本 id(字典序,来自 core 的扫描)。
|
||||
///
|
||||
/// 扫描失败(`versions/` 读不出来 / 断链联接)时返回空:列表退回「只有档案」的旧行为。
|
||||
/// 这里**不**把错误往上抛——实例页打不开是比「少列几个外部实例」严重得多的故障。
|
||||
fn external_versions(env: &HostEnv, profiles: &ProfileList) -> Vec<String> {
|
||||
let Ok(scan) = unlucky_core::game::adopt::scan(&env.game, &env.layout) else {
|
||||
return Vec::new();
|
||||
};
|
||||
scan.candidates
|
||||
.into_iter()
|
||||
.filter(|candidate| candidate.adoptable())
|
||||
.filter(|candidate| {
|
||||
!profiles
|
||||
.profiles
|
||||
.iter()
|
||||
.any(|profile| profile.version_id == candidate.version_id)
|
||||
})
|
||||
.map(|candidate| candidate.version_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 外部实例的视图 id(= 懒创建时写进 `profiles.json` 的档案 id)。
|
||||
///
|
||||
/// **不能直接拿版本 id 当实例 id**:`core::platform::layout::is_valid_instance_id` 只收
|
||||
/// `[A-Za-z0-9_-]{1,64}`(`config/instances/<id>.json` 与启动链的 `read_instance_config` 都按
|
||||
/// 这个白名单校验),而版本目录名可以是中文——用户实测的那个就是 `樱落怡然客户端`。
|
||||
/// 直接沿用版本 id 会让**启动**在写盘前就死在 `LaunchFailureStage::InstanceConfig`。
|
||||
///
|
||||
/// 前缀 + 稳定哈希 ⇒ 同一个版本每次拿到同一个 id(懒创建因此幂等),且是合法实例 id。
|
||||
///
|
||||
/// **M274(多根发现)**:两个根里都可能有 `1.20.4`,所以这里是**唯一**该把「根」并进身份的
|
||||
/// 地方——到时把 `root` 也喂进哈希(`external_instance_id(root, version_id)`),
|
||||
/// 列表就自然出两条,不必动 [`instances`] 的其余部分。
|
||||
fn external_instance_id(version_id: &str) -> String {
|
||||
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for byte in version_id.as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
format!("ext-{hash:016x}")
|
||||
}
|
||||
|
||||
/// 一条外部版本 → 磁贴(显示名 = 版本目录名;**只读**,不落任何文件)。
|
||||
fn external_summary(
|
||||
env: &HostEnv,
|
||||
version_id: &str,
|
||||
played: &BTreeMap<String, String>,
|
||||
) -> InstanceSummary {
|
||||
let id = external_instance_id(version_id);
|
||||
let (mc_version, loader) = describe_version(env, version_id);
|
||||
InstanceSummary {
|
||||
last_played: played.get(&id).cloned(),
|
||||
isolation: isolation_of(env, &id),
|
||||
launch: super::launch_args::view(env, &id, version_id),
|
||||
version_present: env.game.is_version_installed(version_id),
|
||||
name: version_id.to_string(),
|
||||
version_id: version_id.to_string(),
|
||||
mc_version,
|
||||
loader,
|
||||
runtime: InstanceRuntime::Idle,
|
||||
icon: None,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 拿实例的档案;外部实例(盘上有版本、没有档案)**就地补一条并落盘**。
|
||||
///
|
||||
/// 只在「用户第一次改这个实例」时才走到这里([`set_isolation`] / 启动参数写回),于是
|
||||
@@ -279,16 +220,6 @@ pub(super) fn ensure_profile(env: &HostEnv, id: &str) -> Result<InstanceProfile,
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// 视图 id → 版本 id 的反查(只认「可登记、且还没有档案绑定」的版本)。
|
||||
///
|
||||
/// 与 [`external_versions`] 同一份判据:合成的视图 id 只在那个集合里有意义,
|
||||
/// 于是「界面上出现过的外部实例」与「能懒创建的外部实例」永远是同一批。
|
||||
fn external_version_of(env: &HostEnv, profiles: &ProfileList, id: &str) -> Option<String> {
|
||||
external_versions(env, profiles)
|
||||
.into_iter()
|
||||
.find(|version_id| external_instance_id(version_id) == id)
|
||||
}
|
||||
|
||||
/// 改实例隔离档位(写实例设置文件;不动任何数据)。
|
||||
///
|
||||
/// 外部实例(盘上有版本、还没有档案)**第一次**改档位时才懒创建档案并落盘
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! 自动发现的实例(M273):盘上扫出来的版本 → 磁贴,以及它们的**运行目录判据**。
|
||||
//!
|
||||
//! 从 [`super::instances`] 里拆出来只为守 400 行硬线(那边装了档案侧的翻译)。本文件的两件事:
|
||||
//!
|
||||
//! 1. **哪些盘上的版本算实例**:`versions/<id>/<id>.json` 能解析、没有被任何档案绑定
|
||||
//! (判据来自 `core::game::adopt::scan`,本文件只做「有没有档案」这一步);
|
||||
//! 2. **它们从哪个目录跑**:[`base_mode`]——`versions/<id>/` 里有痕迹(`mods` / `saves` /
|
||||
//! `config` / `options.txt`)就按独立目录,否则按共用目录(游戏根)。
|
||||
//!
|
||||
//! 两条都**只读**:本文件不建目录、不写 `profiles.json`、不碰 `versions/` 一个字节。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use unlucky_core::game::isolation::IsolationMode;
|
||||
use unlucky_core::game::ProfileList;
|
||||
|
||||
use crate::dto::{InstanceRuntime, InstanceSummary};
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::{describe_version, global_mode, isolation_of};
|
||||
|
||||
/// `versions/<version_id>/` 里这些名字出现任何一个,就说明这个版本本来就被当成
|
||||
/// **独立目录**在用(它自己的 mods / 存档 / 配置都在里面)。判据只 `exists()`,不读内容。
|
||||
///
|
||||
/// 为什么是这四个:HMCL / PCL / 官方启动器的「版本隔离」都把运行目录放在 `versions/<id>/`,
|
||||
/// 于是跑过一次就会留下 `mods/` `saves/` `config/` `options.txt` 中的至少一个。
|
||||
const RUN_DIR_TRACES: [&str; 4] = ["mods", "saves", "config", "options.txt"];
|
||||
|
||||
/// 版本目录里有没有「它自己用过」的痕迹(只读)。
|
||||
fn has_own_run_dir(env: &HostEnv, version_id: &str) -> bool {
|
||||
let dir = env.game.version_dir(version_id);
|
||||
RUN_DIR_TRACES.iter().any(|name| dir.join(name).exists())
|
||||
}
|
||||
|
||||
/// 实例的**基底隔离档**(实例文件之下那一层)。
|
||||
///
|
||||
/// | 实例 | 基底 | 依据 |
|
||||
/// |---|---|---|
|
||||
/// | 有档案(向导 / 用户 / AI 建的) | 全局 `instances.mode` | D20 原口径,一字不变 |
|
||||
/// | **自动发现的**(id 带 [`crate::dto::DISCOVERED_ID_PREFIX`]) | `versions/<id>` 里有痕迹 ⇒ `isolated`;否则 ⇒ `shared` | 「别人怎么存我们就怎么读」 |
|
||||
///
|
||||
/// 为什么自动发现的实例不能一律 `isolated`:「版本隔离」下 `isolated` 的**运行目录就是**
|
||||
/// `versions/<id>/`。一个从没跑过、也没有 `mods`/`saves` 的外部版本若按 `isolated` 处理,
|
||||
/// 第一次启动就会在**别人的版本目录**里新长出 `mods/` `saves/` `config/`(D20 的
|
||||
/// `AdoptTarget` 缺省值正是为了躲开这件事而选 `InstanceDir`)。反过来,盘上已经有这些痕迹的
|
||||
/// 版本本来就是独立目录实例,按 `shared` 处理会让它突然读到 `.minecraft` 根下别人的存档 ——
|
||||
/// 两种错都真实存在,所以判据只能是**盘上的事实**。
|
||||
///
|
||||
/// 只读:这里不建目录、不写任何文件(写盘只发生在用户第一次改设置时,见 [`super::ensure_profile`])。
|
||||
pub(in crate::host) fn base_mode(env: &HostEnv, id: &str, version_id: &str) -> IsolationMode {
|
||||
if !crate::dto::is_discovered_instance(id) {
|
||||
return global_mode(env);
|
||||
}
|
||||
if has_own_run_dir(env, version_id) {
|
||||
IsolationMode::Isolated
|
||||
} else {
|
||||
IsolationMode::Shared
|
||||
}
|
||||
}
|
||||
|
||||
/// 盘上有版本、但没有档案绑定的那些版本 id(字典序,来自 core 的扫描)。
|
||||
///
|
||||
/// 扫描失败(`versions/` 读不出来 / 断链联接)时返回空:列表退回「只有档案」的旧行为。
|
||||
/// 这里**不**把错误往上抛——实例页打不开是比「少列几个外部实例」严重得多的故障。
|
||||
pub(super) fn external_versions(env: &HostEnv, profiles: &ProfileList) -> Vec<String> {
|
||||
let Ok(scan) = unlucky_core::game::adopt::scan(&env.game, &env.layout) else {
|
||||
return Vec::new();
|
||||
};
|
||||
scan.candidates
|
||||
.into_iter()
|
||||
.filter(|candidate| candidate.adoptable())
|
||||
.filter(|candidate| {
|
||||
!profiles
|
||||
.profiles
|
||||
.iter()
|
||||
.any(|profile| profile.version_id == candidate.version_id)
|
||||
})
|
||||
.map(|candidate| candidate.version_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 外部实例的视图 id(= 懒创建时写进 `profiles.json` 的档案 id)。
|
||||
///
|
||||
/// **不能直接拿版本 id 当实例 id**:`core::platform::layout::is_valid_instance_id` 只收
|
||||
/// `[A-Za-z0-9_-]{1,64}`(`config/instances/<id>.json` 与启动链的 `read_instance_config` 都按
|
||||
/// 这个白名单校验),而版本目录名可以是中文——用户实测的那个就是 `樱落怡然客户端`。
|
||||
/// 直接沿用版本 id 会让**启动**在写盘前就死在 `LaunchFailureStage::InstanceConfig`。
|
||||
///
|
||||
/// 前缀 + 稳定哈希 ⇒ 同一个版本每次拿到同一个 id(懒创建因此幂等),且是合法实例 id。
|
||||
///
|
||||
/// **M274(多根发现)**:两个根里都可能有 `1.20.4`,所以这里是**唯一**该把「根」并进身份的
|
||||
/// 地方——到时把 `root` 也喂进哈希(`external_instance_id(root, version_id)`),
|
||||
/// 列表就自然出两条,不必动 [`instances`] 的其余部分。
|
||||
pub(super) fn external_instance_id(version_id: &str) -> String {
|
||||
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
for byte in version_id.as_bytes() {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
}
|
||||
format!("{}{hash:016x}", crate::dto::DISCOVERED_ID_PREFIX)
|
||||
}
|
||||
|
||||
/// 一条外部版本 → 磁贴(显示名 = 版本目录名;**只读**,不落任何文件)。
|
||||
pub(super) fn external_summary(
|
||||
env: &HostEnv,
|
||||
version_id: &str,
|
||||
played: &BTreeMap<String, String>,
|
||||
) -> InstanceSummary {
|
||||
let id = external_instance_id(version_id);
|
||||
let (mc_version, loader) = describe_version(env, version_id);
|
||||
InstanceSummary {
|
||||
last_played: played.get(&id).cloned(),
|
||||
isolation: isolation_of(env, &id, version_id),
|
||||
launch: super::super::launch_args::view(env, &id, version_id),
|
||||
version_present: env.game.is_version_installed(version_id),
|
||||
name: version_id.to_string(),
|
||||
version_id: version_id.to_string(),
|
||||
mc_version,
|
||||
loader,
|
||||
runtime: InstanceRuntime::Idle,
|
||||
icon: None,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 视图 id → 版本 id 的反查(只认「可登记、且还没有档案绑定」的版本)。
|
||||
///
|
||||
/// 与 [`external_versions`] 同一份判据:合成的视图 id 只在那个集合里有意义,
|
||||
/// 于是「界面上出现过的外部实例」与「能懒创建的外部实例」永远是同一批。
|
||||
pub(super) fn external_version_of(
|
||||
env: &HostEnv,
|
||||
profiles: &ProfileList,
|
||||
id: &str,
|
||||
) -> Option<String> {
|
||||
external_versions(env, profiles)
|
||||
.into_iter()
|
||||
.find(|version_id| external_instance_id(version_id) == id)
|
||||
}
|
||||
@@ -189,9 +189,10 @@ fn isolation_write_then_read_round_trips() {
|
||||
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles).expect("写清单");
|
||||
let id = profiles.profiles[0].id.as_str().to_string();
|
||||
|
||||
assert_eq!(isolation_of(&env, &id), "isolated");
|
||||
// 有档案的实例:基底档 = 全局档(`isolated`),与 M273 之前一字不差。
|
||||
assert_eq!(isolation_of(&env, &id, "1.20.1"), "isolated");
|
||||
set_isolation(&env, &id, "shared").expect("切档位");
|
||||
assert_eq!(isolation_of(&env, &id), "shared");
|
||||
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();
|
||||
}
|
||||
@@ -143,13 +143,17 @@ fn external_instance_writes_nothing_until_first_edit() {
|
||||
|
||||
let id = snapshot[0].id.clone();
|
||||
assert_eq!(
|
||||
isolation_of(&env, &id),
|
||||
"isolated",
|
||||
"没有实例文件时按全局档位显示"
|
||||
isolation_of(&env, &id, "A"),
|
||||
"shared",
|
||||
"`versions/A/` 里没有 mods / saves / config / options.txt ⇒ 基底档是 shared(按共用目录读)"
|
||||
);
|
||||
|
||||
set_isolation(&env, &id, "shared").expect("第一次改档位 = 懒创建档案");
|
||||
assert_eq!(isolation_of(&env, &id), "shared");
|
||||
set_isolation(&env, &id, "isolated").expect("第一次改档位 = 懒创建档案");
|
||||
assert_eq!(
|
||||
isolation_of(&env, &id, "A"),
|
||||
"isolated",
|
||||
"实例文件里写下的档位优先于基底档"
|
||||
);
|
||||
|
||||
let list = unlucky_core::game::install::instance::read_profiles(&env.layout).expect("读档案");
|
||||
assert_eq!(list.profiles.len(), 1, "懒创建只该写一条");
|
||||
@@ -163,7 +167,7 @@ fn external_instance_writes_nothing_until_first_edit() {
|
||||
assert_eq!(created.name, "A");
|
||||
|
||||
// 幂等:再改一次不会长出第二条档案。
|
||||
set_isolation(&env, &id, "isolated").expect("第二次改档位");
|
||||
set_isolation(&env, &id, "shared").expect("第二次改档位");
|
||||
let list = unlucky_core::game::install::instance::read_profiles(&env.layout).expect("读档案");
|
||||
assert_eq!(list.profiles.len(), 1, "重复改档位不该再写第二条档案");
|
||||
|
||||
@@ -232,3 +236,69 @@ fn the_chinese_version_folder_from_the_field_report_shows_up() {
|
||||
tile.id
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- 运行目录判据(M273 r1 · P2-1)
|
||||
//
|
||||
// **真值表**(自动发现的实例):
|
||||
//
|
||||
// | `versions/<id>/` 里有 | 隔离档 | gameDir | 为什么 |
|
||||
// |---|---|---|---|
|
||||
// | `mods` / `saves` / `config` / `options.txt` 任一 | `isolated` | `versions/<id>` | 它本来就是「版本隔离」形态,别人的数据就放在里面 |
|
||||
// | 一个都没有 | `shared` | 游戏根(`.minecraft`) | 从没用过隔离目录,写进 `versions/<id>` 等于往别人的版本目录里拉屎 |
|
||||
//
|
||||
// 两条用例都顺带断言**派生前后盘上零变化**:判据是只读的,写盘只在用户第一次改设置时发生。
|
||||
|
||||
/// `versions/A/` 里只有清单 ⇒ 按**共用目录**跑(gameDir = 游戏根)。
|
||||
#[test]
|
||||
fn external_instance_without_traces_runs_from_the_game_root() {
|
||||
let root = temp::Root::new("ull-host-game-union-shared");
|
||||
let env = HostEnv::at_root(&root.0);
|
||||
write_version(&env, "A");
|
||||
|
||||
let before = temp::entries_under(&root.0);
|
||||
let snapshot = instances(&env);
|
||||
assert_eq!(snapshot.len(), 1);
|
||||
assert_eq!(
|
||||
snapshot[0].isolation, "shared",
|
||||
"没有 mods / saves / config / options.txt ⇒ 共用目录"
|
||||
);
|
||||
let dir = super::super::saves::run_dir(&env, &snapshot[0]).expect("派生运行目录");
|
||||
assert_eq!(
|
||||
dir,
|
||||
env.game.root(),
|
||||
"共用目录的运行目录应当就是 .minecraft 根"
|
||||
);
|
||||
assert_eq!(
|
||||
before,
|
||||
temp::entries_under(&root.0),
|
||||
"判定 + 派生运行目录不许在盘上多出任何东西"
|
||||
);
|
||||
}
|
||||
|
||||
/// `versions/B/mods/` 存在 ⇒ 按**独立目录**跑(gameDir = `versions/B`)。
|
||||
#[test]
|
||||
fn external_instance_with_mods_runs_in_its_version_folder() {
|
||||
let root = temp::Root::new("ull-host-game-union-isolated");
|
||||
let env = HostEnv::at_root(&root.0);
|
||||
write_version(&env, "B");
|
||||
std::fs::create_dir_all(env.game.version_dir("B").join("mods")).expect("mods 目录");
|
||||
|
||||
let before = temp::entries_under(&root.0);
|
||||
let snapshot = instances(&env);
|
||||
assert_eq!(snapshot.len(), 1);
|
||||
assert_eq!(
|
||||
snapshot[0].isolation, "isolated",
|
||||
"`versions/B/mods/` 是「这个版本自带数据」的痕迹 ⇒ 独立目录"
|
||||
);
|
||||
let dir = super::super::saves::run_dir(&env, &snapshot[0]).expect("派生运行目录");
|
||||
assert_eq!(
|
||||
dir,
|
||||
env.game.version_dir("B"),
|
||||
"独立目录的运行目录应当是它自己的版本文件夹"
|
||||
);
|
||||
assert_eq!(
|
||||
before,
|
||||
temp::entries_under(&root.0),
|
||||
"判定 + 派生运行目录不许在盘上多出任何东西"
|
||||
);
|
||||
}
|
||||
@@ -19,13 +19,16 @@ use super::job::{spawn, HostTask, JobSender};
|
||||
pub const DEFAULT_JDK_MAJOR: u32 = 17;
|
||||
|
||||
/// 该装哪个大版本:取第一个实例版本的要求,没有实例就给 17。
|
||||
///
|
||||
/// 「第一个实例」读的是**并集快照**([`super::game::instances`]),不是 `profiles.json`:
|
||||
/// M273 之后只有自动发现实例的用户(别的启动器装好了版本、ULL 一个档案都没有)完全合法,
|
||||
/// 按旧写法他们会拿到默认 JDK17 而不是那个版本真正要求的 JDK,而 `profiles.json` 里
|
||||
/// 恰好什么都没有。有档案时顺序与旧写法一致(档案在前),所以既有行为不变。
|
||||
pub fn target_major(env: &HostEnv) -> u32 {
|
||||
let profiles = super::game::profiles(env);
|
||||
profiles
|
||||
.profiles
|
||||
super::game::instances(env)
|
||||
.first()
|
||||
.map(|profile| {
|
||||
java::requirement_for_version(&env.game, &profile.version_id)
|
||||
.map(|instance| {
|
||||
java::requirement_for_version(&env.game, &instance.version_id)
|
||||
.minimum
|
||||
.max(8)
|
||||
})
|
||||
|
||||
@@ -193,13 +193,16 @@ pub fn build_request(
|
||||
Ok((request, warnings))
|
||||
}
|
||||
|
||||
/// 派生运行目录(D20 覆盖链:实例 `isolation` > 全局 `instances.mode`)。
|
||||
/// 派生运行目录(D20 覆盖链:实例 `isolation` > **基底档**,见 [`super::game::base_mode`])。
|
||||
///
|
||||
/// 基底档对自动发现的实例是「盘上有没有痕迹」,对其它实例就是全局 `instances.mode`——
|
||||
/// 于是这里不需要知道实例是从哪来的,只有一处判据。
|
||||
fn run_dir(env: &HostEnv, target: &LaunchTarget) -> Result<PathBuf, LaunchFailureView> {
|
||||
let global = super::game::global_mode(env);
|
||||
let base = super::game::base_mode(env, &target.id, &target.version_id);
|
||||
let config = read_instance_config(&env.layout, &target.id).map_err(|error| {
|
||||
LaunchFailureView::new(LaunchFailureStage::InstanceConfig, error.to_string())
|
||||
})?;
|
||||
let derivation = derive_game_dir(&env.game, &target.version_id, global, config.as_ref(), None)
|
||||
let derivation = derive_game_dir(&env.game, &target.version_id, base, config.as_ref(), None)
|
||||
.map_err(|error| {
|
||||
LaunchFailureView::new(LaunchFailureStage::Isolation, error.to_string())
|
||||
})?;
|
||||
|
||||
@@ -184,19 +184,13 @@ fn toggle(env: &HostEnv, id: &str, file: &str, enabled: bool, running: bool) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// 实例的运行目录(D20 覆盖链,与启动同一份派生;**不写盘**)。
|
||||
/// 实例的运行目录(D20 覆盖链 + 自动发现的基底档,与启动同一份派生;**不写盘**)。
|
||||
fn run_dir(env: &HostEnv, instance: &InstanceSummary) -> Result<PathBuf, String> {
|
||||
let global = super::game::global_mode(env);
|
||||
let base = super::game::base_mode(env, &instance.id, &instance.version_id);
|
||||
let config = read_instance_config(&env.layout, &instance.id)
|
||||
.map_err(|error| format!("读实例设置失败:{error}"))?;
|
||||
let derivation = derive_game_dir(
|
||||
&env.game,
|
||||
&instance.version_id,
|
||||
global,
|
||||
config.as_ref(),
|
||||
None,
|
||||
)
|
||||
.map_err(|error| format!("派生运行目录失败:{error}"))?;
|
||||
let derivation = derive_game_dir(&env.game, &instance.version_id, base, config.as_ref(), None)
|
||||
.map_err(|error| format!("派生运行目录失败:{error}"))?;
|
||||
Ok(derivation.game_dir)
|
||||
}
|
||||
|
||||
|
||||
@@ -240,19 +240,13 @@ pub(super) fn target(
|
||||
Ok((instance.clone(), dir))
|
||||
}
|
||||
|
||||
/// 实例的运行目录(D20 覆盖链,与启动同一份派生;**不写盘**)。
|
||||
/// 实例的运行目录(D20 覆盖链 + 自动发现的基底档,与启动同一份派生;**不写盘**)。
|
||||
pub(super) fn run_dir(env: &HostEnv, instance: &InstanceSummary) -> Result<PathBuf, String> {
|
||||
let global = super::game::global_mode(env);
|
||||
let base = super::game::base_mode(env, &instance.id, &instance.version_id);
|
||||
let config = read_instance_config(&env.layout, &instance.id)
|
||||
.map_err(|error| format!("读实例设置失败:{error}"))?;
|
||||
let derivation = derive_game_dir(
|
||||
&env.game,
|
||||
&instance.version_id,
|
||||
global,
|
||||
config.as_ref(),
|
||||
None,
|
||||
)
|
||||
.map_err(|error| format!("派生运行目录失败:{error}"))?;
|
||||
let derivation = derive_game_dir(&env.game, &instance.version_id, base, config.as_ref(), None)
|
||||
.map_err(|error| format!("派生运行目录失败:{error}"))?;
|
||||
Ok(derivation.game_dir)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user