compat 派发机制

[!INFO] Git Commit: 93da3e1 | Updated: 2026-10-01

本 mod 不硬编码 compat 列表,而是靠 @Compat 注解 + FML 的 ASMDataTable 在运行期扫描, 再用反射逐个实例化。全部逻辑集中在 StructureCompat.java、Compat.java 两个文件。

@Compat 注解

Compat.java 是一个只有单个成员的运行时注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Compat {
    String[] value();
}

value() 是依赖的 modid 列表,不是类名。注意 RetentionPolicy.CLASS——注解写入 class 文件但不保留到 运行期反射,这正是它必须通过 FML 的 ASM 扫描而非 Class.forName 查注解的原因。

两阶段流程

阶段一:preInit 收集候选

for (ASMData d : event.getAsmData().getAll(Compat.class.getName())) {
    if (((List<String>) d.getAnnotationInfo().get("value")).stream().allMatch(Loader::isModLoaded)) {
        list.add(d.getObjectName());
    }
}
compats = list;

StructureCompat.java:42-50。getAll() 扫描本 jar 中所有带 @Compat 的类型,对每个类型取出 value() 里的 modid 并要求全部 Loader.isModLoaded 为真,才把类名存进 compats。任何一个依赖 mod 缺失,该 compat 就整条被跳过——这是全有全无(AND)门控,不是 OR。

阶段二:postInit 反射实例化

for (String compat : compats) {
    try {
        Class.forName(compat).getConstructor().newInstance();
        success += 1;
    } catch (InvocationTargetException e) {
        LOG.error("Compat activation errored!", e.getTargetException());
    } catch (ReflectiveOperationException e) {
        LOG.error("Cannot load!", e);
    }
}

StructureCompat.java:59-69。注册逻辑全部写在每个 compat 类的构造函数里,无参构造即"启用"。 Class.forName 使用调用者的类加载器(StructureCompat 的 loader),所以能解析到 api 引入的 StructureLib 类型与 implementation 引入的其它 mod 类型。

全部 9 个 compat 及其门控 modid

compat 类 门控 modid 作用
CompatBaubles Baubles 饰品栏物品提供者
CompatForestry Forestry 林业背包物品提取器
CompatBackpackMod Backpack 仅对末影背包开放自动取物
CompatAdventureBackpack adventurebackpack 穿戴背包物品提供者 + 提取器
CompatAppliedEnergistics appliedenergistics2 AE2 无线终端 + 便携单元提取器
CompatWCT ae2wct WCT 无线合成终端提取器
CompatAE2FC ae2wct AE2FC 无线终端提取器
CompatRailcraft Railcraft(Railcraft.MOD_ID 常量) 6 组多方块结构信息
CompatBogoSorter bogosorter 中键整理事件拦截

两处值得注意的门控取值

  • CompatAE2FC 的门控是 ae2wct 而不是 AE2FluidCraft 自己的 modid。 源码里写的是 @Compat("ae2wct")(CompatAE2FC.java:20)。即 AE2FC 的兼容层只有在 WCT 也装的情况下才会加载。 由于 dependencies.gradle 中 AE2FluidCraft-Rework 传递性依赖 WirelessCraftingTerminal (且显式 exclude module: 'Baubles'),实际打包中两者必然同时存在,所以该门控成立。
  • CompatRailcraft 用的是编译期常量 @Compat(Railcraft.MOD_ID)(CompatRailcraft.java:37), 而其余 8 个都是字符串字面量。两者运行时等价,但 Railcraft 的取值来自被依赖 jar 而非本仓库。

门控 modid 的大小写

上表中同时存在 Baubles、Forestry、Backpack、Railcraft 等首字母大写的 id,以及 adventurebackpack、appliedenergistics2、ae2wct、bogosorter 等全小写的 id。 Loader.isModLoaded 在 1.7.10 中是大小写不敏感的(内部按小写规范化),因此两种写法都能命中, 但风格上并不统一——@Mod 注解的 modid 惯例是小写,这里并不成立。

扩展方式

新增一个兼容层只需三步,无需改动 StructureCompat:

  1. 新建类,加 @Compat("目标modid");
  2. 把注册逻辑写进 public 无参构造函数;
  3. 在 dependencies.gradle 里把对应 mod 加为 implementation(或 compileOnly)。

无需修改任何中央列表——这是 @Retention(CLASS) + ASMDataTable 扫描的收益。