MC 问题 mod 二分排查工具 — 用二分法自动定位"不报错、不崩溃"的问题 mod。
渲染异常 / 行为异常 / 性能异常这类加载器零提示的问题, 传统排查只能人肉开关 mod。 本工具把 "开关 mod → 启动游戏 → 观察结果" 循环自动化为状态机, 以游戏进程生命周期为轮次边界, log2(n) 轮收敛到罪魁 mod。
Python 3.12+ / PySide6 / psutil / watchdog / pypinyin / Windows
scanner ──枚举/解析──▶ model + depgraph ──图/闭包──▶ engine(纯函数, 不碰文件系统)
│ │ RoundPlan(目标启停集)
▼ ▼
watcher + processmon ◀──事件── executor(diff 重命名, 幂等, 回读验证)
▼
ui/app.py(PySide6 信号槽, 全程序唯一有"时间"概念的层)
- 引擎只做纯状态转移(报告 → 归算 → 下一计划), 不碰文件系统
- 执行层以 diff 重命名实现幂等启停, apply 后回读磁盘, 引擎以"实际生效集"归算答案(不信计划自封)
- 观察层: watchdog 监听 logs/latest.log(启动信号)与 crash-reports/(崩溃信号); psutil 绑定游戏进程做内核级等待(零 CPU), 绑定失败才降级为进程快照轮询
- UI 层是唯一调度者, 耗时操作全走后台线程, 界面零阻塞
- v0.2 外围层: snapshots(状态快照) / repair(缺失依赖修补) / sortkey(拼音排序键) / ui.style+panels(主题与依赖画框), 均为独立模块, 不侵入五层主链
python -m pip install -r requirements.txt
python main.py
- 选择 .minecraft 实例的 mods 目录 → 扫描(解析全部顶层 jar 元数据)
- 点「开始排查」→ 基准轮: 按当前状态启动游戏确认 bug 存在(确定存在可跳过)
- 之后每轮: 工具禁用约一半嫌疑 mod → 提示你启动游戏 → 测完正常关闭游戏 → 右侧按钮回答 "问题还在 / 消失了 / 未测试"
- 排查中随时自由干预: 右键钉扎 mod(钉启=恒参与测试 / 钉禁=恒禁用, 均留在嫌疑推理)或冻结无辜 mod(恒启用+移出调度), 下轮计划自动吸收; 表格支持名称/拼音实时过滤
- 收敛后自动进入验证轮(只启用嫌疑 mod 及其必需依赖)→ 终局报告
- 一键还原所有 mod 到初始状态
会话全程自动存档(sessions/), 中断后可恢复继续。
- 表头点击排序: 全列拼音键排序(中文名按拼音序), 版本列数值分段 ("1.10.2" 正确大于 "1.9.4"), 状态/嫌疑列复合键; 列宽/列序/排序随布局持久化
- 最后修改列: 名称右侧展示文件 mtime
- 双击状态格 = 级联启停: 禁用连带拖死依赖它的整条链(与引擎闭包同向), 启用对称地连带拉起; 仅空闲态可用(防与排查推理脱钩)
- 双击任意行 = 依赖画框: 上半"依赖它的"(禁它会拖死的), 下半"它依赖的"; 条目附"该模组还依赖于 / 还被其他模组依赖"的额外关系提醒
- 快照系统(snapshots/): 一键保存全部 mod 启停状态全集, 随时恢复回已知 状态(与排查会话互补: 会话存进度, 快照存状态)
- 修补系统: 声明了但目录内无人提供的依赖 → 按文件名反查候选 jar → 确认后改写其 mods.toml 的 modId 接通依赖边(原件备份 .orig; 多 mods 块的 jar 只提示不改写)
- 判决非模态化: 每轮结果在右侧按钮区作答, 全程可见不被弹窗遮挡; 右列集中全部动作按钮
- 调试直通: "就当我开关过游戏了"按钮跳过真实启动直接进判决(信号链自测用)
- 黑金直角主题: 全局 QSS, modbisect/ui/style.py 是唯一色源
- 单击表格行 → 左侧树面板联动: 上半「依赖它的」(递归传递, 禁用即拖死全链), 下半「它依赖的」(需求链, 缺失依赖橙色叶子)
- 双树 QTreeWidget, 可拖分栏(QSplitter); 树宽 200-320px; 顶级默认展开一级
- 环路安全: 环路节点灰显「(环路)」不展开(路径栈剪枝); 深度超 32 层截断
- 节点着色: 启用绿/禁用红/缺失橙; tooltip = jar 身份 + 提供 modid
- 与画框(双击)互补: 画框=单跳邻接总览, 树=递归链路
- 启停级联后树状态实时同步(刷新事件链)
单击看树, 双击操作 — 两手势职责分离, 互不干扰。
- 新增四种卷帘排查方向: 顶到底/底到顶 × 禁用/启用, 每轮按锁定行序卷动 n 个
- 锁序 = 点击「开始排查」时的表格显示序(初始启用集 W 内); 开排查后改排序不影响
- 罪魁段锁定后自动段内转二分收敛; 经典二分模式行为不变
- 启用方向首轮为静默底场实验(其余全禁), 建议先创建快照
- 原生禁用 mod(可选 DLC/多版本共存)不在卷动范围, 磁盘态永不触碰
- 会话格式 v2: 卷帘规格随会话持久化, 旧 v1 会话照常恢复
- 双击级联启停: 选中行高亮不再丢失(表格就地更新, 行身份锚定)
- 右侧按钮组不再闪烁(轻操作退出全局状态机, 在途旗标三入口互斥)
- 依赖树不再闪空态(刷新信号屏蔽, 尾部统一 refresh)
- 冒烟 61 → 63 断言(级联后选中保留 / 树锚定不漂移)
- 阻碍子流程(v0.5): 某轮游戏起不来/崩溃 → 作答"未测试" → 引擎在最近可测盘面上做可测性子二分(探针试探禁用候选, 两答案均 严格缩池), 自动冻结破坏可测性的 mod(恒启用+移出调度), 嫌疑 排空时结案指路
- 全拖活锁修(v0.5.1): 探针预报拦截 — 半探全拖时降级单点扫描
- 手动冻结/解冻(v0.5.2): 已知无辜的 mod 直接冻结(右列按钮, 恒启用+移出嫌疑); 解冻对称恢复资格; 会话 v3 事件重放
- 调试直通常亮 + 手动宣布退出(v0.5.3): 调试按钮任何状态可用; WAIT_GAME 下"就当我开关过游戏了" = 手动宣布退出(吞掉晚到的 真实退出事件, 复用既有分流逻辑)
- 钉扎 = 双向自由意志: 比冻结更细 — 钉启(恒参与每轮测试) / 钉禁(恒禁用, 连闭包拖死依赖它的整条链), 都保留嫌疑资格继续 参与二分推理(冻结 = 移出调度)
- 操作: 右键菜单钉启/钉禁/撤销; 排查中双击状态列 = 智能钉 (已钉撤销, 未钉按磁盘态反向钉); 钉扎计数金色标签实时显示
- 钉扎在计划出口统一生效, 每轮盘面自动吸收(引擎归算逻辑零改动); 基准轮前可钉禁疑似罪魁再重测基准
- 过滤框: 表格实时过滤, 名称/拼音双通道子串匹配; 排序变化后 过滤态自动跟随行身份重放
- 收敛结案新出口: 嫌疑收敛至钉禁闭包(指名 culprit)/钉启集(建议撤 部分钉启)/不可测与钉扎冲突(指路重测)
- 会话格式 v4: 钉扎事件随会话持久化, 恢复后精确重建
- 恒启用闭包根修: 冻结/钉启成员连同其支撑依赖链整体保留 (闭包而非点集) — 修复稀疏盘面悬空导致的连环冻结死循环 (真机 6h 会话轮 27-40 暴露)
- 收敛三选一: 嫌疑收敛到单个单元后不再强制进入验证轮, 三选一: 结案(未经隔离验证) / 就地隔离验证 / 干净实例复测(推荐)
- 干净实例复测: 嫌疑(+历次复测排除的共谋候选+完整依赖链)导出 到自选实例根(路径与 config 拷贝范围记忆在配置, 不入会话); 在 干净实例测试后回来作答复现与否
- 复测循环: 复现 → 确诊罪魁(隔离验证完成); 不复现 → 冻结嫌疑 继续找共谋者 — 组合复现 → 共谋结案("多个模组共同导致的问题, 建议给双方作者同时反馈"); 全部排除且域尽 → "组合成因未复现" 防御结案
- 复测包清理提醒: 终局弹窗+日志双通道提示手动删除(程序不代删 用户游戏目录)
- 会话格式 v5: 收敛决策与复测作答入史, 恢复后精确重建
- 仅处理 mods 目录顶层的 jar(不递归)
- 假设同一时刻只运行一个游戏实例
- 游戏崩溃的那一轮观察可能无效, 判决区会以警告色提示并建议重测
- 快照恢复以 size 指纹拦截目录错配(差异警告需确认后才恢复); 修补只改 modId 行(多 mods 块 jar 只提示不改写, 原件备份 .orig)
- 就地验证不复现 = 观察噪声结案; 组合成因排查已于 v0.7 交付(复测循环)
- 禁用方式 = 文件名追加后缀(xxx.jar.disabled, HMCL/PCL 通用约定), config.json 可调 disabled_suffix 等参数(首次运行自动生成)
python tests/test_core.py # 核心模块: 引擎(冻结/钉扎/复测)/执行器/会话/快照/修补/排序键(231 断言)
python tests/smoke.py # UI 冒烟: offscreen 无头全链路, 覆盖 v0.2-v0.7 全功能(141 断言)
MPL-2.0, 见 LICENSE。