Files
Unlucky-Launcher/crates/app-iced/src/app/state/mods_shot.rs
T
tower-worker a0f61d8bb5 feat(mods): 本地 Mod 列表与启停(M224 MODTOGGLE)
三层一次做完:core 读写目录 → presenter DTO/host → 实例详情页 MODS 区。
r1 复审后 rebase 到 main 71144df(M223 已合入),并修掉四条文档疵。

core(crates/core/src/addon/mods/)
- suffix:`.disabled` / `.old` 的**唯一判据**(四态枚举 + 展示名 + 双向改名目标名),
  收敛原先两份同构实现——`export/scan.rs::is_disabled_path` 与 `import/plan/paths.rs::suffix_variant`
  改为委派/再导出,签名与行为不变。
- list:单根语义(入口是 derive_game_dir 派生出的运行目录)列 `<run>/mods/`,不按扩展名过滤,
  符号链接/目录/怪名字逐条登记原因;`mods/` 本身是符号链接时拒(不跟随)。
- toggle:启停 = 在 `mods/` 里改名。五条守卫(裸文件名 / 目录可用 / 普通文件 / 不覆盖 / 不做
  copy+delete 兜底)+ 幂等;`.disabled` 只插在 `.old` 之前。

presenter
- dto/mods:ModsCommand / ModListView / ModFailureView(不拼文案,只给结构 + OS 原文)。
- host/mods:后台线程列目录与启停,运行目录走与启动同一份 D20 派生、不写任何设置;
  游戏在跑时 host 侧直接挡。
- state/mods:快照整体替换、迟到结果丢弃、在飞标记;`AppState::mods`。

app-iced(实例详情页)
- MODS 区(`pages/instances/mods.rs`):名字 + 大小 + `.old` 徽标 + Switch;运行中/在飞只读。
- 进入详情页即后台列一次(不走 3 s 观察窗,避免打红 M192 的「空闲不烧帧」守卫)。
- MODS 卡排在启动参数**之前**(详情列是滚动区,排在后面 1180×1000 下永远看不见)。
- i18n:19 条中英文案(`catalog/instances.rs` + 金表)。

rebase 冲突解法(三个文件,全是"两侧同段新增",都保留):
- `i18n/catalog/instances.rs` + `i18n/catalog/golden.rs`:M223 的 Adopt* 段在前、M224 的
  InstancesMods* 段在后(= `Key::ALL` 顺序 = 金表顺序)。
- `presenter/src/state.rs`:`adopt` 与 `mods` 字段/初始化/apply_core 分支都留;`tiles()` 归
  `state/tiles.rs`(本分支搬的),`apply_agent`/`agent_rail_visible` 归 `state/agent.rs`
  (M223 搬的)——两处搬移不重叠,state.rs 里都不再出现。

r1 四条文档疵:
1. `pages/instances/mods.rs` 模块文档:挂载位置写反("启动参数卡之后"→实际之前),并补上为什么;
2. `state/mods.rs`:引用不存在的 `CoreEvent::ModsChanged/ModsFailed` → 改成 `CoreEvent::Mods`
   + 文字说明 Ok 落快照 / Err 落具名失败;
3. `app/state/mods_shot.rs`:文档说"目录串取真实派生结果"但代码留空 → 改成"故意留空";
4. `app/state/refresh.rs::note_mods_failure`:文档说"连点两次看见两次红字"与去重机制不符 →
   改成按 `(实例, 文件, 大类)` 三元组去重(防刷屏),换文件/换实例/换大类仍会再弹。

顺带(r1 已确认的 main 红):`pages/download/options/view.rs:88-89` 两条
`clippy::doc_lazy_continuation` 只缩进修掉,文字未动。
2026-11-10 21:09:37 +08:00

98 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 出图取证用的 MODS 夹具(`ULL_MODS_FIXTURE=1`)。
//!
//! # 它解决什么
//!
//! D30 的 MODS 区截图需要一屏**有内容的列表**。产品路径的列表来自后台线程(列 `mods/` → 快照),
//! 而截图通道在第 3 帧就出图(`CAPTURE_AT_FRAME`):那条后台往返来不及完成,截出来会是
//! 「正在读取 mods/…」。M157 的启动参数区、M222 的下载页、M223 的导入都是同一个问题,
//! 处理方式一致:**状态注入**,且注入点放在「快照 → 页面模型」之前,于是转换逻辑照样被执行。
//!
//! # 边界(不要把它读成产品能力)
//!
//! - 只在环境变量存在时生效;产品路径读不到它 ⇒ 一帧都不影响;
//! - 它注入的是**快照**([`ModListView`]),不是页面模型——`Mods::from_state` 的转换、
//! 计数、`.old` 徽标、在飞置灰走的都是产品代码;
//! - 真实链路由两条端到端用例钉住(`host::mods::tests` 在真盘上列目录 + 启停)。
/// 夹具开关是否打开(`ULL_MODS_FIXTURE` 非空且不为 `0`)。
#[must_use]
pub fn enabled() -> bool {
std::env::var("ULL_MODS_FIXTURE")
.map(|value| {
let value = value.trim();
!value.is_empty() && value != "0"
})
.unwrap_or(false)
}
/// 一份固定快照(实例 id 用调用方给的那个)。
///
/// `dir` 故意留空:夹具不碰路径(这一版页面也不画它),写一个假路径反而会被当成"真的派生结果"。
#[must_use]
pub fn view(instance: &str) -> unlucky_presenter::dto::ModListView {
use unlucky_presenter::dto::{ModEntryView, ModListView};
let entry = |file: &str, name: &str, enabled: bool, bytes: u64, old: bool| ModEntryView {
file: file.to_string(),
name: name.to_string(),
enabled,
bytes,
old,
};
// 夹具不碰路径:`dir` 留空(页面这一版不画它,它只是 DTO 的一部分)。
let dir = String::new();
ModListView {
instance: instance.to_string(),
dir,
exists: true,
entries: vec![
entry(
"sodium-fabric-0.5.11.jar",
"sodium-fabric-0.5.11.jar",
true,
1_048_576,
false,
),
entry(
"iris-fabric-1.8.5.jar",
"iris-fabric-1.8.5.jar",
true,
2_621_440,
false,
),
entry(
"jei-1.20.4-forge-17.3.0.49.jar.disabled",
"jei-1.20.4-forge-17.3.0.49.jar",
false,
1_363_148,
false,
),
entry(
"aether-1.20.1-1.0.0.jar.old",
"aether-1.20.1-1.0.0.jar",
true,
7_340_032,
true,
),
entry(
"create-1.20.1-0.5.1.f.jar.disabled",
"create-1.20.1-0.5.1.f.jar",
false,
4_194_304,
false,
),
entry(
"fabric-api-0.92.2+1.20.4.jar",
"fabric-api-0.92.2+1.20.4.jar",
true,
2_097_152,
false,
),
],
skipped: Vec::new(),
running: false,
known: true,
}
}