用户实跑反馈两条,本轮各自定位到根因并修掉:
1. 「下载页没有前置加载器选项」= 功能未做:`Message::InstallVersion` 直接派
`AppCommand::InstallVersion`(= 只装原版),core 早在 M32/M35/M45 就有四家加载器的
安装管线。现在选中版本之后面板先给「安装选项」区(原版 / Forge / NeoForge / Fabric /
Quilt + 该加载器的官方版本列表 + 实例名),点「安装」才派
`AppCommand::CreateInstance`。
- 加载器版本列表复用 core 的 `LoaderSource::loader_versions`,经新增的
`AppCommand::ListLoaderVersions` / `CoreEvent::LoaderVersions` 回流;
缓存键是 `(加载器, 原版版本)`,切版本不会串列表;取不到给具名原因并**禁掉确认**。
- 不兼容组合用静态规则在**选中那一刻**说清(NeoForge 需 1.20.1+、Fabric/Quilt 需
1.14+、远古版四家都不支持),不让用户「选完才失败」,也不为不可能的组合作无谓联网。
- 选项草稿(选中 + 加载器 + 加载器版本 + 实例名)住在 `App`,`refresh_view` 带进页面
模型(与分类筛选同一做法;页面自己存不住跨事件的东西)。
2. 「点取消没反应」= 真 bug,两跳各断一半:
- 第一跳(UI 侧):取消确实生效了,但**还在跑的后台线程继续发进度**,`InstallProgress(None)`
刚把面板收回,下一条「下载中」立刻又点亮它 —— 用户看到的就是「点了没反应」。
修法:`CoreHost` 加安装代次 `install_epoch`(开工与取消都 +1),`HostTask::Install` /
`InstallDone` 带代次回来,代次不符一律丢。
- 第二跳(core 侧):`execute` 没有任何取消入参,取消只停在段边界,传输根本没停。
新增 `execute_with_cancel(…, cancel: Option<&AtomicBool>)`(既有 `execute` 原样包装,
CLI 一字未改):取消后不再起新文件,在飞的那个与「取消 future」`select` 赛跑,取消赢就
drop 掉下载 future(响应流关闭、`.ullpart` 由 `PartFile::Drop` 清掉),并且不写版本清单,
返回新的 `InstallErrorKind::Cancelled`。
- 另外:取消后**绝不登记实例**(文件段跑完但用户已取消的那一路也拦住);取消文案把
「哪些留着、哪些一定没发生」说清(D21)。
实测(真网络装置 `host/cancel_tests.rs`,`#[ignore]` 守着,跑法见文件头):
取消前 files_done=2 / `.ullpart` 613 字节;取消后 20 s 进度事件 **0 条**、`.ullpart` **0 字节**
(-613)、版本清单未落盘、实例目录不存在。取消前是 +201466 字节(同一个装置量的)。
文案全部进 i18n(中英,9 条新 key + 金表同步)。
121 lines
5.1 KiB
Rust
121 lines
5.1 KiB
Rust
//! 加载器版本清单(下载页「安装选项」的版本下拉,M105)。
|
|
//!
|
|
//! ## 为什么要有这一支
|
|
//!
|
|
//! core 早就有现成能力:[`unlucky_core::addon::loader::LoaderSource::loader_versions`] 按
|
|
//! `(加载器, 原版版本)` 给出**官方那份**可用版本表(Fabric/Quilt 走 fabric meta 的 loader 列表,
|
|
//! Forge/NeoForge 走 maven 的 `maven-metadata.xml` 再按 `matches_mc` 过滤)。
|
|
//! 缺的从来不是能力,是 presenter 到 UI 的那一跳——于是用户在下载页只能装原版,
|
|
//! 「自己有的能力不接出来」是 M105 要收的口子。
|
|
//!
|
|
//! ## 一次查询只回答一个问题
|
|
//!
|
|
//! 事件里带 `(kind, mc_version)` 原样回传,`state.rs` 拿它当缓存键:**切原版版本 = 换键**,
|
|
//! 于是 A 版本的 Fabric 列表不可能挂到 B 版本下面(这条是 M105 复审明确要求的)。
|
|
//! 取不到(网络/该组合官方没有版本)走 `Err(人读原因)`,页面据此**禁掉确认**并给具名错误,
|
|
//! 绝不静默显示成空列表。
|
|
|
|
use unlucky_core::addon::loader::{LoaderKind as CoreLoaderKind, LoaderSource};
|
|
use unlucky_core::download::blocking::block_on;
|
|
|
|
use crate::dto::LoaderKind;
|
|
|
|
use super::env::HostEnv;
|
|
use super::job::{spawn, HostTask, JobSender};
|
|
|
|
/// 拉一份加载器版本列表(网络在后台线程里,UI 不卡)。
|
|
pub fn spawn_list(env: HostEnv, tx: JobSender, kind: LoaderKind, mc_version: String) {
|
|
spawn("loader-versions", move || {
|
|
let result = fetch(&env, kind, &mc_version);
|
|
let _ = tx.send(HostTask::LoaderVersions {
|
|
kind,
|
|
mc_version,
|
|
result,
|
|
});
|
|
});
|
|
}
|
|
|
|
/// 同步拉取(后台线程里跑):DTO 的加载器词表 → core 的加载器词表 → 官方清单。
|
|
///
|
|
/// 原版(`Vanilla`)没有「加载器版本」这回事,直接给空列表而不是报错:调用方(页面)
|
|
/// 在选了原版时根本不画这个下拉,这里是防守。
|
|
pub fn fetch(env: &HostEnv, kind: LoaderKind, mc_version: &str) -> Result<Vec<String>, String> {
|
|
if kind == LoaderKind::Vanilla {
|
|
return Ok(Vec::new());
|
|
}
|
|
let Some(kind) = CoreLoaderKind::parse(kind.as_str()) else {
|
|
return Err(format!("未知加载器 `{}`", kind.as_str()));
|
|
};
|
|
if mc_version.trim().is_empty() {
|
|
return Err("没有选择原版版本".to_string());
|
|
}
|
|
let engine = super::engine::engine(env);
|
|
let meta_cache = env.layout.meta_cache();
|
|
block_on(async {
|
|
LoaderSource::new(&engine, &meta_cache)
|
|
.loader_versions(kind, mc_version.trim())
|
|
.await
|
|
})
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use crate::dto::{AppEvent, CoreEvent};
|
|
use crate::state::AppState;
|
|
|
|
/// **缓存键必须是 `(加载器, 原版版本)`**(M105 的硬要求,复审点名的那条)。
|
|
///
|
|
/// 现场:在 1.20.1 上查过 Fabric 的列表,接着点 1.21.9;键里若没有原版版本,1.21.9 会直接
|
|
/// 显示 1.20.1 那份列表 —— 用户选了个「1.21.9 上不存在」的加载器版本,直到点安装才失败。
|
|
#[test]
|
|
fn cache_is_keyed_per_loader_and_minecraft_version() {
|
|
let mut state = AppState::new();
|
|
state.apply(AppEvent::Core(CoreEvent::LoaderVersions {
|
|
kind: LoaderKind::Fabric,
|
|
mc_version: "1.20.1".to_string(),
|
|
result: Ok(vec!["0.16.5".to_string()]),
|
|
}));
|
|
assert!(state
|
|
.loader_versions_for(LoaderKind::Fabric, "1.20.1")
|
|
.is_some());
|
|
assert!(state
|
|
.loader_versions_for(LoaderKind::Fabric, "1.21.9")
|
|
.is_none());
|
|
assert!(state
|
|
.loader_versions_for(LoaderKind::Quilt, "1.20.1")
|
|
.is_none());
|
|
|
|
// 失败也是「查过了」:页面据此显示具名原因并禁掉确认,不再无限重试
|
|
state.apply(AppEvent::Core(CoreEvent::LoaderVersions {
|
|
kind: LoaderKind::Quilt,
|
|
mc_version: "1.20.1".to_string(),
|
|
result: Err("network down".to_string()),
|
|
}));
|
|
assert!(state
|
|
.loader_versions_for(LoaderKind::Quilt, "1.20.1")
|
|
.is_some());
|
|
}
|
|
|
|
/// 原版没有加载器版本:给空表而不是报错(页面不画这个下拉,防守而已)。
|
|
#[test]
|
|
fn vanilla_has_no_loader_versions() {
|
|
let dir = std::env::temp_dir().join(format!("ull-loader-versions-{}", std::process::id()));
|
|
let env = HostEnv::at_root(&dir);
|
|
assert_eq!(fetch(&env, LoaderKind::Vanilla, "1.20.1"), Ok(Vec::new()));
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
|
|
/// 空版本号在联网前就被拒(不做无意义的请求,也给出人读原因)。
|
|
#[test]
|
|
fn empty_mc_version_is_rejected_before_network() {
|
|
let dir = std::env::temp_dir().join(format!("ull-loader-empty-{}", std::process::id()));
|
|
let env = HostEnv::at_root(&dir);
|
|
let err = fetch(&env, LoaderKind::Fabric, " ").unwrap_err();
|
|
assert!(err.contains("没有选择原版版本"), "{err}");
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|
|
}
|