配方(Recipes)
配方(Recipe)是在 Minecraft 世界中将一组对象转化为其他对象的一种方式。虽然 Minecraft 本身主要用这个系统来实现物品(item)之间的转化,但这个系统的设计允许任意类型的对象——比如方块(block)、实体(entity)等——进行转化。几乎所有的配方都使用配方数据文件;在本文中,除非特别说明,“配方”指的都是数据驱动的配方。
配方数据文件位于 data/<namespace>/recipe/<path>.json 目录下。例如,minecraft:diamond_block 这个配方的数据文件路径为 data/minecraft/recipe/diamond_block.json。
术语(Terminology)
- 配方 JSON 或 配方文件,指的是由
RecipeManager加载和存储的 JSON 文件。它包含配方类型、输入与输出,以及其他附加信息(例如处理时间)。 Recipe是对所有 JSON 字段的代码内表示,包含匹配逻辑(“这个输入是否匹配该配方?”)以及其他属性。RecipeInput是为配方提供输入的类型。有多种子类,例如CraftingInput或SingleRecipeInput(用于熔炉等类似场景)。- 配方成分(ingredient),或简称 成分,指的是配方中的单个输入(而
RecipeInput通常表示需要与配方成分进行匹配的一组输入)。成分系统非常强大,因此被单独介绍在专门的文章中。 PlacementInfo用于定义配方所包含的物品以及这些物品应填充在哪些索引位置。如果无法根据提供的物品捕获配方结构(例如,仅更改数据组件时),则会使用PlacementInfo#NOT_PLACEABLE。SlotDisplay定义了在配方查看器(如配方书)中单个格子的显示方式。RecipeDisplay定义了一个配方的所有SlotDisplay,以供配方查看器(如配方书)使用。虽然接口只包含配方结果和执行配方所用工作台的方法,但其子类型可以捕获如成分或网格大小等信息。RecipeManager是服务器端的单例字段,负责保存所有已加载的配方。RecipeSerializer本质上是MapCodec和StreamCodec的包装器,两者都用于序列化。RecipeType是注册的配方类型,等价于Recipe。主要用于按类型查找配方。一般来说,不同的合成容器应使用不同的RecipeType。例如,minecraft:crafting配方类型涵盖了minecraft:crafting_shaped和minecraft:crafting_shapeless配方序列化器,以及特殊的合成序列化器。RecipeBookCategory是在配方书中查看配方时用于分组的类别。- 配方进度(advancement),即解锁配方时的进度系统。虽然不是必需的,并且玩家通常更喜欢使用配方查看器模组而忽略它,但配方数据生成器会为你自动生成,因此推荐直接使用它。
RecipePropertySet用于定义菜单中某个输入槽可以接受的成分列表。RecipeBuilder用于数据生成(datagen)期间创建 JSON 配方文件。- 配方工厂(recipe factory) 是一个方法引用,用于通过
RecipeBuilder创建一个Recipe。它可以是构造函数的引用、静态构造方法,或者专门为此目的创建的函数式接口(通常命名为Factory)。
JSON 规范(JSON Specification)
配方文件的内容会根据所选类型有很大不同。但所有配方文件都包含 type 和 neoforge:conditions 属性:
{
// 配方类型。这会映射到配方序列化器注册表(recipe serializer registry)中的一个条目。
"type": "minecraft:crafting_shaped",
// 数据加载条件列表。可选项,由 NeoForge 添加。详细信息见上方链接的文章。
"neoforge:conditions": [ /*...*/ ]
}
Minecraft 提供的所有类型的完整列表可见 内置配方类型(Built-In Recipe Types)文章。模组也可以 定义自己的配方类型。
使用配方(Using Recipes)
配方的加载、存储和获取都通过 RecipeManager 类完成。你可以通过 ServerLevel#recipeAccess 获取该类,或者——如果没有 ServerLevel 可用——可以通过 ServerLifecycleHooks.getCurrentServer()#getRecipeManager 获取。服务器默认不会将配方同步到客户端,而是只发送 RecipePropertySet,用于限制菜单栏槽位的输入。此外,每当某个配方被解锁用于配方书时,其 RecipeDisplay 及对应的 RecipeDisplayEntry 会发送到客户端(所有 Recipe#isSpecial 返回 true 的配方除外)。因此,配方逻辑应始终在服务器端运行。
获取配方最简单的方法是通过其资源键(resource key):
RecipeManager recipes = serverLevel.recipeAccess();
// RecipeHolder<?> 是资源键与配方本身的记录(record)。
Optional<RecipeHolder<?>> optional = recipes.byKey(
ResourceKey.create(Registries.RECIPE, ResourceLocation.withDefaultNamespace("diamond_block"))
);
optional.map(RecipeHolder::value).ifPresent(recipe -> {
// 在这里可以对获取到的配方进行任意操作。注意:配方类型可能是任意的。
});
更实用的方法是构造一个 RecipeInput 并尝试获取匹配的配方。在下面的示例中,我们将使用 CraftingInput#of 创建一个包含一个钻石块的 CraftingInput。这将创建一个无序输入(shapeless input);若要创建有序输入(shaped input),应使用 CraftingInput#ofPositioned,其他类型输入则使用其它 RecipeInput(例如,熔炉配方通常会使用 new SingleRecipeInput)。
RecipeManager recipes = serverLevel.recipeAccess();
// 构造一个 RecipeInput(配方输入),具体类型取决于配方需求。比如对于合成配方(crafting recipe),需要构造一个 CraftingInput。
// 参数依次为宽度、高度和物品列表。
CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK)));
// 配方持有者(recipe holder)上的泛型通配符应当继承自 CraftingRecipe。
// 这样做可以在后续操作中获得更好的类型安全性。
Optional<RecipeHolder<? extends CraftingRecipe>> optional = recipes.getRecipeFor(
// 要获取配方的类型,这里我们使用合成(crafting)类型。
RecipeType.CRAFTING,
// 我们的配方输入。
input,
// 当前的世界(level)上下文。
serverLevel
);
// 这会返回钻石块 → 9 颗钻石的合成配方(除非有数据包修改了该配方)。
optional.map(RecipeHolder::value).ifPresent(recipe -> {
// 在这里可以执行你想要的操作。注意,此时 recipe 是 CraftingRecipe 类型,而不是 Recipe<?>。
});
另外,你也可以获取一个可能为空的配方列表,这在可能有多个配方匹配输入的情况下尤其有用:
RecipeManager recipes = serverLevel.recipeAccess();
CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK)));
// 这里返回的不是 Optional,可以直接使用。不过,列表可能为空,表示没有匹配的配方。
Stream<RecipeHolder<? extends Recipe<CraftingInput>>> list = recipes.recipeMap().getRecipesFor(
// 参数和上面相同。
RecipeType.CRAFTING, input, serverLevel
);
当我们得到了正确的配方输入后,通常还需要获取配方输出。可以通过调用 Recipe#assemble 方法实现:
RecipeManager recipes = serverLevel.recipeAccess();
CraftingInput input = CraftingInput.of(...);
Optional<RecipeHolder<? extends CraftingRecipe>> optional = recipes.getRecipeFor(...);
// 使用 ItemStack.EMPTY 作为兜底返回值。
ItemStack result = optional
.map(RecipeHolder::value)
.map(recipe -> recipe.assemble(input, serverLevel.registryAccess()))
.orElse(ItemStack.EMPTY);
如有需要,也可以遍历某一类型的所有配方,方法如下:
RecipeManager recipes = serverLevel.recipeAccess();
// 和之前一样,传入你想要的配方类型。
Collection<RecipeHolder<?>> list = recipes.recipeMap().byType(RecipeType.CRAFTING);
其他配方机制(Other Recipe Mechanisms)
在原版 Minecraft 中,有些机制通常被认为是配方(recipe),但在代码中实现方式不同。这通常是由于历史原因,或者这些“配方”是由其他数据(比如 标签)动态生成的。
配方查看类模组通常无法自动识别这 些配方。要支持这些模组,需要手动适配,具体请参考对应模组的文档。
铁砧配方(Anvil Recipes)
铁砧(Anvil)有两个输入槽和一个输出槽。原版(vanilla)的用途仅限于工具修复、合成以及重命名。由于这些用例都需要特殊处理,因此并没有提供配方文件。不过,你可以通过 AnvilUpdateEvent 来扩展这个系统。这个 事件 允许你获取输入物品(左输入槽)和材料(右输入槽),并且可以设置输出的物品堆栈(item stack)、所需经验值以及消耗的材料数量。你还可以通过 取消 事件来阻止整个过程。
// 这个示例允许用一整组泥土修复一把石镐,消耗一半泥土,并花费 3 级经验。
@SubscribeEvent
public static void onAnvilUpdate(AnvilUpdateEvent event) {
ItemStack left = event.getLeft();
ItemStack right = event.getRight();
if (left.is(Items.STONE_PICKAXE) && right.is(Items.DIRT) && right.getCount() >= 64) {
event.setOutput(Items.STONE_PICKAXE);
event.setMaterialCost(32);
event.setCost(3);
}
}
酿造(Brewing)
详见 生物效果与药水(Mob Effects & Potions)文章中的酿造章节。
扩展合成网格大小(Extending the Crafting Grid Size)
负责保存有序合成配方(shaped crafting recipes)内存表示的 ShapedRecipePattern 类,其槽位数量被硬编码为 3x3,这对于希望在复用原版有序合成配方类型的同时增加更大合成台的模组来说是个障碍。为了解决这个问题,NeoForge 补丁加入了一个静态方法:ShapedRecipePattern#setCraftingSize(int width, int height),允许你提升这个限制。你应该在 FMLCommonSetupEvent 期间调用它。这里以最大值为准,比如如果有一个模组添加了 4x6 的合成台,另一个添加了 6x5 的合成台,最终合成网格会变成 6x6。
ShapedRecipePattern#setCraftingSize 不是线程安全的。必须包裹在 event#enqueueWork 调用中。
数据生成(Data Generation)
和大多数其他 JSON 文件一样,配方也可以通过数据生成(datagen)自动生成。对于配方,你需要继承 RecipeProvider 类并重写 #buildRecipes 方法, 同时继承 RecipeProvider.Runner 类以便传递给数据生成器:
public class MyRecipeProvider extends RecipeProvider {
// 构造用于运行的数据提供器
protected MyRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) {
super(provider, output);
}
@Override
protected void buildRecipes() {
// 在这里添加你的合成配方。
}
// 添加到数据生成器的运行器
public static class Runner extends RecipeProvider.Runner {
// 从 `GatherDataEvent` 获取参数。
public Runner(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider) {
super(output, lookupProvider);
}
@Override
protected abstract RecipeProvider createRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) {
return new MyRecipeProvider(provider, output);
}
}
}
需要注意的是 RecipeOutput 参数。Minecraft 会使用这个对象自动为你生成一个合成配方进度(advancement)。除此之外,NeoForge 还为 RecipeOutput 注入了 条件(conditions) 支持,可以通过 #withConditions 方法调用。
合成配方本身通常通过 RecipeBuilder 的子类添加。所有原版合成配方构建器(vanilla recipe builders)在本文中不做详细介绍(它们在 内置合成配方类型(Built-In Recipe Types) 一文中有详细说明),而如何创建你自己的构建器则在 自定义合成配方页面 有讲解。
和所有其他数据提供器一样,合成配方提供器(recipe providers)也必须像下面这样注册到 GatherDataEvent:
@SubscribeEvent
public static void gatherData(GatherDataEvent.Client event) {
// 如果要添加数据包对象,先调用 event.createDatapackRegistryObjects(...)
event.createProvider(MyRecipeProvider.Runner::new);
}
合成配方提供器还为常见场景提供了一些辅助方法,例如 twoByTwoPacker(用于 2x2 格子的合成配方)、threeByThreePacker(用于 3x3 格子的合成配方)或 nineBlockStorageRecipes(用于 3x3 格子的合成配方,以及 1 个方块合成 9 个物品的配方)。