配方类型对比

RecEx 用 4 个自有的数据包装类承载 4 类配方,字段名与输出 JSON 一一对应。它们相对原版/Forge 配方的共同改动是:把 ItemStack 之类的对象图拍平成只有基本类型与 String 的浅结构,以便 Gson 直接序列化。

对比表

维度 ShapedRecipe ShapelessRecipe OreDictShapedRecipe FurnaceRecipe
源类型 net.minecraft.item.crafting.ShapedRecipes net.minecraft.item.crafting.ShapelessRecipes net.minecraftforge.oredict.ShapedOreRecipe FurnaceRecipes.smelting().getSmeltingList() 的 Map 条目
JSON type shaped shapeless shapedOreDict smelting
输入字段 iI iI iI input
输出字段 o o o output
输入容器 List<Item> Item[] List<IItem> 单个 Item
输入元素类型 恒为 Item 恒为 Item Item 或 ItemOreDict Item
保留空槽 是(null 占位) 不适用(本来就没有空格) 不适用 不适用
类形态 普通类 普通类 普通类 Java record(@Desugar)
抓取方式 遍历 recipeItems 网格数组 遍历 recipeItems 列表 6 分支类型判定 Map entrySet().stream()
额外逻辑 无 输入排序两次 类型分派 + OreDict 反查 无

各类型多抓了什么

ShapedRecipe — 有序合成

for (ItemStack stack : original.recipeItems) {
    Item item = RecipeUtil.formatRegularItemStack(stack);
    rec.iI.add(item);          // ← 没有 null 判断
}

formatRegularItemStack(null) 返回 null,而这里没有像 GT 分支那样的 if (item == null) continue;,所以空格子会以 null 元素进入 iI。Gson 序列化 null 数组元素会写成 null,于是网格位置被保留,形状隐式编码在数组顺序里。

代价:iI 里混有 null,消费方必须容忍;同时 recipeWidth / recipeHeight 从未被读取——源码全文没有访问这两个字段,所以输出里没有宽高,只有位置。

ShapelessRecipe — 无序合成

输入容器是 Item[] 而非 List<Item>,且 iI 用 rec.iI = ...toArray(Item[]::new) 整体赋值(其它三个类都是逐个 add)。

因为无序配方没有格子概念,排序被用了两次:一次在重建 ShapelessRecipes 时对 r.recipeItems 排序,一次在映射成 Item[] 时再排一次。原版 recipeItems 本身不含 null,所以不存在空槽问题。

OreDictShapedRecipe — 矿物词典有序合成

输入是 List<IItem>——IItem 是个空标记接口(IItem.java 里只有一个 public interface IItem {}),由 Item 和 ItemOreDict 同时实现。声明成 List<IItem> 而非 List<Object> 是为了让集合语义明确。

抓取时对每个 getInput() 元素做6 分支类型判定:

输入的 Java 类型 处理 产出成分
ItemStack 直接格式化 Item
String parseOreDictionary(String) 反查 OreDict ItemOreDict
String[] parseOreDictionary(String[]) 合并多名字 ItemOreDict
net.minecraft.item.Item 包成 new ItemStack(item) Item
Block 包成 new ItemStack(block, 1, Short.MAX_VALUE) Item(m = 32767)
ArrayList<?> 逐个元素 formatRegularItemStack,并把元素命中的 OreDict 名收进 dns ItemOreDict
其它 log.warn("OreDict Input Type not parsed! ...") 不产出,被静默丢弃

因此 iI 数组里可能混着两种形状完全不同的对象(Item 与 ItemOreDict),Gson 按运行时类型序列化,消费方可以靠键是否存在(有 dns/ims 即为 ItemOreDict)区分。

ArrayList 分支的细节:

  • 每个元素调 OreDictionary.getOreIDs(stack),把得到的名字收进 dns
  • 过滤掉 null、空串、以及 equalsIgnoreCase("Unknown") 的名字
  • 去重也用 equalsIgnoreCase
  • 入口有 if (!list.isEmpty()) 守卫,且内联构造 ItemOreDict 后还会判 if (!item.ims.isEmpty()),空结果不产出

String[] 分支不内联这道守卫,而是委托给 RecipeUtil.parseOreDictionary(String[])。该重载只守了数组长度:

if (names == null || names.length == 0) return null;
...
return retItem;   // 无条件返回,未检查合并后的 dns/ims 是否为空

所以全部名字都查不到时,它仍返回一个非 null 但 dns/ims 为空的 ItemOreDict;调用处只判 if (item != null),空壳于是被写进 iI。 (单名重载 parseOreDictionary(String) 确实有 items.isEmpty() 守卫并返回 null, 所以单名分支不会出空壳。)不对称就在这里。

细节与完整代码见 成分类型对比 中的专节。

排序方面,OredictInput 为 6 种可能的输入类型各配了一个比较器(ItemStack / String / String[] / Item / List / ItemStack[]),COMPARE_OREDICT_INPUT 先比较"类型序号"再按对应比较器比。

FurnaceRecipe — 熔炼

public record FurnaceRecipe(Item input, Item output) {}

唯一使用 record 语法的类(配 @Desugar 以发布 Java 8 字节码)。它是全 mod 唯一字段名未被缩写的类型:JSON 键是 input / output 而非 iI / o。

数据来源是 FurnaceRecipes.smelting().getSmeltingList() 这个 Map<ItemStack, ItemStack>,按 entrySet() 流式映射,没有排序——导出顺序取决于原版 Map 的迭代顺序。熔炼配方没有"有序"概念,所以不存在形状丢失问题。

差异归因

4 个类型的差别本质上来自它们各自源类型的差别:

  • Shaped / Shapeless 分离,是因为原版就是两个不同的类,网格语义不同
  • OreDictShaped 单独一个,是因为 Forge 的 ShapedOreRecipe 输入是异构多态的(6 种类型),普通 Item 装不下
  • Furnace 单独一个,是因为它根本不是"合成"而是"输入→输出"的映射,没有输入集合
  • GT 配方连包装类体系都没加入这 4 个,而是另起 GregtechMachine / GregtechRecipe 一套(按机器分组,含 eut/dur 等 GT 专有量),见 GT 配方表遍历

相关条目