嫌疑模组识别(Suspected Mods)

基本信息

属性 值
报告分区名 Suspected Mods
实现类 vfyjxf.bettercrashes.utils.ModIdentifier
注入点 CrashReport.populateEnvironment TAIL(CrashReportMixin.java:82-102)
界面显示 黄色 0xE0E000 居中,最多 3 行

算法

ModIdentifier.identifyFromStacktrace(Throwable)(:39-57):

  1. 建「jar 文件 → ModContainer」映射(makeModMap,:89-111), 并缓存在静态字段 cachedModMap,只建一次
  2. 沿 getCause() 链广度遍历整棵异常树, 收集所有 StackTraceElement.getClassName() 去重成 LinkedHashSet
  3. 对每个类名:
    • 跳过 org.spongepowered.asm.mixin.*(:61, 注释说明 Mixin 库被所有 mod 共享,识别出来没有意义)
    • 用反射调 LaunchClassLoader.untransformName(String) 还原混淆后的类名 (拿 SRG 名去 getResource 才能找到 class 文件,:64-65, 115-125)
    • 解析 class 的 URL;jar: 协议则截到 ! 之前取出 jar 路径
    • 用 canonical File 去 modMap 里查所属 mod
  4. 合并所有命中的 ModContainer

构建 modMap 的两个排除

makeModMap(:97-99)显式跳过两个容器:

  • Loader.instance().getMinecraftModContainer() —— 本体 jar
  • getIndexedModList().get("FML") —— Forge jar

注释里引用了 Forge 的 issue #4919 作为 workaround 依据。

在报告里怎么出现

CrashReportMixin.betterCrashes$afterPopulateEnvironment(:82-102) 用 addCrashSectionCallable 惰性注册,在报告被序列化时才计算:

  • 每项格式为 mod.getName() + " (" + mod.getModId() + ")"
  • 空集时字面量写 "Unknown"
  • 整个 callable 被 catch (Throwable e) 包裹,异常时 把异常堆栈当作返回值塞进报告

界面消费

GuiProblemScreen.getModListString()(:235-252)把 ((CrashReportExt) report).betterCrashes$getSuspectedMods() 转成显示名列表:

  • 字段为 null(即 callable 从未执行过)→ 显示 bettercrashes.gui.common.identificationErrored = “[Error identifying mod]”
  • 空集 → unknownCause = “Unknown”

⚠️ 界面这里只显示 getName(),而报告文本里是 Name (modId) —— 两处格式不同是有意为之(界面窄,报告要精确)。

源码缺陷(源码核对 2026-10-01 审计)

  • ⚠️ CrashReportMixin.betterCrashes$getVanillaFixComment(:152-162)有 NPE 隐患: if (Math.random() < 0.01 && !betterCrashes$suspectedMods.isEmpty()) 在 callable 尚未被调用时 betterCrashes$suspectedMods 仍为 null ⇒ NPE。被 catch (Throwable ignored) {} 吞掉,退回原版 witty comment。 1% 的概率路径静默失效,无日志。
  • ⚠️ 源码缺陷:反射 setAccessible(true)(ModIdentifier.java:119) 对 LaunchClassLoader.untransformName 强行开放访问。 该方法名是 1.7.10 LaunchWrapper 的内部实现, 换启动器/映射后可能不存在 ⇒ NoSuchMethodException 被包成 RuntimeException 抛出,整个识别流程失败。
  • ⚠️ 源码缺陷:identifyFromClass 中 url.getFile().substring(0, url.getFile().indexOf('!'))(:74) 若 URL 中没有 !(非标准 jar URL),indexOf 返回 -1, substring(0, -1) 抛 StringIndexOutOfBoundsException。 该异常不在 :80 的 catch (URISyntaxException | IOException) 覆盖范围内。
  • identifyFromStacktrace 沿 cause 链收集,但 Throwable 的 suppressed 异常未被纳入(对比 StacktraceDeobfuscator 两者都遍历)。