跳到主要内容
版本:1.21.5

配方(Recipes)

配方(Recipes)是在Minecraft世界中将一组对象转换为其他对象的方式。虽然Minecraft纯粹将此系统用于物品转换,但该系统的构建方式允许任何类型的对象 - 方块、实体等 - 被转换。几乎所有配方都使用配方数据文件;除非另有明确说明,本文中的"配方"假定为数据驱动的配方。

配方数据文件位于data/<namespace>/recipe/<path>.json。例如,配方minecraft:diamond_block位于data/minecraft/recipe/diamond_block.json

术语

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

JSON规范

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

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

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

使用配方

配方通过RecipeManager类加载、存储和获取,而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)));
// 这些不是Optionals,可以直接使用。但是,列表可能为空,表示没有匹配的配方。
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);

数据生成

配方可以通过数据生成(data generation)自动生成。这可以确保所有配方都有适当的条目,并减少手动工作。

// 在数据生成类中
@Override
protected void buildRecipes(RecipeOutput output) {
ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.DIAMOND_BLOCK)
.pattern("DDD")
.pattern("DDD")
.pattern("DDD")
.define('D', Items.DIAMOND)
.unlockedBy("has_diamond", has(Items.DIAMOND))
.save(output);
}

最佳实践

  1. 使用适当的配方类型:根据配方的性质选择正确的配方类型。
  2. 提供进度解锁:为配方提供进度解锁条件。
  3. 使用数据生成:自动生成配方以减少错误。
  4. 测试配方:确保配方在不同条件下正确工作。
  5. 考虑平衡性:确保配方与其他配方平衡。

通过遵循这些指南,你可以有效地在模组中创建和使用配方。