配方
配方是在 Minecraft 世界内将一组对象转换为其他对象的机制。虽然 Minecraft 只用它转换物品,但该系统的设计允许转换任意类型的对象,包括方块、实体等。几乎所有配方都使用配方数据文件;除非明确说明,本文的“配方”均指数据驱动的配方。
配方数据文件位于 data/<namespace>/recipe/<path>.json。例如,配方 minecraft:diamond_block 位于 data/minecraft/recipe/diamond_block.json。
术语
- 配方 JSON 或配方文件:由
RecipeManager加载并存储的 JSON 文件,包含配方类型、输入、输出,以及处理时间等额外信息。 Recipe:保存所有 JSON 字段在代码中的表示,并包含匹配逻辑(“此输入是否匹配配方?”)和其他一些属性。RecipeInput:向配方提供输入的类型,有多个子类,例如CraftingInput或用于熔炉等场景的SingleRecipeInput。- 配方原料,简称原料:配方的单项输入(而
RecipeInput通常表示需要与配方原料进行匹配的一组输入)。原料系统功能很强,因此有专门的文章介绍。 PlacementInfo:定义配方包含哪些物品,以及它们应占据哪些索引。如果无法根据所提供物品在一定程度上描述配方(例如仅修改数据组件),则使用PlacementInfo#NOT_PLACEABLE。SlotDisplay:定义单个槽位在配方书等配方查看器中的显示方式。RecipeDisplay:定义供配方书等查看器使用的配方SlotDisplay。接口本身只包含配方结果与所用工作站的方法,但子类型可以记录原料、网格大小等信息。RecipeManager:服务端的单例字段,保存所有已加载配方。RecipeSerializer:本质上是MapCodec和StreamCodec的包装,两者均用于序列化。RecipeType:与Recipe对应的已注册类型,主要用于按类型查找配方。通常,不同合成容器应使用不同RecipeType。例如,minecraft:crafting配方类型涵盖minecraft:crafting_shaped、minecraft:crafting_shapeless及特殊合成序列化器。RecipeBookCategory:在配方书中查看配方时,用来表示一组配方的分类。- 配方进度:负责在配方书中解锁配方的进度。它并非必需,玩家通常更倾向使用配方查看模组,因而往往忽略它。不过,配方数据提供器会为你生成它,建议保留。
RecipePropertySet:定义菜单中某个输入槽可以接受的原料列表。RecipeBuilder:在数据生成期间用于创建配方 JSON。- 配方工厂:用于从
RecipeBuilder创建Recipe的方法引用。可以是构造函数引用、静态构建方法,或专门为此创建的函数式接口(通常命名为Factory)。
JSON 规范
配方文件内容随所选类型有很大差异。所有配方文件共同拥有 type 和 neoforge:conditions 属性:
{
// The recipe type. This maps to an entry in the recipe serializer registry.
"type": "minecraft:crafting_shaped",
// A list of data load conditions. Optional, NeoForge-added. See the article linked above for more information.
"neoforge:conditions": [ /*...*/ ]
}
Minecraft 提供的完整类型列表见内置配方类型。模组也可以定义自己的配方类型。
使用配方
配方通过 RecipeManager 类加载、存储和获取。该类可通过 ServerLevel#recipeAccess 获取;没有 ServerLevel 时,可以使用 ServerLifecycleHooks.getCurrentServer()#getRecipeManager。服务端默认不会将配方同步到客户端,而只发送用于限制菜单槽位输入的 RecipePropertySet。此外,当配方在配方书中解锁时,其 RecipeDisplay 和对应的 RecipeDisplayEntry 会发送给客户端(不包括所有 Recipe#isSpecial 返回 true 的配方)。因此,配方逻辑应始终在服务端运行。
最简单的获取方式是使用资源键:
RecipeManager recipes = serverLevel.recipeAccess();
// RecipeHolder<?> is a record of the resource key and the recipe itself.
Optional<RecipeHolder<?>> optional = recipes.byKey(
ResourceKey.create(Registries.RECIPE, Identifier.withDefaultNamespace("diamond_block"))
);
optional.map(RecipeHolder::value).ifPresent(recipe -> {
// Do whatever you want to do with the recipe here. Be aware that the recipe may be of any type.
});
更实用的方法是构造 RecipeInput,并尝试获取匹配配方。本例使用 CraftingInput#of 创建包含一个钻石块的 CraftingInput。这会创建无序输入;有序输入应使用 CraftingInput#ofPositioned;其他输入则使用其他 RecipeInput(例如熔炉配方通常使用 new SingleRecipeInput)。
RecipeManager recipes = serverLevel.recipeAccess();
// Construct a RecipeInput, as required by the recipe. For example, construct a CraftingInput for a crafting recipe.
// The parameters are width, height and items, respectively.
CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK)));
// The generic wildcard on the recipe holder should then extend CraftingRecipe.
// This allows for more type safety later on.
Optional<RecipeHolder<? extends CraftingRecipe>> optional = recipes.getRecipeFor(
// The recipe type to get the recipe for. In our case, we use the crafting type.
RecipeType.CRAFTING,
// Our recipe input.
input,
// Our level context.
serverLevel
);
// This returns the diamond block -> 9 diamonds recipe (unless a datapack changes that recipe).
optional.map(RecipeHolder::value).ifPresent(recipe -> {
// Do whatever you want here. Note that the recipe is now a CraftingRecipe instead of a Recipe<?>.
});
也可以获取所有匹配输入的配方列表,该列表可能为空。合理预期会有多个匹配配方时,这尤其有用:
RecipeManager recipes = serverLevel.recipeAccess();
CraftingInput input = CraftingInput.of(1, 1, List.of(new ItemStack(Items.DIAMOND_BLOCK)));
// These are not Optionals, and can be used directly. However, the list may be empty, indicating no matching recipes.
Stream<RecipeHolder<? extends Recipe<CraftingInput>>> list = recipes.recipeMap().getRecipesFor(
// Same parameters as above.
RecipeType.CRAFTING, input, serverLevel
);
取得正确配方输入后,还需要获取输出,调用 Recipe#assemble 即可:
RecipeManager recipes = serverLevel.recipeAccess();
CraftingInput input = CraftingInput.of(...);
Optional<RecipeHolder<? extends CraftingRecipe>> optional = recipes.getRecipeFor(...);
// Use ItemStack.EMPTY as a fallback.
ItemStack result = optional
.map(RecipeHolder::value)
.map(recipe -> recipe.assemble(input))
.orElse(ItemStack.EMPTY);
必要时也可以遍历某类型的所有配方:
RecipeManager recipes = serverLevel.recipeAccess();
// Like before, pass the desired recipe type.
Collection<RecipeHolder<?>> list = recipes.recipeMap().byType(RecipeType.CRAFTING);
配方优先级
配方有时会相互重叠,通常是因为某个配方图案使用特定物品,而另一个相同图案使用包含该物品的标签。这时原版使用最先找到的配方,具体取决于读取和加载顺序。这可能导致问题:如果特定物品配方在标签配方之后加载,就永远无法取得该特定物品配方。
为解决这一问题,NeoForge 引入配方优先级,以排列应先显示哪些配方。条目表示为从配方注册表键到整数优先级的映射。优先级按从高到低排序,未指定的配方默认为 0。这意味着大于 0 的配方排在前面,小于 0 的配方排在后面。优先级映射位于 data/<namespace>/recipe_priorities.json。所有配方优先级会合并;若 replace 为 true,则清空之前加载的所有条目。
JSON
{
// When true, clears out all previously loaded entries.
"replace": false,
// The map of recipe entries to their priority values.
// If a recipe does not have a priority, it defaults to 0.
"entries": {
// Points to 'data/examplemod/recipe/higher_priority.json'
// This recipe will be checked before any defaults.
"examplemod:higher_priority": 1,
// Points to 'data/examplemod/recipe/lower_priority.json'
// This recipe will be checked after any defaults.
"examplemod:lower_priority": -1,
// Points to 'data/examplemod/recipe/even_lower_priority.json'
// This recipe will be checked after any defaults and the
// 'lower_priority' recipe.
"examplemod:even_lower_priority": -2
}
}
数据生成
// Generates the recipe priorities
public class ExamplePrioritiesProvider extends RecipePrioritiesProvider {
public ExamplePrioritiesProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
// Replace 'examplemod' with your mod id.
super(output, registries, "examplemod");
}
@Override
protected void start() {
// Registers a recipe entry to a priority value.
this.add(
// Points to 'data/examplemod/recipe/higher_priority.json'
ResourceKey.create(Registries.RECIPE, Identifier.fromNamespaceAndPath("examplemod", "higher_priority")),
// This recipe will be checked before any defaults.
1
);
this.add(
// Points to 'data/examplemod/recipe/lower_priority.json'
Identifier.fromNamespaceAndPath("examplemod", "lower_priority"),
// This recipe will be checked after any defaults.
-1
);
this.add(
// Points to 'data/examplemod/recipe/even_lower_priority.json'
// The namespace is inferred from the mod id passed to the provider.
"even_lower_priority",
// This recipe will be checked after any defaults and the 'lower_priority' recipe.
-2
);
}
}
其他配方机制
原版中的一些机制通常被视为配方,但代码实现不同。这通常源于历史原因,或因为这些“配方”是根据标签等其他数据构建的。
警告 配方查看模组通常不会自动识别这些配方,需要手动添加支持。详情请参阅对应模组的文档。
铁砧配方
铁砧有两个输入槽和一个输出槽。原版仅用于工具修复、合并和重命名,这些用途都需要特殊处理,因此没有配方文件。不过,可以通过 AnvilUpdateEvent 扩展该系统。此事件允许获取输入(左侧输入槽)和材料(右侧输入槽),并设置输出物品堆叠、经验消耗和材料消耗数量。也可以通过取消事件阻止整个过程。
// This example allows repairing a stone pickaxe with a full stack of dirt, consuming half the stack, for 3 levels.
@SubscribeEvent // on the game event bus
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() >= 32) {
event.setOutput(new ItemStack(Items.STONE_PICKAXE));
event.setMaterialCost(32);
event.setXpCost(3);
}
}
酿造
扩大合成网格
ShapedRecipePattern 类负责保存有序合成配方的内存表示,硬编码了 3×3 槽位的上限。这妨碍了希望添加更大工作台、同时复用原版有序配方类型的模组。为解决此问题,NeoForge 添加了静态方法 ShapedRecipePattern#setCraftingSize(int width, int height),允许提高上限。应在 FMLCommonSetupEvent 期间调用。各维度采用最大的值;例如,一个模组添加 4×6 工作台,另一个添加 6×5 工作台,最终上限为 6×6。
重要警告
ShapedRecipePattern#setCraftingSize并非线程安全,必须包装在event#enqueueWork调用中。
客户端配方
原版默认不会向逻辑客户端发送任何配方,而是同步 RecipePropertySet/SelectableRecipe.SingleInputSet,确保用户交互时客户端行为正确。此外,在配方书中解锁配方时,会同步其 RecipeDisplay。不过,这两种方式的适用范围有限,尤其在需要配方本身的更多数据时。对于这些情况,NeoForge 提供了将指定 RecipeType 的完整配方发送给客户端的方法。
需要在游戏事件总线监听两个事件:OnDatapackSyncEvent 和 RecipesReceivedEvent。先调用 OnDatapackSyncEvent#sendRecipes,指定要同步到客户端的 RecipeType。然后,通过 RecipesReceivedEvent#getRecipeMap 提供的 RecipeMap 访问配方。此外,玩家退出世界时,应通过 ClientPlayerNetworkEvent.LoggingOut 清除客户端保存的配方。
// Assume we have some custom RecipeType<ExampleRecipe> EXAMPLE_RECIPE_TYPE
@SubscribeEvent // on the game event bus
public static void datapackSync(OnDatapackSyncEvent event) {
// Specify what recipe types to sync to the client
event.sendRecipes(EXAMPLE_RECIPE_TYPE);
}
// In some class only on the physical client
private static final List<RecipeHolder<ExampleRecipe>> EXAMPLE_RECIPES = new ArrayList<>();
@SubscribeEvent // on the game event bus only on the physical client
public static void recipesReceived(RecipesReceivedEvent event) {
// First remove the previous recipes
EXAMPLE_RECIPES.clear();
// Then store the recipes you want
EXAMPLE_RECIPES.addAll(event.getRecipeMap().byType(EXAMPLE_RECIPE_TYPE));
}
@SubscribeEvent // on the game event bus only on the physical client
public static void clientLogOut(ClientPlayerNetworkEvent.LoggingOut event) {
// Clear the stored recipes on world log out
EXAMPLE_RECIPES.clear();
}
警告 如果计划同步自己配方类型的配方,应在两个物理端都处理
OnDatapackSyncEvent。包括单人游戏在内,所有世界都区分服务端与客户端,因此在客户端引用来自服务端的数据包注册表条目很可能导致游戏崩溃。
数据生成
与多数其他 JSON 文件一样,配方可以通过数据生成创建。继承 RecipeProvider 并重写 #buildRecipes,再继承 RecipeProvider.Runner,将它传给数据生成器:
public class MyRecipeProvider extends RecipeProvider {
// Construct the provider to run
protected MyRecipeProvider(HolderLookup.Provider provider, RecipeOutput output) {
super(provider, output);
}
@Override
protected void buildRecipes() {
// Add your recipes here.
}
// The runner to add to the data generator
public static class Runner extends RecipeProvider.Runner {
// Get the parameters from the `GatherDataEvent`s.
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 // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
// Call event.createDatapackRegistryObjects(...) first if adding datapack objects
event.createProvider(MyRecipeProvider.Runner::new);
}
配方提供器还提供常见场景的辅助方法,例如 twoByTwoPacker(2×2 方块配方)、threeByThreePacker(3×3 方块配方)或 nineBlockStorageRecipes(3×3 方块配方和将一个方块拆成九个物品的配方)。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。