复审三条(p2-3items / fix-then-merge,其中两条按 p1 对待):
**P2-1 取消后仍为每个剩余文件各发一次请求(p1)**
`select` 先轮询左支 ⇒ `install_one` 第一次 poll 就把 `engine.download(…)` 派出去了;
只把标志检查写在右支,等于取消后「每一个剩余 item 都创建并至少 poll 一次」
(一次整文件 sha1 + 一次请求),盘上已就绪的还会第一 poll 就 `Ready` 并回调 `sink`。
修:`async move` 第一句就 `if is_cancelled(cancel) { return None; }`,这些 item 连
`install_one` 都不进。守门用例 `cancelling_mid_run_stops_creating_remaining_files`
(三文件全在盘上、并发 1、sink 首次回调即置标志 ⇒ 数 sink 回调次数):
修后 1,删掉那两行变异后 **3**(见报告,红/绿均实跑)。
模块文档同步改成与实现一致,并把复审点名的固有边界写进注释:取消瞬间最多有
`concurrency`(GUI 传 16)个 future 已经过了那道门,其中「盘上已就绪」的仍会跑完自己的
回调(靠 presenter 的安装代次丢掉这些进度事件),真在下载的会在同一次 poll 被取消。
**P2-3 真网络装置的断言比声称的弱,且有一条探的是恒不成立的路径(p1)**
① `.ullpart` 原来只断言「5 s 后不再增长」⇒「留下一个不增长的非 0 中间文件」会绿;
改成 `assert_eq!(parts_after, 0)`。
② 实例登记原来探 `<game>/instances`(根本不存在 ⇒ 恒 false,什么也没证明);
改成读 `LauncherLayout::profiles_config()` 的 `read_profiles()` 并断言 0 条。
③ 模块文档里那条探针的路径一并订正。
变异自证:留一个 20 字节 `.ullpart` ⇒ 红(`left: 20 / right: 0`);
走真 `instance::register()` 造一条 profile ⇒ 红(`profiles.json 里 1 条实例`)。
**P2-2 取消文案仍是硬编码中文,与新选项区同屏一中一英**
`presenter` 没有 i18n 依赖,所以不是搬常量能解决的。改成发稳定信号:
新增 `CoreEvent::InstallCancelled`(不带文本),`AppState::install_cancelled` 只计数;
app 层在 `refresh_view` 里比对计数、按 `Key::DownloadCancelled` 现求文本弹一次 Toast
(`pages::download::cancelled_toast`)。`dispatch.rs` 的中文 Toast 删掉。
语言入口收成 `app::view::ui_lang()` 一处(现恒为英文;M106 接上语言只改这里,
M106 合并前中文译文在产品路径不可达)。守门:`cancelling_announces_once_in_the_page_language`
(只弹一条、文案来自目录、不含中文、重复 refresh 不重复弹)。
门禁(真实退出码):fmt=0、clippy(--workspace --all-targets -j 2 -D warnings)=0、
`cargo test -p unlucky-app-iced --lib`=0(440 passed)、`cargo test -p unlucky-core`=0、
`cargo test --workspace --exclude unlucky-app -j 2`=0(57 个 target 全 ok,0 failed)。
181 lines
9.2 KiB
Rust
181 lines
9.2 KiB
Rust
//! `CoreHost::dispatch` 的指令实现(从 `host/mod.rs` 拆出,守 400 行上限)。
|
||
//!
|
||
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.env` / `self.tx` 等。
|
||
|
||
use crate::dto::{AppCommand, Backend, CoreEvent, CreateRequest, LoaderKind};
|
||
use crate::toast::ToastKind;
|
||
|
||
use super::accounts::{self, LoginJob};
|
||
use super::{game, install, java, launch, loader_versions, logs, settings, versions, CoreHost};
|
||
|
||
impl CoreHost {
|
||
/// 下发一条用户意图(`Backend::dispatch` 的实现体)。
|
||
pub(super) fn dispatch_command(&mut self, command: AppCommand) {
|
||
self.dispatch_inner(command);
|
||
}
|
||
}
|
||
impl CoreHost {
|
||
/// 指令分发(见 `dispatch_command`)。
|
||
fn dispatch_inner(&mut self, command: AppCommand) {
|
||
match command {
|
||
AppCommand::RefreshAll => self.refresh_all(),
|
||
AppCommand::LaunchInstance(id) => self.start_launch(&id),
|
||
AppCommand::StopInstance(id) => match self.running_pid(&id) {
|
||
Some(pid) => match launch::stop(pid) {
|
||
Ok(()) => self.toast(ToastKind::Info, "已请求停止游戏进程"),
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
None => self.toast(ToastKind::Warn, "这个实例当前没有在跑"),
|
||
},
|
||
AppCommand::ListVersions(_) => versions::spawn_list(self.env.clone(), self.tx.clone()),
|
||
AppCommand::ListLoaderVersions { kind, mc_version } => {
|
||
loader_versions::spawn_list(self.env.clone(), self.tx.clone(), kind, mc_version);
|
||
}
|
||
AppCommand::InstallVersion(version) => {
|
||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||
let epoch = self.next_install_epoch();
|
||
install::spawn_install(
|
||
self.env.clone(),
|
||
self.tx.clone(),
|
||
CreateRequest {
|
||
name: version.clone(),
|
||
version,
|
||
loader: LoaderKind::Vanilla,
|
||
loader_version: String::new(),
|
||
},
|
||
cancel,
|
||
epoch,
|
||
);
|
||
}
|
||
AppCommand::CreateInstance(request) => {
|
||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||
let epoch = self.next_install_epoch();
|
||
install::spawn_install(self.env.clone(), self.tx.clone(), request, cancel, epoch);
|
||
}
|
||
AppCommand::CancelInstall => {
|
||
// 取消的三级语义(M105 实测后定稿,见 `install::CANCELLED`):
|
||
// ① **面板立刻收回**(不等后台,用户点了就有反应);
|
||
// ② **作废旧任务的代次**:后台线程是异步停的(取消延迟 = 一个数据块),
|
||
// 它带回来的进度事件一律因代次不符被 `handle` 丢掉——面板不会被重新点亮。
|
||
// 这是「点了没反应」的真根因:以前生效了又被旧事件盖回去;
|
||
// ③ 后台线程经 `install::run` 的取消检查停在段边界、**绝不登记实例**,
|
||
// core 的 `execute_with_cancel` 还会就地掐断在飞的那个文件。
|
||
//
|
||
// 为什么不在这里 join 后台线程:UI 线程绝不能为了「等它停」而阻塞
|
||
// (一个数据块的延迟在人眼下就是「立刻」,但足以让 UI 卡住一帧以上)。
|
||
if let Some(cancel) = &self.install_cancel {
|
||
cancel.store(true, std::sync::atomic::Ordering::SeqCst);
|
||
}
|
||
self.install_epoch = self.install_epoch.wrapping_add(1);
|
||
self.install = None;
|
||
self.send(CoreEvent::InstallProgress(None));
|
||
// 只发信号、不发文案(复审 P2-2):presenter 没有 i18n 依赖,
|
||
// 写死的中文会和页面语言打架。提示语由 app 层按 `Key` 现求。
|
||
self.send(CoreEvent::InstallCancelled);
|
||
}
|
||
AppCommand::SetIsolation { id, mode } => {
|
||
match game::set_isolation(&self.env, &id, &mode) {
|
||
Ok(()) => {
|
||
self.toast(ToastKind::Success, "隔离档位已更新");
|
||
self.refresh_instances();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
}
|
||
}
|
||
AppCommand::RefreshLogSessions => {
|
||
self.send(CoreEvent::LogSessionsChanged(logs::sessions(&self.env)));
|
||
}
|
||
AppCommand::OpenLogSession(id) => self.open_session(&id),
|
||
AppCommand::ImportCrashLog(path) => match logs::analyze_crash_file(&path) {
|
||
Ok((label, report)) => {
|
||
self.send(CoreEvent::DiagnosisReady(report));
|
||
self.toast(ToastKind::Info, label);
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Warn, reason),
|
||
},
|
||
AppCommand::AddOfflineAccount(name) => match accounts::add_offline(&self.env, &name) {
|
||
Ok(display) => {
|
||
self.toast(ToastKind::Success, format!("已添加离线账户 {display}"));
|
||
self.refresh_accounts();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::RemoveAccount(key) => match accounts::remove(&self.env, &key) {
|
||
Ok(display) => {
|
||
if self.active_account.as_deref() == Some(key.as_str()) {
|
||
self.active_account = None;
|
||
}
|
||
self.toast(ToastKind::Info, format!("已删除账户 {display}"));
|
||
self.refresh_accounts();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::SwitchAccount(id) => {
|
||
self.active_account = Some(id);
|
||
self.refresh_accounts();
|
||
self.toast(ToastKind::Info, "已切换启动账户");
|
||
}
|
||
AppCommand::StartDeviceLogin => {
|
||
self.login = Some(LoginJob::new());
|
||
let job = self.login.clone().unwrap_or_default();
|
||
self.send(CoreEvent::DeviceLoginChanged(crate::dto::DeviceLoginView {
|
||
phase: crate::dto::DeviceLoginPhase::Requesting,
|
||
message: "正在向微软申请设备码…".to_string(),
|
||
..crate::dto::DeviceLoginView::new()
|
||
}));
|
||
accounts::start_login(self.env.clone(), self.tx.clone(), job);
|
||
}
|
||
AppCommand::StartBrowserLogin => {
|
||
self.login = Some(LoginJob::new());
|
||
let job = self.login.clone().unwrap_or_default();
|
||
self.send(CoreEvent::DeviceLoginChanged(crate::dto::DeviceLoginView {
|
||
phase: crate::dto::DeviceLoginPhase::Requesting,
|
||
message: "正在准备浏览器登录(本地回调端口)…".to_string(),
|
||
..crate::dto::DeviceLoginView::new()
|
||
}));
|
||
accounts::start_browser_login(self.env.clone(), self.tx.clone(), job);
|
||
}
|
||
AppCommand::CancelDeviceLogin => {
|
||
if let Some(job) = &self.login {
|
||
job.cancel();
|
||
}
|
||
self.login = None;
|
||
self.send(CoreEvent::DeviceLoginChanged(
|
||
crate::dto::DeviceLoginView::new(),
|
||
));
|
||
}
|
||
AppCommand::InstallJava(vendor) => {
|
||
self.java_busy = true;
|
||
self.java_message = format!("正在安装 Java({vendor})…");
|
||
self.refresh_java();
|
||
java::spawn_install(
|
||
self.env.clone(),
|
||
self.tx.clone(),
|
||
unlucky_core::game::java::JdkVendor::parse(&vendor),
|
||
);
|
||
}
|
||
AppCommand::SetSetting { key, value } => match settings::set(&self.env, &key, &value) {
|
||
Ok(()) => {
|
||
self.send(CoreEvent::SettingsChanged(settings::groups(&self.env)));
|
||
self.java_chain.clear();
|
||
self.refresh_java();
|
||
}
|
||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||
},
|
||
AppCommand::DismissNotice(id) => {
|
||
self.notices.retain(|notice| notice.id != id);
|
||
self.send(CoreEvent::NoticesChanged(self.notices.clone()));
|
||
}
|
||
// AI 对话:agent 的 GUI host 未落地前用脚本回复(不接真实模型)
|
||
other @ (AppCommand::AskAgent(_) | AppCommand::StopAgent | AppCommand::SetYolo(_)) => {
|
||
self.chat.dispatch(other);
|
||
for event in self.chat.drain_events() {
|
||
self.events.push_back(event);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|