跳到主要内容
版本:1.21.6 - 1.21.8

配方(Recipes)

配方是一种在 Minecraft 世界中将一组对象转化为其他对象的方式。尽管 Minecraft 纯粹将此系统用于物品转化,但该系统的构建方式允许任何类型的对象——方块、实体等——被转化。几乎所有配方都使用配方数据文件;除非明确说明,否则本文中的“配方”假定为数据驱动的配方。

配方数据文件位于 data/<命名空间>/recipe/<路径>.json。例如,配方 minecraft:diamond_block 位于 data/minecraft/recipe/diamond_block.json

术语(Terminology)

  • 配方 JSON配方文件 是由 RecipeManager 加载和存储的 JSON 文件。它包含诸如配方类型、输入和输出以及附加信息(例如处理时间)等内容。
  • Recipe 保存所有 JSON 字段的代码内表示,以及匹配逻辑(“此输入是否匹配配方?”)和一些其他属性。
  • RecipeInput 是一种向配方提供输入的类型。有几个子类,例如 CraftingInputSingleRecipeInput(用于熔炉等)。
  • 配方成分成分 是配方的单个输入(而 RecipeInput 通常代表要对照配方成分检查的输入集合)。成分是一个非常强大的系统,因此在 它们自己的文章 中进行了概述。
  • PlacementInfo 是配方所包含物品以及它们应填充的索引的定义。如果无法根据提供的物品在一定程度上捕获配方(例如,仅更改数据组件),则使用 PlacementInfo#NOT_PLACEABLE
  • SlotDisplay 定义单个槽位在配方查看器(如配方书)中应如何显示。
  • RecipeDisplay 定义配方的 SlotDisplay 以供配方查看器(如配方书)使用。虽然该接口仅包含配方结果和进行配方的工作站的方法,但子类型可以捕获诸如成分或网格大小等信息。
  • RecipeManager 是服务器上的一个单例字段,保存所有加载的配方。
  • RecipeSerializer 基本上是 MapCodecStreamCodec 的包装器,两者都用于序列化。
  • RecipeTypeRecipe 的注册类型等效物。它主要用于按类型查找配方。根据经验法则,不同的制作容器应使用不同的 RecipeType。例如,minecraft:crafting 配方类型涵盖 minecraft:crafting_shapedminecraft:crafting_shapeless 配方序列化器,以及特殊合成序列化器。
  • RecipeBookCategory 是在通过配方书查看时代表某些配方的分组。
  • 配方 进度 是负责在配方书中解锁配方的进度。它们不是必需的,并且通常被玩家忽视,而倾向于配方查看器模组,但是 配方数据提供器 会为你生成它们,因此建议直接使用。
  • RecipePropertySet 定义菜单中定义的输入槽位可以接受的可用成分列表。
  • RecipeBuilder 在数据生成期间用于创建 JSON 配方。
  • 配方工厂 是用于从 RecipeBuilder 创建 Recipe 的方法引用。它可以是对构造函数的引用,也可以是静态构建器方法,或者是专门为此目的创建的函数式接口(通常命名为 Factory)。

JSON 规范(JSON Specification)

配方文件的内容根据所选类型而有很大差异。所有配方文件共有的部分是 typeneoforge:conditions 属性:

{
// 配方类型。这映射到配方序列化器注册表中的一个条目。
"type": "minecraft:crafting_shaped",
// 数据加载条件列表。可选,NeoForge 添加。有关详细信息,请参阅上面的文章。
"neoforge:conditions": [ /*...*/ ]
}

Minecraft 提供的类型的完整列表可以在 内置配方类型文章 中找到。模组也可以 定义自己的配方类型

使用配方(Using Recipes)

配方通过 RecipeManager 类加载、存储和获取,而该类又通过 ServerLevel#recipeAccess 获取,或者——如果你没有 ServerLevel 可用——通过 ServerLifecycleHooks.getCurrentServer()#getRecipeManager 获取。服务器默认不会将配方同步到客户端,而是只发送用于限制菜单槽位输入的 RecipePropertySet。此外,每当配方在配方书中被解锁时,其 RecipeDisplay 和相应的 RecipeDisplayEntry 会被发送到客户端(不包括所有 Recipe#isSpecial 返回 true 的配方)。因此,配方逻辑应始终在服务器上运行。

获取配方的最简单方法是按其资源键:

