配方适配器

本 mod 带 7 个 IAdapter 实现,用来把第三方 mod 的 NEI 配方处理器翻译成 本 mod 认识的「原料 / 产物 / 催化剂」三组标签。每个适配器只在对应 mod 存在时才加载。

基本信息

属性 值
接口 me.towdium.jecalculation.nei.adapter.IAdapter
实现数量 7(Forestry / AE2 / GregTech6 / GregTech / GTPP / Avaritia / EnderIO / Thaum 共 8 个类,但 GregTech 与 GregTech6 互斥,实际最多同时 7 个)
容器 Adapter.adapters(public static List<IAdapter>)
初始化 Adapter.init(),由 NEICalculatorConfig.loadConfig() 调用
转换入口 Adapter.handleRecipe(recipe, index, inputs, outputs)
接口默认 getAllOverlayIdentifier() 默认返回 Collections.emptySet()

准确地说:adapter/ 包下共 8 个 IAdapter 实现类,但 Adapter.init() 里 GregTech 与 GregTech6 是 if / else 互斥分支 (由 GregTech6.isGT6() 决定),所以同时最多注册 7 个。

加载条件(Adapter.init())

适配器 判定条件 检测日志
Forestry Loader.isModLoaded("Forestry") Forestry detected
AE2 Loader.isModLoaded("appliedenergistics2") ae2 detected
GregTech6 Loader.isModLoaded("gregtech") 且 GregTech6.isGT6() 为真 gregtech6 detected
GregTech Loader.isModLoaded("gregtech") 且 !isGT6() gregtech5 detected
GTPP Loader.isModLoaded("miscutils")(包在 try/catch 里,失败打 Init error with gt++) gt++ detected
Avaritia Loader.isModLoaded("Avaritia") Avaritia detected
EnderIO Loader.isModLoaded("EnderIO") EnderIO detected
Thaum Loader.isModLoaded("thaumcraftneiplugin") Thaum detected

init() 首尾各打一条日志(=====Just Enough Calculation Init Start===== / … Finish=====)。

GregTech6.isGT6() 的判定

Class<?> buildInfoclz = Class.forName("gregtech.BuildInfo");
String version = (String) buildInfoclz.getField("version").get(null);
return version.startsWith("6.");

反射读 gregtech.BuildInfo.version,判断是否以 "6." 开头。 任何 ClassNotFoundException / NoSuchFieldException / IllegalAccessException 都吞掉并返回 false(即当作 GT5 处理)。

overlay 标识符

getAllOverlayIdentifier() 的返回值直接被 NEICalculatorConfig.loadConfig() 拿去 API.registerGuiOverlay 注册。汇总如下:

适配器 handleRecipe 实现类数 overlay 标识符来源
AE2 1(空实现) 硬编码 Arrays.asList("inscriber", "grindstone")
Thaum 1 硬编码 5 个
EnderIO 1 硬编码 6 个
Avaritia 1(空实现) 硬编码 Arrays.asList("extreme_compression", "extreme")
Forestry 1 空集(未覆写,走接口默认 Collections.emptySet())
GTPP 1 反射 + 硬编码
GregTech(GT5) 1 反射
GregTech6 1 反射

Forestry 是唯一一个不提供任何 overlay 标识符的适配器——它只做 handleRecipe 的数据转换,不会让 Forestry 机器的配方页多出一个计算器按钮。 Forestry 配方能被处理,是因为 Forestry 的处理器大多继承 TemplateRecipeHandler 且未覆写标识符,从而被 loadConfig() 里那个 null 通配标识符覆盖。

overlay 标识符逐个枚举

适配器 全部标识符
AE2 "inscriber"、"grindstone"(2 个)
Thaum "arcaneshapedrecipes"、"arcaneshapelessrecipes"、"aspectsRecipe"、"cruciblerecipe"、"infusionCrafting"(5 个)
EnderIO "EnderIOAlloySmelter"、"EIOEnchanter"、"EnderIOSagMill"、"EnderIOSliceAndSplice"、"EnderIOSoulBinder"、"EnderIOVat"(6 个)
Avaritia "extreme_compression"、"extreme"(2 个)
Forestry 空集
GTPP 反射 GTPP_Recipe$GTPP_Recipe_Map.sMappings + GTPP_Recipe$GTPP_Recipe_Map_Internal.sMappingsEx,再硬编码补 "GTPP_Decayables"、"Decayables"
GregTech(GT5) isPreRecipeRefactor 为真 → 反射 gregtech.api.util.GT_Recipe$GT_Recipe_Map.sMappings;否则反射 gregtech.api.recipe.RecipeMap.ALL_RECIPE_MAPS
GregTech6 反射 gregapi.recipes.Recipe$RecipeMap 的静态字段 RECIPE_MAPS,遍历取值读其 mNameNEI 字段

GT6 的反射实现对每个 map 单独 try/catch (IllegalAccessException) 跳过失败的项; Class.forName 整体失败则 printStackTrace 并返回空集合(此时 GregTech 机器的配方页不会有计算器按钮,但不会崩)。

handleRecipe 的通用流程

Adapter.handleRecipe 先无条件收集原始数据:

