fix(launch): M298 收尾失败不再静默、Toast 接上渲染点

三条提示通道各漏一半,用户才会「启动失败但找不到崩溃日志」:

1) 收尾失败静默不弹:finish() 报错那条出口把 LaunchFinished.session 发成真实
   会话,而它登记的现场走 before_start 写死空串 ⇒ dialog.rs 的并发守卫
   (!finished.session.is_empty() && report.session != finished.session)成立后
   return。改成**会话归属单一来源**:before_start 新增可选 SessionAttribution,
   presenter 的 publish_before_start 把它写进报告后**返回同一个值**给 DTO;
   两处不可能再各写一遍。守卫原样保留(A 场的现场不许解释 B 场,有用例钉住)。

2) Toast 只 push 不渲染:新增 app/view/toast.rs 覆盖层(MD3E 胶囊 =
   ShapeScale::Full,底部居中,InverseSurface 底),接在页面树之上、模态之下;
   过期仍走 clock.rs 的 expire;进/出过渡复用模态那一对腿(motion::modal_spec /
   modal_scale / scaled),时间轴在 update 侧由 state/toast.rs 采样,关掉动效时
   恒为终值(出图可复现)。两个拷贝路径补上 view_dirty(推了不置脏要等下一次事件)。

3) 三个贴线文件先拆再改(纯搬移 + pub use 保路径,机器核对:搬出的 14 个函数体
   逐行相同):crash_report.rs 394→318(+context.rs / slot.rs)、
   host/launch.rs 399→233(+build.rs)、app/dialog.rs 390→178(+model.rs)。

证据:DevDocs/artifacts/m298/toast-capsule.png(--screenshot + ULL_TOAST_FIXTURE,
图上可见「游戏崩溃 · 12.0 秒」胶囊)。
This commit is contained in:
AskaEth committed 2026-11-13 13:18:11 +08:00
1 parent fc8f5c6794
commit 97768b7e3f
19 files changed
+1699 -553

No files matched your search

@@ -0,0 +1,48 @@
//! 出图取证用的 Toast 夹具(`ULL_TOAST_FIXTURE`;产品路径零影响)。
//!
//! # 它解决什么
//!
//! M298 的交付图要证明「提示真的画在窗口底部」,而产品路径的 toast 由事件推出来
//! (安装取消 / 启动收尾 / 复制成功),截图通道在第 3 帧就出图:等不到那条事件。
//! 处理方式与 `mods_shot` / `saves_shot` 一致——**状态注入**,注在「快照 → 页面模型」之前,
//! 于是覆盖层、胶囊几何、i18n 文案走的都是产品代码。
//!
//! # 边界(不要把它读成产品能力)
//!
//! - 只在环境变量存在时生效(`ULL_TOAST_FIXTURE` 非空且不为 `0`);
//! - 推的是**一条真实形状的提示**:文案由产品函数 `launch_toast` 按**生效语言**现求,
//! 不是夹具里写死的一句中文;
//! - 剩余寿命走 `ToastCenter` 的默认 TTL(4 s),过期仍由 `app/update/clock.rs` 的 `expire` 管。
use unlucky_presenter::dto::{LaunchFinished, LogOutcome};
use unlucky_presenter::toast::ToastKind;
use super::App;
/// 夹具开关是否打开(`ULL_TOAST_FIXTURE` 非空且不为 `0`)。
fn enabled() -> bool {
std::env::var("ULL_TOAST_FIXTURE")
.map(|value| {
let value = value.trim();
!value.is_empty() && value != "0"
})
.unwrap_or(false)
}
/// 推一条「游戏崩溃」的收尾提示(就是用户在真机上看到的那一句)。
pub(super) fn seed(app: &mut App) {
if !enabled() {
return;
}
// 形状取自真实收尾事件:崩溃 + 退出码 1 + 跑了 12 秒。
let finished = LaunchFinished {
id: "survival".to_string(),
session: String::new(),
outcome: LogOutcome::Crashed,
exit_code: Some(1),
uptime_ms: 12_000,
failure: None,
};
let text = crate::app::view::instances::launch_toast(&finished, app.lang());
app.state.toasts.push(ToastKind::Error, text);
}