复审 P2-1:扫盘搬后台之后,`App::dispatch(RefreshAll)` 返回时快照还没到(`pump` 只抽队列、
不 `tick`),**首帧必然**在快照之前。改前那一帧画的是「计数 0 + 还没有实例 + 实心导入 CTA」
——一句假话,而且 CTA 能点。改后三态分开:
- 真源放 presenter:`dto::InstancePhase { Scanning, Stalled, Ready }`,`AppState::instances_phase`
在 `CoreEvent::InstancesChanged` 落地时置 `Ready`;**不从 `instances.is_empty()` 推**(那就是 bug 形态)。
- `Scanning` = 首个快照未到:详情列画「正在扫描实例…」占位(新 key `InstancesScanning`,golden 两语),
列头**不写计数**(藏起来,不写 0 也不写「—」),**没有**空态 CTA。
- `Stalled` = 有界兜底:`host::game::SCAN_STALL = 15 s`(= 仓库内最坏实测 1.35 s 的约 11 倍,
理由在常量文档里)。超时只**报**一次、不掐线程;迟到的结果照样落地(代次没变)。
那一屏给一枚「重新扫描」→ `InstanceMessage::RetryScan` → 新命令 `AppCommand::RefreshInstances`
(只重扫实例目录,不重拉版本清单)。那一支在 `selected_instance_id()` 守卫**之前**——冷启动没有选中项。
- `Ready` = 收到过快照(空列表也算):这时才轮到 M273 的两种空态(没选中 / 一个都没有)。
- 已就绪时**不发** `Scanning`:否则每次刷新列表都退回占位(把卡顿换成闪屏)。
判据(都能红,变异原文见报告):
- `pages/instances/tests/cold_boot.rs`:真控件树(未就绪 7 枚按钮 = 有实例那一屏,零实例 = 8 枚,
多的那枚就是 CTA;每行 2 条文字节点的差分数行数)+ 源码结构(`Scanning`/`Stalled` 两支不碰
`detail_empty` 与空态文案、超时那支接 `RetryScan`、三个 key 真的被读)。
- `app/update/instances/scan_phase.rs`:首帧 = `Scanning`、空快照 = `Ready`、`Stalled` 经重试回到
`Scanning`、重试只派 `RefreshInstances`。夹具 `ULL_INSTANCE_PHASE` 只给出图(与 `ULL_SAVES_FIXTURE` 同一条纪律)。
顺手收 N1~N4(全是措辞 / 观测口径,不动行为):
- N1 两处 docstring 不再声称「`admit` 写成 `>=` 会红」(实测等价),改成「去掉或恒真才红」;
- N2 收尾用例那条 Idle 断言的失败消息改准(它不区分新旧快照,区分靠条数/代次断言);
- N3 boot 双刷新必然丢 1 份 ⇒ 丢弃日志分成「此前没有快照落地(启动期,非交错)」与「已有落地(真交错)」,
`Scan::dropped` 的文档登记这条已知噪音;
- N4 app 用例 docstring 收窄成「一拍无事件不清表、不钉时钟」(它绕过 `App::dispatch`,抓不住那 5 行)。
拆文件守 400 行:`detail/scan.rs`(分派 + 占位)、`refresh_stall_tests.rs`(空根 / 超时)、
`app/update/instances/scan_phase.rs`(app 级三态用例);`i18n` 的 `PAGE_SOURCES` 同步加上新页面文件。
132 lines
5.4 KiB
Rust
132 lines
5.4 KiB
Rust
//! 实例页的**演示数据 → 视图模型**转换(M84 的出图基线靠这份数据)。
|
||
//!
|
||
//! 只被 [`crate::pages::demo_model`] 用:产品路径的转换在 `app/view.rs`(presenter 快照 → 模型)。
|
||
//! 两份数据源喂同一个渲染函数,这是 [`crate::pages::model`] 存在的理由。
|
||
|
||
use unlucky_i18n::{Catalog, Key, Lang};
|
||
use unlucky_presenter::dto::LaunchArgsView;
|
||
|
||
use super::super::model::{Detail, Instances, LaunchFacts, Meta, Row, ScanPhase};
|
||
use super::data::{DEMO, NOTE, SELECTED};
|
||
use super::state::LaunchForm;
|
||
|
||
/// 演示路径的目录:**恒英文**。
|
||
///
|
||
/// 为什么不是 `model.lang`:本函数只被 `pages::demo_model` 调用,而那条路(`pages_demo` harness /
|
||
/// 单测)走 `Model::new`,`lang` 恒为 `Lang::En`(harness 没有换语言的开关)。产品路径不读这里
|
||
/// ——它走 `app/view.rs` 的快照转换。等 harness 有 `--lang` 时把这一行换成参数即可。
|
||
fn catalog() -> Catalog {
|
||
Lang::En.catalog()
|
||
}
|
||
|
||
/// 元信息卡的标签(与 `app/view/instances.rs` 的 `detail_of` 同一批 key,只是这里恒英文)。
|
||
fn label(key: Key) -> &'static str {
|
||
catalog().text(key)
|
||
}
|
||
|
||
/// 把 M84 的演示条目 [`DEMO`] 转成视图模型(真页面走 `app/view.rs` 的 `AppState` 转换)。
|
||
pub(crate) fn demo() -> Instances {
|
||
let rows = DEMO
|
||
.iter()
|
||
.map(|entry| Row {
|
||
id: entry.name.to_string(),
|
||
name: entry.name.to_string(),
|
||
supporting: format!("{} · {}", entry.version, entry.loader),
|
||
status: None,
|
||
})
|
||
.collect();
|
||
|
||
let entry = &DEMO[SELECTED];
|
||
let detail = Detail {
|
||
id: entry.name.to_string(),
|
||
name: entry.name.to_string(),
|
||
subtitle: format!("{} · {}", entry.version, entry.loader),
|
||
meta: vec![
|
||
meta(label(Key::InstancesMetaVersion), entry.version),
|
||
meta(label(Key::InstancesMetaLoader), entry.loader),
|
||
meta(label(Key::InstancesMetaMemory), entry.memory),
|
||
meta(label(Key::InstancesMetaJava), entry.java),
|
||
meta(label(Key::InstancesMetaMods), entry.mods),
|
||
meta(label(Key::InstancesMetaLastPlayed), entry.last_played),
|
||
],
|
||
note: Some(NOTE.to_string()),
|
||
status: None,
|
||
can_launch: true,
|
||
can_stop: false,
|
||
launch: launch_facts(),
|
||
};
|
||
|
||
Instances {
|
||
rows,
|
||
detail: Some(detail),
|
||
// 演示数据就是「已就绪」的那一屏(M302 的三态里只有它适合出图:
|
||
// 演示目录里没有后台线程,画「正在扫描」会是假的)
|
||
scan: ScanPhase::Ready,
|
||
}
|
||
}
|
||
|
||
fn meta(label: &str, value: &str) -> Meta {
|
||
Meta {
|
||
label: label.to_string(),
|
||
value: value.to_string(),
|
||
}
|
||
}
|
||
|
||
/// 启动参数区的演示事实(M157 的 D30 出图夹具)。
|
||
///
|
||
/// `ULL_LAUNCH_ARGS_FIXTURE=gated` 出「版本不够、三态灰化」那一屏;缺省(`open`)是
|
||
/// 「26.2 实例 + Vulkan 可选」的正常态。**这是出图夹具,不是产品逻辑**:产品路径的门槛
|
||
/// 来自 presenter 快照(core 的 `graphics_backend_allowed`),与这个环境变量无关。
|
||
pub(crate) fn launch_facts() -> LaunchFacts {
|
||
let gated = gated_fixture();
|
||
LaunchFacts {
|
||
allowed: !gated,
|
||
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
|
||
min_version: "26.2-snapshot-2".to_string(),
|
||
memory_effective_mb: if gated { 2048 } else { 4096 },
|
||
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
|
||
backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
|
||
options: ["default", "opengl", "vulkan"]
|
||
.iter()
|
||
.map(|id| (*id).to_string())
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// 演示用的表单初值(页面状态的所有者是 `App`;harness 没有 `App`,自己持有一份)。
|
||
///
|
||
/// 返回**拥有**的表单而不是 `&'static`:出图/单测各自持有一份即可(一帧借用一次),
|
||
/// 而 `static` 会让所有出图路径共享同一份可编辑状态(两个参数的控件现在都是单行
|
||
/// `field::text_field` + `String`,不再是早先的多行编辑器)。
|
||
pub(crate) fn demo_form() -> LaunchForm {
|
||
LaunchForm::from_view(Some(DEMO[SELECTED].name), &demo_view())
|
||
}
|
||
|
||
fn demo_view() -> LaunchArgsView {
|
||
let gated = gated_fixture();
|
||
LaunchArgsView {
|
||
instance_id: DEMO[SELECTED].name.to_string(),
|
||
jvm_args: "-XX:+UseG1GC".to_string(),
|
||
game_args: "--fullscreen".to_string(),
|
||
memory_mb: (!gated).then_some(4096),
|
||
memory_effective_mb: if gated { 2048 } else { 4096 },
|
||
java_path: String::new(),
|
||
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
|
||
graphics_backend: if gated { "default" } else { "vulkan" }.to_string(),
|
||
graphics_backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
|
||
graphics_backend_allowed: !gated,
|
||
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
|
||
graphics_backend_min_version: "26.2-snapshot-2".to_string(),
|
||
graphics_backend_options: ["default", "opengl", "vulkan"]
|
||
.iter()
|
||
.map(|id| (*id).to_string())
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
fn gated_fixture() -> bool {
|
||
std::env::var("ULL_LAUNCH_ARGS_FIXTURE")
|
||
.map(|value| value.trim().eq_ignore_ascii_case("gated"))
|
||
.unwrap_or(false)
|
||
}
|