配方(Recipes)
配方是在Minecraft世界中将一组对象转换为其他对象的一种方式。尽管Minecraft仅将此系统用于物品转换,但该系统的构建方式允许任何类型的对象(方块、实体等)进行转换。几乎所有配方都使用配方数据文件;除非另有明确说明,否则本文中的"配方"假定为数据驱动的配方。
配方数据文件位于data/<命名空间>/recipe/<路径>.json。例如,配方minecraft:diamond_block位于data/minecraft/recipe/diamond_block.json。
术语(Terminology)
- 配方JSON或配方文件是由
RecipeManager加载和存储的JSON文件。它包含配方类型、输入和输出以及附加信息(例如处理时间)等信息。 - **
Recipe**保存所有JSON字段的代码表示,以及匹配逻辑("此输入是否匹配配方?")和其他一些属性。 - **
RecipeInput**是向配方提供输入的类型。有几个子类,例如CraftingInput或SingleRecipeInput(用于熔炉和类似容器)。 - 配方原料或简称原料是配方的单个输入(而
RecipeInput通常表示要检查配方原料的输入集合)。原料是一个非常强大的系统,因此在其自己的文章中概述。 - **
PlacementInfo**是配方包含的物品以及它们应填充的索引的定义。如果配方无法根据提供的物品在一定程度上捕获(例如,仅更改数据组件),则使用PlacementInfo#NOT_PLACEABLE。 - **
SlotDisplay**定义单个槽位在配方查看器(如配方书)中的显示方式。 - **
RecipeDisplay**定义配方查看器(如配方书)使用的配方的SlotDisplays。虽然接口仅包含配方结果和配方执行的工作站的方法,但子类型可以捕获原料或网格大小等信息。 - **
RecipeManager**是服务器上保存所有加载配方的单例字段。 - **
RecipeSerializer**基本上是围绕MapCodec和StreamCodec的包装器,两者都用于序列化。 - **
RecipeType**是Recipe的注册类型等效项。主要在按类型查找配方时使用。根据经验,不同的合成容器应使用不同的RecipeTypes。例如,minecraft:crafting配方类型涵盖minecraft:crafting_shaped和minecraft:crafting_shapeless配方序列化器,以及特殊合成序列化器。 - **
RecipeBookCategory**是通过配方书查看时表示某些配方的组。 - **配方进度**是负责在配方书中解锁配方的进度。它们不是必需的,通常被玩家忽略,而倾向于使用配方查看器模组,但是配方数据提供程序会为您生成它们,因此建议直接使用。
- **
RecipePropertySet**定义菜单中定义的输入槽位可以接受的可用原料列表。 - **
RecipeBuilder**在数据生成期间用于创建JSON配方。 - 配方工厂是用于从
RecipeBuilder创建Recipe的方法引用。它可以是构造函数的引用,或静态构建器方法,或专门为此目的创建的函数式接口(通常名为Factory)。
JSON规范(JSON Specification)
配方文件的内容根据所选类型而有很大差异。所有配方文件共有的属性是type和neoforge:conditions:
{
// 配方类型。这映射到配方序列化器注册表中的条目。
"type": "minecraft:crafting_shaped",
// 数据加载条件列表。可选,NeoForge添加。有关更多信息,请参见上面链接的文章。
"neoforge:conditions": [ /*...*/ ]
}
Minecraft提供的完整类型列表可以在内置配方类型文章中找到。模组也可以定义自己的配方类型。
使用配方(Using Recipes)
配方通过RecipeManager类加载、存储和获取,而RecipeManager又通过ServerLevel#recipeAccess获取,或者如果没有可用的ServerLevel,则通过ServerLifecycleHooks.getCurrentServer()#getRecipeManager获取。服务器默认不同步配方到客户端,而是仅发送RecipePropertySets以限制菜单槽位的输入。此外,每当配方在配方书中解锁时,其RecipeDisplays和相应的RecipeDisplayEntrys会发送到客户端(不包括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,其他输入将使用其他RecipeInputs(例如,熔炉配方通常使用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);
如果需要,也可以遍历某种类型的所有配方。这是这样完成的:
RecipeManager recipes = serverLevel.recipeAccess();
// 与之前一样,传递所需的配方类型。
Collection<RecipeHolder<?>> list = recipes.recipeMap().byType(RecipeType.CRAFTING);
其他配方机制(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(Items.STONE_PICKAXE);
event.setMaterialCost(32);
event.setCost(3);
}
}
酿造(Brewing)
请参阅生物效果与药水文章中的酿造章节。