// raw inputs
recipe.getIngredientStacks(index).stream()
      .map(ps -> (Object[]) ps.items)
      .forEach(inputs::add);

// raw outputs
PositionedStack resultStack = recipe.getResultStack(index);
if (resultStack != null) outputs.add(resultStack.items);

然后把同一个 recipe 依次喂给所有已加载的适配器:

try {
    for (IAdapter adapter : adapters) {
        adapter.handleRecipe(recipe, index, inputs, outputs);
    }
} catch (Exception e) {
    Utilities.addChatMessage(Utilities.ChatMessage.RECIPE_TRANSFER_ERROR);
    JustEnoughCalculation.logger.error("Exception when handling recipe: " + recipe.getClass().getName());
    e.printStackTrace();
}

整个循环被一个 try/catch 包住:任何一个适配器抛异常,后续适配器全部被跳过, 并向聊天栏发一条 RECIPE_TRANSFER_ERROR。

各适配器额外做的事

适配器 handleRecipe 实际做什么
AE2 空实现 {} —— 纯粹只为贡献 inscriber / grindstone 两个 overlay 标识符
Avaritia 空实现 {} —— 纯粹只为贡献 extreme_compression / extreme 两个 overlay 标识符
Forestry 8 个工厂处理器逐一识别,从 RecipeHandlerBase.CachedBaseRecipe.getFluidTanks() 里挖出流体(见下表)
Thaum 命中 5 个神秘学处理器时,把「奥义(aspect)」物品从原料里剔除
EnderIO 命中 SagMillRecipeHandler 时把 getOtherStacks() 全部塞进 outputs;命中 VatRecipeHandler 时反射挖流体
GTPP / GregTech / GregTech6 处理 GregTech 的虚拟机器(VM)配方结构

Thaum — 剔除奥义

静态块反射加载 5 个处理器类(全限定名前缀 com.djgiannuzz.thaumcraftneiplugin.nei.recipehandler.):

ArcaneShapedRecipeHandler、ArcaneShapelessRecipeHandler、AspectRecipeHandler、 CrucibleRecipeHandler、InfusionRecipeHandler

命中任一后遍历 inputs,把注册名为 thaumcraftneiplugin:Aspect 的 ItemStack 从原料列表中 it.remove() 掉——奥义是神秘学的「元数据」而非真实材料, 留在原料里会让计算结果完全失真。

Class.forName 的 ClassNotFoundException 在这里是 ignored 静默吞掉的 (与 EnderIO 的 printStackTrace 不同)。

EnderIO — 轧机与槽罐

静态块反射加载(同样 ClassNotFoundException 只 printStackTrace):

用途 类名
defaultHandlers(唯一一项) crazypants.enderio.nei.SagMillRecipeHandler
vat crazypants.enderio.nei.VatRecipeHandler
  • 命中 defaultHandlers:outputs.addAll(getOtherStacks(index))——碎矿机的副产物。
  • 否则若 vat != null && vat.isInstance(recipe):反射 crazypants.enderio.nei.VatRecipeHandler$InnerVatRecipe 的 arecipes 字段取出 CachedRecipe,再反射其 result(→ outputs)与 inFluid(→ inputs)两个字段。 反射前后用 setAccessible(true/false) 成对切换。

static 块在类加载时执行,此时 EnderIO 若未安装,defaultHandlers 为空、 vat 为 null。但由于 Adapter.init() 只在 isModLoaded("EnderIO") 时才 new EnderIO(),这不会造成实际故障。

Forestry 的槽位坐标判定

Forestry 适配器大量依赖 PositionedFluidTank.position.x 和 PositionedStack.relx 来区分「原料」和「产物」:

判定 含义
tank.position.x == 30 该流体槽是原料 → inputs.add(...)
其他 x 值 该流体槽是产物 → outputs.add(...)
positionedStack.relx == 34 该物品是原料 → inputs.add(items)
其他 relx 该物品是产物 → outputs.add(items)

具体到 8 个工厂处理器:

处理器 提取方式
NEIHandlerBottler getFluids() 的第 0 组 → inputs
NEIHandlerCarpenter getFluids() 的第 0 组 → inputs
NEIHandlerCentrifuge getOtherStacks() → outputs
NEIHandlerFabricator getFluids() 的第 0 组 → inputs
NEIHandlerFermenter 按 position.x == 30 分流
NEIHandlerMoistener 流体第 0 组 → inputs;getOtherStacks() 按 relx == 34 分流
NEIHandlerSqueezer getFluids() 的第 0 组 → outputs
NEIHandlerStill 按 position.x == 30 分流

getFluids 有一句源码注释警告类型问题:It won't check the type——它直接强转 RecipeHandlerBase.CachedBaseRecipe,不做 instanceof 校验。

Adapter.convertFluid

一个静态辅助方法(不走 adapters 列表):

if (Loader.isModLoaded("gregtech")) {
    if (GregTech6.isGT6()) return GregTech6.convertFluid(itemStack);
    else                   return GregTech.convertFluid(itemStack);
}
return itemStack;

即装了 GregTech 时把「流体显示物品」转成 GregTech 流体,否则原样返回。 它被 NEIPlugin.getLabelUnderMouse() 和 NEIPlugin.openRecipeGui() 调用。

相关条目