RecipeManager recipes = serverLevel.recipeAccess();
// RecipeHolder<?> 是资源键和配方本身的记录。
Optional<RecipeHolder<?>> optional = recipes.byKey(
ResourceKey.create(Registries.RECIPE, ResourceLocation.withDefaultNamespace("diamond_block"))
);
optional.map(RecipeHolder::value).ifPresent(recipe -> {
// 在此处对配方执行任何你想要的操作。请注意,配方可能是任何类型。
});

更实用的方法是构造一个 RecipeInput 并尝试获取匹配的配方。在此示例中,我们将使用 CraftingInput#of 创建一个包含一个钻石块的 CraftingInput。这将创建一个无序输入,有序输入将使用 CraftingInput#ofPositioned,其他输入将使用其他 RecipeInput(例如,熔炉配方通常使用 new SingleRecipeInput)。

RecipeManager recipes = serverLevel.recipeAccess();
// 构造 RecipeInput,如配方所需。例如,为合成配方构造 CraftingInput。
// 参数分别是宽度、高度和物品。
CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK)));
// 配方持有器上的泛型通配符应扩展 CraftingRecipe。
// 这允许以后更多的类型安全性。
Optional<RecipeHolder<? extends CraftingRecipe>> optional = recipes.getRecipeFor(
// 要获取配方的配方类型。在我们的例子中,我们使用合成类型。
RecipeType.CRAFTING,
// 我们的配方输入。
input,
// 我们的世界上下文。
serverLevel
);
// 这将返回钻石块 -> 9 个钻石的配方(除非数据包更改了该配方)。
optional.map(RecipeHolder::value).ifPresent(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);

配方优先级(Recipe Priorities)

有时,配方可能会与其他配方重叠,通常是因为一个图案使用特定物品,而另一个相同图案使用包含该物品的标签。在这些情况下,原版使用它找到的第一个配方,这由首先读取和加载的配方决定。这可能是个问题,因为如果特定物品配方在基于标签的配方之后加载,那么特定物品配方可能永远无法获取。

为了解决这个问题,NeoForge 引入了配方优先级来排序应首先显示哪些配方。条目表示为配方注册表键到整数优先级值的映射。优先级值基于最高值排序,未指定的配方默认为 0。这意味着优先级大于 0 的配方先排序,而优先级小于 0 的配方后排序。优先级映射位于 data/<命名空间>/recipe_priorities.json 中,其中所有配方优先级都合并在一起,除非 replace 为 true,这将清除所有先前加载的条目。

{
// 当为 true 时,清除所有先前加载的条目。
"replace": false,
// 配方条目到其优先级值的映射。
// 如果配方没有优先级,则默认为 0。
"entries": {
// 指向 'data/examplemod/recipe/higher_priority.json'
// 此配方将在任何默认值之前被检查。
"examplemod:higher_priority": 1,
// 指向 'data/examplemod/recipe/lower_priority.json'
// 此配方将在任何默认值之后被检查。
"examplemod:lower_priority": -1,
// 指向 'data/examplemod/recipe/even_lower_priority.json'
// 此配方将在任何默认值以及 'lower_priority' 配方之后被检查。
"examplemod:even_lower_priority": -2
}
}

其他配方机制(Other Recipe Mechanisms)

原版中的一些机制通常被认为是配方,但在代码中以不同方式实现。这通常是由于历史原因,或者因为“配方”是从其他数据(例如 标签)构建的。

注意

配方查看器模组通常不会拾取这些配方。必须手动添加对这些模组的支持,请参阅相应模组的文档以获取更多信息。

铁砧配方(Anvil Recipes)

铁砧有两个输入槽和一个输出槽。唯一的原版用例是工具修复、合并和重命名,由于每个用例都需要特殊处理,因此没有提供配方文件。但是,可以使用 AnvilUpdateEvent 来构建该系统。此 事件 允许获取输入(左侧输入槽)和材料(右侧输入槽),并允许设置输出物品堆叠,以及经验成本和要消耗的材料数量。也可以通过 取消 事件来完全阻止该过程。

// 此示例允许用一整组泥土修复石镐,消耗半组,消耗 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(new ItemStack(Items.STONE_PICKAXE));
event.setMaterialCost(32);
event.setXpCost(3);
}
}

酿造(Brewing)

参见 生物效果与药水文章中的酿造章节

扩展合成网格大小(Extending the Crafting Grid Size)

