AskaEth
|
0156adb44d
|
fix(presenter): M306 r1 —— 「编译器一声不吭」是错的:改准两处文档 + 补一条源码级守卫
r1 复审实测:只删 `live.rs::discovered` 那三行 ⇒ `unused_imports` 点名
`legacy_external_instance_id`,`clippy -- -D warnings` 直接红(EXIT=101)。
`#![allow(dead_code)]` 只盖「函数变死码」,盖不住「use 行变空」——那是 `unused` 组另一条 lint。
- `live.rs` / `game/tests.rs` 两处文档改成准确版本:删掉那三行 ⇒ `cargo test` 仍全绿,但
`unused_imports` 会让 CI 红;而**这道保险可绕过**(连 import 一起删 / 加 `#[allow(unused_imports)]`
⇒ 编译期一条 warning 都没有),所以行为用例之外再加一道源码网
- `game/tests.rs` 新增 `the_legacy_pass_through_is_still_in_the_source`:`include_str!` 扫
`live.rs` 的**代码**(每行剥掉 `//` 之后的部分——注释里残留的那条 `if` 原文不算数),断言
放行分支与 import 原样还在、且没有 `unused_imports` 豁免
- 变异实测:把 import 里的名字一起删掉(编译器真不吭声,`grep -c '^warning'` = 0)⇒ 新守卫红
「分支在 = false,import 在 = false」,行为用例同时红;两条网互补
|
2026-11-13 22:56:44 +08:00 |
|
AskaEth
|
2c5fa53cc9
|
test(presenter): M306 —— 给「老档案放行」挂网 + 收三处顺带(错文档 / set_progress 分叉)
- host/game/tests.rs:+2 条判据。①`legacy_external_instance_id(version)` 造出的 M273 老磁贴
⇒ `Scan::revalidate` 必须 `Ok(())`;②同一个版本、id 换成 `external_instance_id(别的根, 版本)`
⇒ 必须 `Err` 且具名。变异实测:只删 `live.rs::discovered` 里那三行 ⇒ ① 红
(`Err("实例 1.20.4 所属的游戏目录已不在…")`),② 不假红;还原后两条全绿
- host/game/tests.rs:210 的错话改正:段间分隔符**等价于一次乘法**(`hash ^= 0` 是恒等),
不是「0 字节也过一遍 xor 也会改哈希」
- state.rs:`set_progress` 降成 `#[cfg(test)]`(它就地改 `instances` 而不动 `published` ⇒ 两份
会分叉;生产推进度走 `CoreEvent::InstanceRuntimeChanged` → `apply`),只被它用的 `Progress`
导入改成路径写法,文件 398 行(门禁 400)
- live.rs:`discovered` 的文档点名守着那三行的两条用例(crate 头 `#![allow(dead_code)]`,
删了编译器一声不吭)
|
2026-11-13 22:39:44 +08:00 |
|
AskaEth
|
2fb61d8006
|
fix(launch): M304 r1 —— 根不见了必须拒绝 + 运行时态落进快照 + 真 host 端到端同拍判据
三条 r1 复审 P2,全部按代码修(不是只改注释)。
**P2-1(唯一实质问题)**:`ext-` 实例「所属根消失 / 改名」被静默放行 ⇒ 可能起错副本。
`live.rs::discovered` 反查落空时原来一律 `Ok(())`;而 `roots::all` 对非配置根是
`if !extra.is_dir() { continue; }`,于是官方根被删 / 改名 / 盘没挂上时反查必然落空 ——
放行之后 `build_request` → `game_for` 会退回配置根,配置根里恰好有同名 `version_id`
就把**另一份**拉起来(gameDir / 存档都在另一边),相对改前(同步扫盘 ⇒ 扫不出来 ⇒ 拒绝)
是回归。改法:把 M273 的「只哈希版本目录名」搬进 `discovered.rs`
(`legacy_external_instance_id`,与 `external_instance_id` 共用同一个多段 FNV),反查落空时
**老档案放行、其余一律拒绝**并给具名原因(+ `stale` 触发重扫)。
另加 `game::tests::instance_id_hashes_are_the_released_ones`:两个哈希公式的字面量钉死
——这次把哈希抽成公共函数,公式一漂就会误杀所有存量 `ext-` 档案。
**P2-2**:`send()` 只认 `InstancesChanged`,而 app 侧 `state.rs:274` 把
`InstanceRuntimeChanged` **就地**写进自己的列表 ⇒ `runtime` 这一位必定分叉,而
`accept::running_pid` 的兜底读的正是它。改:`send()` 同时把运行时态落到 `published`
对应条目,并加用例 `runtime_events_land_in_the_published_snapshot`。
**P2-3**:补真 `CoreHost` + 真 app 的端到端判据(`app/update/instances/scan_phase.rs`):
临时数据根 + 真 host,`advance` 到首份快照落地后点启动,**`advance` 之前**详情页必须已
`can_stop`。变异(把 Busy 挪进启动线程、只有回程才发)实测必红。
判据(新增 3 条 + 1 条钉公式):`revalidate_tests`(根不见了 ⇒ 拒绝)、
`target_tests`(运行时态落快照)、`scan_phase`(真 host 同一拍)、`game::tests`(哈希公式)。
用例夹具按 400 行硬线拆到 `launch/fixtures.rs`。
门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets` 全绿
(426 + 1076)· `clippy --workspace --all-targets -- -D warnings` 0 ·
`fmt --all --check` 0 · 行数全 ≤400(最大 366;未碰的 `app/update/instances.rs` 399)。
|
2026-11-13 20:46:00 +08:00 |
|
AskaEth
|
583a6cf24b
|
perf(launch): M304 —— 点启动/停止不再在 UI 线程扫盘
M302 把 `refresh_instances` 的扫盘搬进了后台线程,但「点 Play」那条路上还留着两次
`game::instances(&self.env)`(`launch_target` 与 `running_pid`):官方根 200 个版本
236 ms、叠加 10 份外部档案 1.35 s,全部落在 UI 线程上。
改法:两条查询都读**已发布快照**(`CoreHost::send` 发 `InstancesChanged` 时留一份),
并在受理那一刻补一道**单实例**复核(`game::Scan::revalidate`),于是
- 调用方线程一次盘都不扫(一次内存查表 + 微秒级复核);
- 快照过期不会拉错东西:档案实例查 `profiles.json` 是否还在、版本有没有被改;
自动发现的 `ext-` 实例用「逐个根重算 id」定位 (根, 版本),再让 `adopt::inspect`
只判这一个版本(不扫 `versions/`)。复核不过 ⇒ 拒绝 + 具名原因 + 立刻重扫。
- 同名不猜:名字兜底只在唯一命中时用,两个同名实例直接拒绝(多根之后很常见)。
- 「点下去立刻有反馈」不变:`Busy` 仍在 `dispatch` 返回前发出(`launch::accept`)。
拆分:`launch_target` / `in_flight` / `mark_pending` / `start_launch` / `running_pid`
从 `host/mod.rs`(原 395/400 行)搬进 `host/launch/accept.rs`;解析与拒绝形状在
`host/launch/target.rs`,单实例复核在 `host/game/refresh/live.rs`。
判据(`host/launch/target_tests.rs`,5 条):
- 同一拍进 Busy(无 tick、无后台回程),且受理不派扫描(代次不变);
- 快照落地后**新增**的实例点启动必须被拒绝(解析改回现场扫盘 ⇒ 红,实测);
- 快照落地后**删掉**的实例点启动必须拒绝并给具名原因(去掉复核 ⇒ 红,实测);
- 两个同名实例不猜;停止读快照里的 pid。
门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets`(423 + 1075 全绿)、
`cargo clippy --workspace --all-targets -- -D warnings`(0)、`cargo fmt --all --check`(0)、
文件行数全 ≤400(`host/mod.rs` 340、`refresh.rs` 382、`launch/` 90/114/242/280)。
|
2026-11-13 20:14:37 +08:00 |
|
AskaEth
|
23c7beee8d
|
test(instances): M302 r2 —— 四条 P2:线程判据真能红 + 两处相位守卫 + 「只报一次」改成可数
- P2-1:`spawn_scan` 的闭包提成 `scan_body`,单测可装探针 + 闸门(只拦同一数据根的扫描)。
新用例 `a_refresh_returns_while_the_scan_body_is_still_parked` 断言四件事:扫描体跑在
**另一条线程**、返回时**一趟都没跑完**、闸门**还没放行**、返回耗时 < 闸门上限的一半。
把 `spawn("instances", job)` 换成直接 `job()` ⇒ 该用例红(原文见报告);旧用例
`a_refresh_returns_before_the_scan_is_done` 的 docstring 改成诚实口径(它证明不了不在调用方线程,
inline 时它照样绿——正是复审 r2 点的那条)。
- P2-2(真缺陷):`check_scan_stall` 补上与 `refresh_instances` 同一个守卫——已就绪
(`adopted().is_some()`)之后不再报 `Stalled`,否则一轮慢刷新会把有数据的一屏换成
「扫描实例比平时久 + 重新扫描」。新用例 `a_slow_refresh_after_ready_never_reports_stalled`。
- P2-3:`a_stalled_scan_reports_once_and_a_late_snapshot_still_lands` 原来把 `drain_events` 调了
两次(第二次必空 ⇒ 断言空转)。改成数实际发出的条数:第一拍恰好 1 条,之后三拍必须 0 条。
- P2-4:新用例 `a_refresh_after_ready_emits_no_scanning_phase`(真 host:已就绪后再刷新一条相位
事件都不许发)+ app 级 `a_refresh_after_ready_keeps_the_list_and_the_tree`(相位仍 `Ready`、
真控件树的文字/按钮节点数与刷新前逐项相等、列表没被清空)。
四条变异(inline / 去 P2-2 守卫 / 去「只报一次」锁 / 去 Scanning 守卫)都实测红,原文进报告。
|
2026-11-13 19:36:33 +08:00 |
|
AskaEth
|
f03411d58b
|
fix(instances): M302 r1 P2 —— 冷启动首帧三态(占位 / 真空态 / 有实例)+ N1~N4
复审 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` 同步加上新页面文件。
|
2026-11-13 19:00:30 +08:00 |
|
AskaEth
|
a0413a5f94
|
feat(host): M302 实例列表刷新搬出 UI 线程(扫盘进后台 + 代次丢弃过期结果)
`refresh_instances()` 以前把「扫盘」与「盖回运行时态」写在一起,而 `dispatch(RefreshAll)`
那一支是唯一没走 `job::spawn` 的(邻座 ListVersions / ListLoaderVersions 都走了)⇒ 同步跑在
UI 线程:官方根 200 版本约 236 ms、叠加 10 份外部档案 1.35 s(仓库内旧实测 M297)。
两半分家:
- `game::instances(&env)` 是纯 env 函数 ⇒ 搬进 `job::spawn("instances", …)`,回程是新载荷
`HostTask::Instances { epoch, instances, elapsed_ms }`;
- 「按 `launches/pending/failed` 覆写 Running/Busy/Failed」要读 host 自己的表 ⇒ 留在 host 线程,
挪到结果回来那一刻(`handle::apply_instances`),结论与之前逐字一致。
顺序:每次刷新领一个只增不减的代次(`game::Scan`),只采纳当代次那份。并发两次刷新 / 扫描途中
删实例 / 收尾触发第三次这三种交错下,先发后到的扫描一律丢(不发事件:清空再重填只是把卡顿换成
白屏)。不做线程取消 —— 扫描是纯读,没有值得取消的东西,丢结果比掐线程安全。
可观测:每次采纳打一行 `[instances] N 个实例 · 扫盘 X ms · epoch E · 丢旧代次 D 份`,
丢弃也各打一行;账本 `game::Scan` 可直接断言(实例数 / 耗时 / 丢包数)。
判据(都真构造:真临时根 + 真 profiles.json + 真线程,只有交付顺序是摆的):
- `a_refresh_returns_before_the_scan_is_done`(改回 inline 即红)
- `two_refreshes_in_flight_publish_only_the_newest`
- `an_older_scan_cannot_resurrect_what_a_newer_one_saw_deleted`
- `a_launch_end_refresh_beats_the_two_that_were_in_flight`
- app 侧 `a_refresh_in_flight_keeps_the_list_and_does_not_pin_the_clock`(不清空 + M298 的
view_dirty 不永久置真)
- `launch_state_tests` 的 5 处 `refresh_instances()` 后补一行 `pump_instances()`(断言一字未动)
|
2026-11-13 17:58:59 +08:00 |
|
AskaEth
|
7d16317411
|
fix(m291): 守卫文档在 400 行内收(⑧ 并入 ⑦ 段,净增 0 行),make_dir_link 可见性收窄到 pub(super)(M291 r1)
|
2026-11-13 00:48:38 +08:00 |
|
AskaEth
|
375cc4f1fa
|
test(presenter): junction 夹具改成逐段 join(带正斜杠的路径让 mklink 报无效语法,用例一直静默跳过),守卫补记 M291 变异(M291)
|
2026-11-13 00:31:14 +08:00 |
|
AskaEth
|
27980cf5b3
|
fix(presenter): junction 夹具改走 platform::child::quiet,两处副本收成一份共享 helper(M291)
|
2026-11-13 00:28:32 +08:00 |
|
AskaEth
|
72137dc658
|
fix(presenter): 回落日志两因并列 + 老 ext 档案覆盖 + 死根档案边界用例(M274 r3)
|
2026-11-12 19:55:52 +08:00 |
|
AskaEth
|
70c6aec6dd
|
fix(presenter): 写设置那道门按(根,版本)判绑定 + 反查回落留日志 + 旗标改真名(M274 r2 P1-1)
|
2026-11-12 19:55:52 +08:00 |
|
AskaEth
|
a614b49538
|
fix(presenter): 多根 r1 复审修四处(磁贴按自己根描述 / 挑 Java 按自己根要求 / 一拍一次扫描 / 只读内容级快照)(M274 r1)
|
2026-11-12 19:55:52 +08:00 |
|
AskaEth
|
dc8323531b
|
feat(presenter): 多根发现(配置根 + 官方启动器标准位置),实例按自己的根解析存档/模组/启动(M274)
|
2026-11-12 19:55:51 +08:00 |
|
AskaEth
|
c5009442c5
|
fix(presenter): 自动发现实例按盘上痕迹选运行目录 + 空态用例补上分支/传参(M273 r1)
r1 复审的两条 P2。
## P2-1:外部实例不再一律 isolated
`versions/<id>/` 里出现 `mods` / `saves` / `config` / `options.txt` 任一 ⇒ `isolated`
(gameDir = `versions/<id>`);否则 ⇒ `shared`(gameDir = 游戏根 `.minecraft`)。
判据只 `exists()`,只读,不改盘;写盘仍然只在用户第一次改设置时(`ensure_profile`)。
- 判据收在 `host/game/discovered.rs::base_mode()`,签名 `(env, id, version_id)`:
**有档案的实例返回全局 `instances.mode`**(D20 原口径,一字不变),自动发现的按痕迹判。
- 派生运行目录的五处(启动 / mods / saves / addon install / addon pack)从传
`global_mode` 改成传 `base_mode` ⇒ 「读到的存档/模组在哪」与磁贴上的 ISOLATION 同一份判据。
- `isolation_of(env, id, version_id)` 多收一个版本 id:实例文件优先,缺省 `base_mode`。
于是懒创建档案(只写 launch 段、isolation 仍为空)之后,磁贴不会从「共用目录」翻回「独立目录」。
- 磁贴/详情:自动发现的实例多一句「运行目录由盘上已有的文件决定」(i18n 中英);
按启动时再弹一条说清**这一次**用哪一边(`pages::instances::discovered_run_toast`,
同文案在屏不重复叠加)。
- `dto::DISCOVERED_ID_PREFIX` / `is_discovered_instance` 成为「自动发现」的公开判据
(不给 `InstanceSummary` 加字段:那要改 24 处结构体字面量,跨 agent / cli)。
## P2-2:空态用例的覆盖面
r1 复审实测:把 `!block.rows.is_empty()` 改成 `block.rows.is_empty()`(0 实例时反而没按钮),
当时三条用例全绿。现在:
- `detail_empty` 拆成 `empty_not_selected` / `empty_no_instances` 两个函数,分支各自可断言;
- `the_cta_lives_only_in_the_no_instances_branch`:CTA 只在「一个都没有」那一支、
「没选中」那一支必须是 `Vec::new()`,且分派必须写成 `if has_instances {`(取反即红);
- `the_page_asks_the_empty_state_with_the_negated_row_count`:两条调用点(宽窗 + 窄窗)
都必须传 `!block.rows.is_empty()`,条数也钉住(多一条即红)。
- 两个变异已自证:传参取反 ⇒ 后一条红;CTA 挪支 ⇒ 前一条红(红原文见交付报告)。
## P3
- `java::target_major()` 改读并集快照:只有自动发现实例的用户原来会拿到默认 JDK17 而不是
该版本要求的 JDK(`profiles.json` 是空的)。有档案时顺序与旧写法一致。
- `instances()` 全量扫仍在 UI 线程——本轮不动,刷新时机与代价写进报告(塔另开一条)。
## 结构
`host/game.rs` 394 → 286 行:自动发现那一簇(痕迹判据 / 扫出来的版本 / 合成视图)搬到
`host/game/discovered.rs`(138 行),只为守 400 行硬线;`game::base_mode` 仍是同一条路径。
## 门禁
app-iced lib 973 passed / presenter all-targets 386 passed / i18n 36 passed /
fmt 0 / clippy -D warnings 0。
|
2026-11-12 14:21:30 +08:00 |
|
AskaEth
|
5479f25068
|
feat(presenter): 实例清单改并集(档案 ∪ versions/*)+ 空态接导入向导(M273)
用户实测:HMCL 能看见 `.minecraft/versions/樱落怡然客户端`,ULL 的实例页是空的
(日志 `instances=0`)。根因不是路径找错,而是 `host::game::instances()` 只列
`profiles.json` 里的档案,第三方启动器装好的版本在界面上根本不存在。
## 列表改并集(presenter/src/host/game.rs)
- `versions/*` 里**能解析出清单**(`<id>/<id>.json` 的 id 与目录名一致、继承链走到底、
符号链接不越界)且没有被任何档案绑定的版本,就地合成一条实例视图:
显示名 = 目录名、`version_id` = 目录名。扫描复用 `core::game::adopt::scan`,
没有另写一套目录遍历。
- 只读:合成视图不建目录、不写 profiles.json、不碰 versions/ 一个字节。
- 档案优先:某版本已被档案绑定时只按档案出一条(名字/隔离档/启动参数都保留),不重复列。
- 坏目录(无清单 / JSON 坏 / id 不符 / 缺父 / 越界)不当实例;`versions/` 读不出来也只是
退回「只有档案」的旧行为,不让实例页报错。
- 外部实例的视图 id 用 `ext-<fnv64>`:`is_valid_instance_id` 只收 `[A-Za-z0-9_-]`,
而版本目录名可以是中文(现场就是),直接沿用版本 id 会让**启动**在读实例设置时就失败。
## 懒创建档案(只有第一次改它才写盘)
`set_isolation()` 与 `apply_launch_args()` 的「找不到实例」改成 `game::ensure_profile()`:
外部实例第一次改隔离档 / 启动参数时才写 profiles.json(幂等,id 与视图 id 相同)。
## 空态可见入口 + 文案(app-iced / i18n)
- 空态分两种:列表里有东西但没选中(原「未选择实例」)与**一个实例都没有**
(新:「还没有实例」+ 实心主行动「导入已有版本」)。用户现场看不到任何入口正是前一种文案
在误导。
- `adopt_action()` 成为「导入已有版本」意图的唯一产地,页头按钮与空态主行动共用它。
## 门禁
`cargo test -p unlucky-app-iced --lib` 971 passed / `cargo test -p unlucky-presenter
--all-targets` 384 passed / `cargo test -p unlucky-i18n` 36 passed /
`cargo fmt --all --check` 0 / `cargo clippy -p unlucky-presenter -p unlucky-app-iced
--all-targets -- -D warnings` 0。
|
2026-11-12 13:13:53 +08:00 |
|
AskaEth
|
e2021645d7
|
feat(launch): M157 实例级启动参数(五项回落 + graphicsBackend 三态)
core: 新增 game/launch_overrides(LaunchOverrides 五项 All-Option + 三级回落 + 文本补丁校验)与 launch/overrides(拼装落位:全局 java.extra_args 进 user.jvm、实例 jvmArgs 进 override_jvm、gameArgs 进 user.game、memory 进 -Xmx、backend 进 --graphicsBackend 并复用既有版本门槛告警);实例 schema 登记 launch.* 五键。
presenter: dto/LaunchArgsView+LaunchArgsPatch,主机侧读/写与门槛判定(backend_allowed),AppCommand::SetInstanceLaunchArgs。
cli: 新命令 instances launch-args(读/写/--clear),launch 装配链消费回落值。
app-iced: 实例详情页「启动参数」区(JVM/游戏参数、内存、Java、三态后端 chips + 灰化提示 + 保存/恢复),表单状态归 App、i18n 中英文案同步。
|
2026-11-07 10:36:35 +08:00 |
|