ShapedRecipePattern 类负责保存有序合成配方的内存中表示,它有一个硬编码的 3x3 槽位限制,阻碍了想要在重用原版有序合成配方类型的同时添加更大合成台的模组。为了解决这个问题,NeoForge 修补了一个静态方法,称为 ShapedRecipePattern#setCraftingSize(int width, int height),允许增加限制。应在 FMLCommonSetupEvent 期间调用它。最大的值在这里胜出,因此,例如,如果一个模组添加了 4x6 合成台,另一个添加了 6x5 合成台,则结果值将为 6x6。

危险

ShapedRecipePattern#setCraftingSize 不是线程安全的。必须将其包装在 event#enqueueWork 调用中。

客户端配方(Client-Side Recipes)

默认情况下,原版不会向 逻辑客户端 发送任何配方。相反,同步 RecipePropertySet / SelectableRecipe.SingleInputSet 以处理用户交互期间适当的客户端行为。此外,当配方在配方书中解锁时,其 RecipeDisplay 会同步。但是,这两种情况的范围有限,尤其是当需要从配方本身获取更多数据时。在这些情况下,NeoForge 提供了一种将给定 RecipeType 的完整配方发送到客户端的方法。

必须在 [游戏事件总线][events] 上监听两个事件:OnDatapackSyncEventRecipesReceivedEvent。首先,通过调用 OnDatapackSyncEvent#sendRecipes 指定要同步到客户端的 RecipeType。然后,可以通过 RecipesReceivedEvent#getRecipeMap 从提供的 RecipeMap 中访问配方。此外,一旦玩家退出世界,应通过 ClientPlayerNetworkEvent.LoggingOut 清除存储在客户端上的任何配方。

// 假设我们有一些自定义的 RecipeType<ExampleRecipe> EXAMPLE_RECIPE_TYPE

@SubscribeEvent // 在游戏事件总线上
public static void datapackSync(OnDatapackSyncEvent event) {
// 指定要同步到客户端的配方类型
event.sendRecipes(EXAMPLE_RECIPE_TYPE);
}

// 仅在物理客户端的某个类中

private static final List<RecipeHolder<ExampleRecipe>> EXAMPLE_RECIPES = new ArrayList<>();

@SubscribeEvent // 仅在物理客户端的游戏事件总线上
public static void recipesReceived(RecipesReceivedEvent event) {
// 首先移除先前的配方
EXAMPLE_RECIPES.clear();

// 然后存储你想要的配方
EXAMPLE_RECIPES.addAll(event.getRecipeMap().byType(EXAMPLE_RECIPE_TYPE));
}

@SubscribeEvent // 仅在物理客户端的游戏事件总线上
public static void clientLogOut(ClientPlayerNetworkEvent.LoggingOut event) {
// 在退出世界时清除存储的配方
EXAMPLE_RECIPES.clear();
}
注意

如果你计划为你的配方类型同步配方,则应在物理两端调用 OnDatapackSyncEvent。所有世界,包括单人游戏,都有服务器和客户端之间的划分,这意味着从客户端引用服务器的数据包注册表条目很可能会导致游戏崩溃。

数据生成(Data Generation)

与大多数其他 JSON 文件一样,配方可以数据生成。对于配方,我们希望扩展 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 RecipeProvider createRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) {
return new MyRecipeProvider(provider, output);
}
}
}

值得注意的是 RecipeOutput 参数。Minecraft 使用此对象自动为你生成配方进度。除此之外,NeoForge 将 条件 支持注入到 RecipeOutput 中,可以通过 #withConditions 调用。

配方本身通常通过 RecipeBuilder 的子类添加。列出所有原版配方构建器超出了本文的范围(它们在 内置配方类型文章 中解释),但是创建你自己的构建器在 自定义配方页面 中解释。

与所有其他数据提供器一样,配方提供器必须注册到 GatherDataEvent,如下所示:

@SubscribeEvent // 在模组事件总线上
public static void gatherData(GatherDataEvent.Client event) {
// 如果添加数据包对象,首先调用 event.createDatapackRegistryObjects(...)

event.createProvider(MyRecipeProvider.Runner::new);
}

配方提供器还为常见场景添加了辅助方法,例如 twoByTwoPacker(用于 2x2 方块配方)、threeByThreePacker(用于 3x3 方块配方)或 nineBlockStorageRecipes(用于 3x3 方块配方和 1 个方块到 9 个物品的配方)。