自定义配方
添加自定义配方至少需要三部分:Recipe、RecipeType 和 RecipeSerializer。根据实现内容,如果无法复用现有子类,还可能需要自定义 RecipeInput、RecipeDisplay、SlotDisplay、RecipeBookCategory 和 RecipePropertySet。
为了演示不同特性,下面实现一种由配方驱动的机制:使用特定物品右键点击世界中的某个 BlockState,破坏该方块状态,并掉落结果物品。
配方输入
先定义要向配方提供什么。配方输入表示玩家此刻实际使用的输入,这一点很重要。因此这里不用标签或原料,而是使用实际存在的物品堆叠和方块状态。
// Our inputs are a BlockState and an ItemStack.
public record RightClickBlockInput(BlockState state, ItemStack stack) implements RecipeInput {
// Method to get an item from a specific slot. We have one stack and no concept of slots, so we just assume
// that slot 0 holds our item, and throw on any other slot. (Taken from SingleRecipeInput#getItem.)
@Override
public ItemStack getItem(int slot) {
if (slot != 0) throw new IllegalArgumentException("No item for index " + slot);
return this.stack();
}
// The slot size our input requires. Again, we don't really have a concept of slots, so we just return 1
// because we have one item stack involved. Inputs with multiple items should return the actual count here.
@Override
public int size() {
return 1;
}
}
配方输入按需创建,无需注册或以任何方式序列化。也不总是需要自定义输入,原版的 CraftingInput、SingleRecipeInput 和 SmithingRecipeInput 已适合许多用途。
配方类
有了输入,接着实现配方本身。它保存配方数据,处理匹配,并返回配方结果,因此通常是自定义配方中最长的类。
// The generic parameter for Recipe<T> is our RightClickBlockInput from above.
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// An in-code representation of our recipe data. This can be basically anything you want.
// Common things to have here is a processing time integer of some kind, or an experience reward.
// Note that we now use an ingredient instead of an item stack for the input.
private final Recipe.CommonInfo commonInfo;
private final RightClickBlockRecipe.BlockBookInfo bookInfo;
private final BlockState inputState;
private final Ingredient inputItem;
private final ItemStackTemplate result;
// Add a constructor that sets all properties.
public RightClickBlockRecipe(Recipe.CommonInfo commonInfo, RightClickBlockRecipe.BlockBookInfo bookInfo, BlockState inputState, Ingredient inputItem, ItemStackTemplate result) {
this.commonInfo = commonInfo;
this.bookInfo = bookInfo;
this.inputState = inputState;
this.inputItem = inputItem;
this.result = result;
}
// Check whether the given input matches this recipe. The first parameter matches the generic.
// We check our blockstate and our item stack, and only return true if both match.
// If we needed to check the dimensions of our input, we would also do so here.
@Override
public boolean matches(RightClickBlockInput input, Level level) {
return this.inputState == input.state() && this.inputItem.test(input.stack());
}
// Return the result of the recipe here, based on the given input. The parameter matches the generic.
// This can be created using `ItemStackTemplate#create`.
@Override
public ItemStack assemble(RightClickBlockInput input) {
return this.result.create();
}
// When true, will prevent the recipe from being synced within the recipe book or awarded on use/unlock.
// This should only be true if the recipe shouldn't appear in a recipe book, such as map extending.
// Although this recipe takes in an input state, it could still be used in a custom recipe book using
// the methods below.
@Override
public boolean isSpecial() {
return true;
}
// This example outlines the most important methods. There is a number of other methods to override.
// Some methods will be explained in the below sections as they cannot be easily compressed and understood here.
// Check the class definition of Recipe to view them all.
}
通用信息
所有配方都有从 JSON 解析的通用信息,不过实现方式可能不同。原版为所有配方提供了 Recipe.CommonInfo。目前,它允许配方指定解锁时是否显示通知提示。CommonInfo 还提供 map codec 和 stream codec,以便与下文的 RecipeSerializer 整合。
因此,可以使用 CommonInfo 实现 Recipe 的一些方法:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
private final Recipe.CommonInfo commonInfo;
// Other fields
public RightClickBlockRecipe(Recipe.CommonInfo commonInfo, ...) {
this.commonInfo = commonInfo;
// Other initializations
}
@Override
public boolean showNotification() {
return this.commonInfo.showNotification();
}
// Other methods
}
提示 不强制使用
CommonInforecord,也不强制在 JSON 中提供show_notification字段。模组开发者可以自行决定是否适合使用。
配方书信息
类似通用信息,还有一些从 JSON 解析的字段与配方书有关。配方书是显示在工作台、熔炉等转换菜单中的配方 GUI。原版为这些字段提供 Recipe.BookInfo<CategoryType> 接口,其中 CategoryType 表示配方分类本身(假定它可以序列化),或可转换为分类的中间可序列化对象。与 CommonInfo 一样,它提供 map codec 和 stream codec,以便与下文的 RecipeSerializer 整合。
例如:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
private final RightClickBlockRecipe.BlockBookInfo bookInfo;
// Other fields
public RightClickBlockRecipe(RightClickBlockRecipe.BlockBookInfo bookInfo, ...) {
this.bookInfo = bookInfo;
// Other initializations
}
@Override
public String group() {
return this.bookInfo.group();
}
@Override
public RecipeBookCategory recipeBookCategory() {
// Convert the serializable entry to its recipe book category.
return switch (this.bookInfo.category()) {
case BUILDING -> RecipeBookCategories.CRAFTING_BUILDING_BLOCKS;
case EQUIPMENT -> RecipeBookCategories.CRAFTING_EQUIPMENT;
case REDSTONE -> RecipeBookCategories.CRAFTING_REDSTONE;
case MISC -> RecipeBookCategories.CRAFTING_MISC;
};
}
// Other methods
public record BlockBookInfo(CraftingBookCategory category, String group) implements Recipe.BookInfo<CraftingBookCategory> {
public static final MapCodec<BlockBookInfo> MAP_CODEC = Recipe.BookInfo.mapCodec(
// Takes in the codec for the generic, the default generic value, and the
// constructor of `(category, group) -> bookInfo`.
CraftingBookCategory.CODEC, CraftingBookCategory.MISC, BlockBookInfo::new
);
public static final StreamCodec<RegistryFriendlyByteBuf, BlockBookInfo> STREAM_CODEC = Recipe.BookInfo.streamCodec(
// Takes in the stream codec for the generic and the constructor of
// `(category, group) -> bookInfo`.
CraftingBookCategory.STREAM_CODEC, BlockBookInfo::new
);
}
}
提示 与
CommonInfo一样,不强制使用BookInfo,也不强制在 JSON 中公开这些字段。模组开发者应判断它是否适合自己的配方。例如,Recipe#isSpecial返回 true 的配方不会出现在配方书中,因此不应使用BookInfo。不过,Recipe#group和recipeBookCategory都必须返回非 null 对象。
配方分组
分组充当键,将多个配方合并为配方书中的一个条目。如果组名为空字符串,该配方就会作为独立条目处理。
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
@Override
public String group() {
return this.bookInfo.group();
}
}
配方书分类
RecipeBookCategory 只是定义此配方在配方书中属于哪个组。例如,铁镐合成配方显示在 RecipeBookCategories#CRAFTING_EQUIPMENT 中,而熟鳕鱼配方显示在 #FURNANCE_FOOD 或 #SMOKER_FOOD 中。每个配方关联一个 RecipeBookCategory。原版分类位于 RecipeBookCategories。
提示 熟鳕鱼有两个配方,分别用于熔炉和烟熏炉。这两个配方属于不同的配方书分类。
如果配方不适合任何现有分类,通常是因为没有使用工作台、熔炉等现有工作站,可以创建新的 RecipeBookCategory。每个 RecipeBookCategory 都必须注册到 BuiltInRegistries#RECIPE_BOOK_CATEGORY:
/// For some DeferredRegister<RecipeBookCategory> RECIPE_BOOK_CATEGORIES
public static final Supplier<RecipeBookCategory> RIGHT_CLICK_BLOCK_CATEGORY = RECIPE_BOOK_CATEGORIES.register(
"right_click_block", RecipeBookCategory::new
);
然后重写 #recipeBookCategory 设置分类:可以直接返回自己的分类,也可以通过配方书信息(如果已实现)映射到相应分类:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
@Override
public RecipeBookCategory recipeBookCategory() {
return switch (this.bookInfo.category()) {
case BUILDING -> RecipeBookCategories.CRAFTING_BUILDING_BLOCKS;
case EQUIPMENT -> RecipeBookCategories.CRAFTING_EQUIPMENT;
case REDSTONE -> RecipeBookCategories.CRAFTING_REDSTONE;
case MISC -> RIGHT_CLICK_BLOCK_CATEGORY.get();
};
}
}
搜索分类
从类型上看,所有 RecipeBookCategory 都是 ExtendedRecipeBookCategory。此外还有一种名为 SearchRecipeBookCategory 的 ExtendedRecipeBookCategory,用于在配方书中查看所有配方时聚合多个 RecipeBookCategory。
NeoForge 允许通过模组事件总线上的 RegisterRecipeBookSearchCategoriesEvent#register,将自己的 ExtendedRecipeBookCategory 指定为搜索分类。register 接收表示搜索分类的 ExtendedRecipeBookCategory,以及组成该分类的各个 RecipeBookCategory。作为搜索分类的 ExtendedRecipeBookCategory 无需注册到某个原版静态注册表。
// In some location
public static final ExtendedRecipeBookCategory RIGHT_CLICK_BLOCK_SEARCH_CATEGORY = new ExtendedRecipeBookCategory() {};
@SubscribeEvent // on the mod event bus
public static void registerSearchCategories(RegisterRecipeBookSearchCategoriesEvent event) {
event.register(
// The search category
RIGHT_CLICK_BLOCK_SEARCH_CATEGORY,
// All recipe categories within the search category as varargs
RecipeBookCategories.CRAFTING_BUILDING_BLOCKS,
RecipeBookCategories.CRAFTING_EQUIPMENT,
RecipeBookCategories.CRAFTING_REDSTONE,
RIGHT_CLICK_BLOCK_CATEGORY.get()
)
}
摆放信息
PlacementInfo 用于定义配方使用方所需的合成要求,以及是否、如何将原料放入关联工作站(如工作台或熔炉)。PlacementInfo 只面向物品原料;若要使用流体、方块等其他原料,必须自行实现周边逻辑。这时可以将配方标记为不可摆放,使用 PlacementInfo#NOT_PLACEABLE 表达。不过,只要配方至少包含一个类似物品的对象,就应创建 PlacementInfo。
可以通过 create 创建 PlacementInfo,它接收单个原料或原料列表;也可以使用 createFromOptionals,它接收可选原料列表。如果配方中需要表示空槽位,应使用 createFromOptionals,为空槽传入空的 optional:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
private PlacementInfo info;
@Override
public PlacementInfo placementInfo() {
// This delegate is in case the ingredient is not fully populated at this point in time
// Tags and recipes are loaded at the same time, which is why this might be the case.
if (this.info == null) {
// Use optional ingredient as the block state may have an item representation
List<Optional<Ingredient>> ingredients = new ArrayList<>();
Item stateItem = this.inputState.getBlock().asItem();
ingredients.add(stateItem != Items.AIR ? Optional.of(Ingredient.of(stateItem)): Optional.empty());
ingredients.add(Optional.of(this.inputItem));
// Create placement info
this.info = PlacementInfo.createFromOptionals(ingredients);
}
return this.info;
}
}
槽位显示
SlotDisplay 表示配方书等配方使用方查看配方时,各槽位应渲染什么内容。SlotDisplay 有两个方法。首先是 resolve:它接收包含可用注册表和燃料值的 ContextMap(见 SlotDisplayContext),以及接收本槽位显示内容的当前 DisplayContentsFactory,并返回将内容列表转换成所需输出后的结果。其次是 type,保存编码、解码显示对象所用的 MapCodec 和 StreamCodec。
SlotDisplay 通常在 Ingredient 的 #display,或模组原料的 ICustomIngredient#display 中实现。但某些输入并非原料,这时需要使用已有 SlotDisplay,或创建新的实现。
原版和 NeoForge 提供以下槽位显示类型:
SlotDisplay.Empty:表示没有任何内容的槽位。SlotDisplay.ItemSlotDisplay:表示物品的槽位。SlotDisplay.ItemStackSlotDisplay:表示物品堆叠模板的槽位。SlotDisplay.TagSlotDisplay:表示物品标签的槽位。SlotDisplay.OnlyWithComponent:过滤另一显示对象,只保留具有给定数据组件的物品。SlotDisplay.WithAnyPotion:表示具有随机DataComponents#POTION_CONTENTS值的输入。SlotDisplay.WithRemainder:表示带合成剩余物的输入。SlotDisplay.AnyFuel:表示所有燃料物品。SlotDisplay.Composite:表示多个其他槽位显示的组合。SlotDisplay.DyedSlotDemo:表示将染料应用到某个目标,并设置DataComponents#DYED_COLOR。SlotDisplay.SmithingTrimDemoSlotDisplay:表示使用给定材料向基础物品应用随机锻造纹饰。FluidSlotDisplay:表示流体。FluidStackSlotDisplay:表示流体堆叠。FluidTagSlotDisplay:表示流体标签。
本例配方有三个“槽位”:BlockState 输入、Ingredient 输入和 ItemStack 结果。Ingredient 输入已有对应 SlotDisplay;ItemStack 可由 SlotDisplay.ItemStackSlotDisplay 表示。但 BlockState 需要自定义 SlotDisplay 和 DisplayContentsFactory,因为现有实现只接受物品堆叠,而本例以不同方式处理方块状态。
先看 DisplayContentsFactory:它负责将某种类型转换为所需的内容显示类型。可用工厂包括:
DisplayContentsFactory.ForStacks:接收ItemStack的转换器。DisplayContentsFactory.ForRemainders:接收输入对象及剩余物对象列表的转换器。ForFluidStacks:接收FluidStack的转换器。
这样,可以实现 DisplayContentsFactory,将给定对象转换为所需输出。例如,SlotDisplay.ItemStackContentsFactory 使用 ForStacks 转换器,将堆叠转换为 ItemStack。
对于本例的 BlockState,创建一个接收状态的工厂,再提供一个直接输出状态本身的基础实现。
// A basic transformer for block states
public interface ForBlockStates<T> extends DisplayContentsFactory<T> {
// Delegate methods
default forState(Holder<Block> block) {
return this.forState(block.value());
}
default forState(Block block) {
return this.forState(block.defaultBlockState());
}
// The block state to take in and transform to the desired output
T forState(BlockState state);
}
// An implementation for a block state output
public class BlockStateContentsFactory implements ForBlockStates<BlockState> {
// Singleton instance
public static final BlockStateContentsFactory INSTANCE = new BlockStateContentsFactory();
private BlockStateContentsFactory() {}
@Override
public BlockState forState(BlockState state) {
return state;
}
}
// An implementation for an item stack output
public class BlockStateStackContentsFactory implements ForBlockStates<ItemStack> {
// Singleton instance
public static final BlockStateStackContentsFactory INSTANCE = new BlockStateStackContentsFactory();
private BlockStateStackContentsFactory() {}
@Override
public ItemStack forState(BlockState state) {
return new ItemStack(state.getBlock());
}
}
有了它,就可以创建新的 SlotDisplay。SlotDisplay.Type 必须注册:
// A simple slot display
public record BlockStateSlotDisplay(BlockState state) implements SlotDisplay {
public static final MapCodec<BlockStateSlotDisplay> CODEC = BlockState.CODEC.fieldOf("state")
.xmap(BlockStateSlotDisplay::new, BlockStateSlotDisplay::state);
public static final StreamCodec<RegistryFriendlyByteBuf, BlockStateSlotDisplay> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), BlockStateSlotDisplay::state,
BlockStateSlotDisplay::new
);
@Override
public <T> Stream<T> resolve(ContextMap context, DisplayContentsFactory<T> factory) {
return switch (factory) {
// Check for our contents factory and transform if necessary
case ForBlockStates<T> states -> Stream.of(states.forState(this.state));
// If you want the contents to be handled differently depending on contents display
// then you can case on other displays like so
case ForStacks<T> stacks -> Stream.of(stacks.forStack(state.getBlock().asItem()));
// If no factories match, then do not return anything in the transformed stream
default -> Stream.empty();
}
}
@Override
public SlotDisplay.Type<? extends SlotDisplay> type() {
// Return the registered type from below
return BLOCK_STATE_SLOT_DISPLAY.get();
}
}
// In some registrar class
/// For some DeferredRegister<SlotDisplay.Type<?>> SLOT_DISPLAY_TYPES
public static final Supplier<SlotDisplay.Type<BlockStateSlotDisplay>> BLOCK_STATE_SLOT_DISPLAY = SLOT_DISPLAY_TYPES.register(
"block_state",
() -> new SlotDisplay.Type<>(BlockStateSlotDisplay.CODEC, BlockStateSlotDisplay.STREAM_CODEC)
);
配方显示
RecipeDisplay 类似 SlotDisplay,不过表示整个配方。默认接口仅记录配方的 result 和表示所用工作台的 craftingStation。RecipeDisplay 也有一个 type,保存编码、解码显示对象所需的 MapCodec 和 StreamCodec。不过,现有 RecipeDisplay 子类型都无法包含在客户端正确渲染本例配方所需的全部信息,因此需要创建自己的 RecipeDisplay。
所有槽位和原料都应表示为 SlotDisplay。网格大小等限制,可以采用开发者自行选择的任意方式提供。
// A simple recipe display
public record RightClickBlockRecipeDisplay(
SlotDisplay inputState,
SlotDisplay inputItem,
SlotDisplay result, // Implements RecipeDisplay#result
SlotDisplay craftingStation // Implements RecipeDisplay#craftingStation
) implements RecipeDisplay {
public static final MapCodec<RightClickBlockRecipeDisplay> MAP_CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(
SlotDisplay.CODEC.fieldOf("input_state").forGetter(RightClickBlockRecipeDisplay::inputState),
SlotDisplay.CODEC.fieldOf("input_item").forGetter(RightClickBlockRecipeDisplay::inputItem),
SlotDisplay.CODEC.fieldOf("result").forGetter(RightClickBlockRecipeDisplay::result),
SlotDisplay.CODEC.fieldOf("crafting_station").forGetter(RightClickBlockRecipeDisplay::craftingStation)
)
.apply(instance, RightClickBlockRecipeDisplay::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, RightClickBlockRecipeDisplay> STREAM_CODEC = StreamCodec.composite(
SlotDisplay.STREAM_CODEC,
RightClickBlockRecipeDisplay::inputState,
SlotDisplay.STREAM_CODEC,
RightClickBlockRecipeDisplay::inputItem,
SlotDisplay.STREAM_CODEC,
RightClickBlockRecipeDisplay::result,
SlotDisplay.STREAM_CODEC,
RightClickBlockRecipeDisplay::craftingStation,
RightClickBlockRecipeDisplay::new
);
@Override
public RecipeDisplay.Type<? extends RecipeDisplay> type() {
// Return the registered type from below
return RIGHT_CLICK_BLOCK_RECIPE_DISPLAY.get();
}
}
// In some registrar class
/// For some DeferredRegister<RecipeDisplay.Type<?>> RECIPE_DISPLAY_TYPES
public static final Supplier<RecipeDisplay.Type<RightClickBlockRecipeDisplay>> RIGHT_CLICK_BLOCK_RECIPE_DISPLAY = RECIPE_DISPLAY_TYPES.register(
"right_click_block",
() -> new RecipeDisplay.Type<>(RightClickBlockRecipeDisplay.CODEC, RightClickBlockRecipeDisplay.STREAM_CODEC)
);
然后重写 #display,为配方创建显示对象:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
@Override
public List<RecipeDisplay> display() {
// You can have many different displays for the same recipe
// But this example will only use one like the other recipes.
return List.of(
// Add our recipe display with the specified slots
new RightClickBlockRecipeDisplay(
new BlockStateSlotDisplay(this.inputState),
this.inputItem.display(),
new SlotDisplay.ItemStackSlotDisplay(this.result),
new SlotDisplay.ItemSlotDisplay(Items.GRASS_BLOCK)
)
)
}
}
配方类型
接下来是配方类型。这部分很简单,因为除了名称,配方类型没有其他关联数据。它是配方系统中两个需要注册的部分之一。因此与其他注册表一样,创建 DeferredRegister 并注册:
public static final DeferredRegister<RecipeType<?>> RECIPE_TYPES =
DeferredRegister.create(Registries.RECIPE_TYPE, ExampleMod.MOD_ID);
public static final Supplier<RecipeType<RightClickBlockRecipe>> RIGHT_CLICK_BLOCK_TYPE =
RECIPE_TYPES.register(
"right_click_block",
// Creates the recipe type, setting `toString` to the registry name of the type
RecipeType::simple
);
注册配方类型后,必须在配方中重写 #getType:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
@Override
public RecipeType<? extends Recipe<RightClickBlockInput>> getType() {
return RIGHT_CLICK_BLOCK_TYPE.get();
}
}
配方序列化器
配方序列化器提供两个 codec:map codec 和 stream codec,分别用于 JSON 与网络的双向序列化。本节不深入介绍 codec 的工作方式,更多信息见 Map Codec 和 Stream Codec。
在配方类内部创建 map codec 和 stream codec。
public static final MapCodec<RightClickBlockRecipe> CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group(
Recipe.CommonInfo.MAP_CODEC.forGetter(recipe -> recipe.commonInfo),
RightClickBlockRecipe.BlockBookInfo.MAP_CODEC.forGetter(recipe -> recipe.bookInfo),
BlockState.CODEC.fieldOf("state").forGetter(RightClickBlockRecipe::getInputState),
Ingredient.CODEC.fieldOf("ingredient").forGetter(RightClickBlockRecipe::getInputItem),
ItemStack.CODEC.fieldOf("result").forGetter(RightClickBlockRecipe::getResult)
).apply(inst, RightClickBlockRecipe::new));
public static final StreamCodec<RegistryFriendlyByteBuf, RightClickBlockRecipe> STREAM_CODEC = StreamCodec.composite(
Recipe.CommonInfo.STREAM_CODEC, recipe -> recipe.commonInfo,
RightClickBlockRecipe.BlockBookInfo.STREAM_CODEC, recipe -> recipe.bookInfo,
ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), RightClickBlockRecipe::getInputState,
Ingredient.CONTENTS_STREAM_CODEC, RightClickBlockRecipe::getInputItem,
ItemStack.STREAM_CODEC, RightClickBlockRecipe::getResult,
RightClickBlockRecipe::new
);
与类型一样,创建并注册序列化器:
public static final DeferredRegister<RecipeType<?>> RECIPE_SERIALIZERS =
DeferredRegister.create(Registries.RECIPE_SERIALIZER, ExampleMod.MOD_ID);
public static final Supplier<RecipeSerializer<RightClickBlockRecipe>> RIGHT_CLICK_BLOCK =
RECIPE_SERIALIZERS.register("right_click_block", ()-> new RecipeSerializer<>(RightClickBlockRecipe.CODEC, RightClickBlockRecipe.STREAM_CODEC));
同样,还必须在配方中重写 #getSerializer:
public class RightClickBlockRecipe implements Recipe<RightClickBlockInput> {
// other stuff here
@Override
public RecipeSerializer<? extends Recipe<RightClickBlockInput>> getSerializer() {
return RIGHT_CLICK_BLOCK.get();
}
}
合成机制
配方各部分齐备后,可以创建一些配方 JSON(见数据生成一节),再如上文所示通过配方管理器查询。之后如何使用配方由你决定。常见用途是创建处理配方的机器,并将当前配方保存为字段。
本例希望玩家用物品右键方块时应用配方,因此使用重载监听器和事件处理器。这只是示例实现,可以按需修改,但必须在服务端运行。为了让客户端和服务端的交互状态一致,还需通过网络同步相关输入状态。
可以建立简单的网络实现来同步配方输入:
// A basic packet class, must be registered.
public record ClientboundRightClickBlockRecipesPayload(
Set<BlockState> inputStates, Set<Holder<Item>> inputItems
) implements CustomPacketPayload {
// ...
}
// Packet stores data in an instance class.
// Present on both server and client to do initial matching.
public interface RightClickBlockRecipeInputs {
Set<BlockState> inputStates();
Set<Holder<Item>> inputItems();
default boolean test(BlockState state, ItemStack stack) {
return this.inputStates().contains(state) && this.inputItems().contains(stack.getItemHolder());
}
}
// Server resource listener so it can be reloaded when recipes are.
public class ServerRightClickBlockRecipeInputs implements ResourceManagerReloadListener, RightClickBlockRecipeInputs {
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "block_recipe_inputs");
private final RecipeManager recipeManager;
private Set<BlockState> inputStates;
private Set<Holder<Item>> inputItems;
public RightClickBlockRecipeInputs(RecipeManager recipeManager) {
this.recipeManager = recipeManager;
}
// Set inputs here as #apply is fired synchronously based on listener registration order.
// Recipes are always applied first.
@Override
public void onResourceManagerReload(ResourceManager manager) {
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server == null) return; // Should never be null
// Populate inputs
Set<BlockState> inputStates = new HashSet<>();
Set<Holder<Item>> inputItems = new HashSet<>();
this.recipeManager.recipeMap().byType(RIGHT_CLICK_BLOCK_TYPE.get())
.forEach(holder -> {
var recipe = holder.value();
inputStates.add(recipe.getInputState());
inputItems.addAll(recipe.getInputItem().items());
});
this.inputStates = Set.copyOf(inputStates);
this.inputItems = Set.copyOf(inputItems);
}
public void syncToClient(Stream<ServerPlayer> players) {
ClientboundRightClickBlockRecipesPayload payload =
new ClientboundRightClickBlockRecipesPayload(this.inputStates, this.inputItems);
players.forEach(player -> PacketDistributor.sendToPlayer(player, payload));
}
@Override
public Set<BlockState> inputStates() {
return this.inputStates;
}
@Override
public Set<Holder<Item>> inputItems() {
return this.inputItems;
}
}
// Client implementation to hold the inputs.
public record ClientRightClickBlockRecipeInputs(
Set<BlockState> inputStates, Set<Holder<Item>> inputItems
) implements RightClickBlockRecipeInputs {
public ClientRightClickBlockRecipeInputs(Set<BlockState> inputStates, Set<Holder<Item>> inputItems) {
this.inputStates = Set.copyOf(inputStates);
this.inputItems = Set.copyOf(inputItems);
}
}
// Handling the recipe instance depending on side.
public class ServerRightClickBlockRecipes {
private static ServerRightClickBlockRecipeInputs inputs;
public static RightClickBlockRecipeInputs inputs() {
return ServerRightClickBlockRecipes.inputs;
}
@SubscribeEvent // on the game event bus
public static void addListener(AddServerReloadListenersEvent event) {
// Register server reload listener
ServerRightClickBlockRecipes.inputs = new ServerRightClickBlockRecipeInputs(
event.getServerResources().getRecipeManager()
);
event.addListener(ServerRightClickBlockRecipeInputs.ID, ServerRightClickBlockRecipes.inputs);
// Make sure it runs after recipes
event.addDependency(VanillaServerListeners.RECIPES, ServerRightClickBlockRecipeInputs.ID);
}
@SubscribeEvent // on the game event bus
public static void datapackSync(OnDatapackSyncEvent event) {
// Send to client
ServerRightClickBlockRecipes.inputs.syncToClient(event.getRelevantPlayers());
}
}
public class ClientRightClickBlockRecipes {
private static ClientRightClickBlockRecipeInputs inputs;
public static RightClickBlockRecipeInputs inputs() {
return ClientRightClickBlockRecipes.inputs;
}
// Handling the sent packet
public static void handle(final ClientboundRightClickBlockRecipesPayload data, final IPayloadContext context) {
// Do something with the data, on the main thread
ClientRightClickBlockRecipes.inputs = new ClientRightClickBlockRecipeInputs(
data.inputStates(), data.inputItems()
);
}
@SubscribeEvent // on the game event bus only on the physical client
public static void clientLogOut(ClientPlayerNetworkEvent.LoggingOut event) {
// Clear the stored inputs on world log out
ClientRightClickBlockRecipes.inputs = null;
}
}
public class RightClickBlockRecipes {
// Make proxy method to access properly
public static RightClickBlockRecipeInputs inputs(Level level) {
return level.isClientSide()
? ClientRightClickBlockRecipes.inputs()
: ServerRightClickBlockRecipes.inputs();
}
}
也可以改为向客户端同步完整配方:
// Present on both server and client to do initial matching.
public interface RightClickBlockRecipeInputs {
Set<BlockState> inputStates();
Set<Holder<Item>> inputItems();
default boolean test(BlockState state, ItemStack stack) {
return this.inputStates().contains(state) && this.inputItems().contains(stack.getItemHolder());
}
}
// Server resource listener so it can be reloaded when recipes are.
public class ServerRightClickBlockRecipeInputs implements ResourceManagerReloadListener, RightClickBlockRecipeInputs {
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "block_recipe_inputs");
private final RecipeManager recipeManager;
private Set<BlockState> inputStates;
private Set<Holder<Item>> inputItems;
public RightClickBlockRecipeInputs(RecipeManager recipeManager) {
this.recipeManager = recipeManager;
}
// Set inputs here as #apply is fired synchronously based on listener registration order.
// Recipes are always applied first.
@Override
public void onResourceManagerReload(ResourceManager manager) {
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server != null) { // Should never be null
// Populate inputs
Set<BlockState> inputStates = new HashSet<>();
Set<Holder<Item>> inputItems = new HashSet<>();
this.recipeManager.recipeMap().byType(RIGHT_CLICK_BLOCK_TYPE.get())
.forEach(holder -> {
var recipe = holder.value();
inputStates.add(recipe.getInputState());
inputItems.addAll(recipe.getInputItem().items());
});
this.inputStates = Set.copyOf(inputStates);
this.inputItems = Set.copyOf(inputItems);
}
}
@Override
public Set<BlockState> inputStates() {
return this.inputStates;
}
@Override
public Set<Holder<Item>> inputItems() {
return this.inputItems;
}
}
// Client implementation to hold the inputs.
public record ClientRightClickBlockRecipeInputs(
Set<BlockState> inputStates, Set<Holder<Item>> inputItems
) implements RightClickBlockRecipeInputs {
public ClientRightClickBlockRecipeInputs(Set<BlockState> inputStates, Set<Holder<Item>> inputItems) {
this.inputStates = Set.copyOf(inputStates);
this.inputItems = Set.copyOf(inputItems);
}
}
// Handling the recipe instance depending on side.
public class ServerRightClickBlockRecipes {
private static ServerRightClickBlockRecipeInputs inputs;
public static RightClickBlockRecipeInputs inputs() {
return ServerRightClickBlockRecipes.inputs;
}
@SubscribeEvent // on the game event bus
public static void addListener(AddServerReloadListenersEvent event) {
// Register server reload listener
ServerRightClickBlockRecipes.inputs = new ServerRightClickBlockRecipeInputs(
event.getServerResources().getRecipeManager()
);
event.addListener(ServerRightClickBlockRecipeInputs.ID, ServerRightClickBlockRecipes.inputs);
// Make sure it runs after recipes
event.addDependency(VanillaServerListeners.RECIPES, ServerRightClickBlockRecipeInputs.ID);
}
@SubscribeEvent // on the game event bus
public static void datapackSync(OnDatapackSyncEvent event) {
// Specify what recipe types to sync to the client
event.sendRecipes(RIGHT_CLICK_BLOCK_TYPE.get());
}
}
public class ClientRightClickBlockRecipes {
private static ClientRightClickBlockRecipeInputs inputs;
public static RightClickBlockRecipeInputs inputs() {
return ClientRightClickBlockRecipes.inputs;
}
@SubscribeEvent // on the game event bus only on the physical client
public static void recipesReceived(RecipesReceivedEvent event) {
// Store the recipes
Set<BlockState> inputStates = new HashSet<>();
Set<Holder<Item>> inputItems = new HashSet<>();
event.getRecipeMap().byType(RIGHT_CLICK_BLOCK_TYPE.get())
.forEach(holder -> {
var recipe = holder.value();
inputStates.add(recipe.getInputState());
inputItems.addAll(recipe.getInputItem().items());
});
ClientRightClickBlockRecipes.inputs = new ClientRightClickBlockRecipeInputs(
inputStates, inputItems
);
}
@SubscribeEvent // on the game event bus only on the physical client
public static void clientLogOut(ClientPlayerNetworkEvent.LoggingOut event) {
// Clear the stored inputs on world log out
ClientRightClickBlockRecipes.inputs = null;
}
}
public class RightClickBlockRecipes {
// Make proxy method to access properly
public static RightClickBlockRecipeInputs inputs(Level level) {
return level.isClientSide()
? ClientRightClickBlockRecipes.inputs()
: ServerRightClickBlockRecipes.inputs();
}
}
然后使用已同步的输入,检查游戏中实际使用的输入:
@SubscribeEvent // on the game event bus
public static void useItemOnBlock(UseItemOnBlockEvent event) {
// Skip if we are not in the block-dictated phase of the event. See the event's javadocs for details.
if (event.getUsePhase() != UseItemOnBlockEvent.UsePhase.BLOCK) return;
// Get parameters to check input first
Level level = event.getLevel();
BlockPos pos = event.getPos();
BlockState blockState = level.getBlockState(pos);
ItemStack itemStack = event.getItemStack();
// Check if the input can result in a recipe on both sides
if (!RightClickBlockRecipes.inputs(level).test(blockState, itemStack)) return;
// If so, make sure on server before checking recipe
if (!level.isClientSide() && level instanceof ServerLevel serverLevel) {
// Create an input and query the recipe.
RightClickBlockInput input = new RightClickBlockInput(blockState, itemStack);
Optional<RecipeHolder<? extends Recipe<CraftingInput>>> optional = serverLevel.recipeAccess().getRecipeFor(
// The recipe type.
RIGHT_CLICK_BLOCK_TYPE.get(),
input,
level
);
ItemStack result = optional
.map(RecipeHolder::value)
.map(e -> e.assemble(input))
.orElse(ItemStack.EMPTY);
// If there is a result, break the block and drop the result in the world.
if (!result.isEmpty()) {
level.removeBlock(pos, false);
ItemEntity entity = new ItemEntity(level,
// Center of pos.
pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5,
result);
level.addFreshEntity(entity);
}
}
// Cancel the event to stop the interaction pipeline regardless of side.
// Already made sure that there could be a result.
event.cancelWithResult(InteractionResult.SUCCESS_SERVER);
}
数据生成
为自己的配方序列化器创建构建器时,需要实现 RecipeBuilder 及其方法。以下是一种部分参考原版代码的常见实现:
// This class is abstract because there is a lot of per-recipe-serializer logic.
// It serves the purpose of showing the common part of all (vanilla) recipe builders.
public abstract class SimpleRecipeBuilder implements RecipeBuilder {
// Make the fields protected so our subclasses can use them.
protected final ItemStackTemplate result;
protected String group = "";
protected boolean showNotification = true;
// Provides a common way to build the recipe unlock advancement.
// If used, the builder must also specify a `RecipeCategory` to determine
// the output folder.
protected final RecipeUnlockAdvancementBuilder advancementBuilder;
protected final RecipeCategory category;
// It is common for constructors to accept the result item stack template.
// Alternatively, static builder methods are also possible.
public SimpleRecipeBuilder(ItemStackTemplate result, RecipeCategory category) {
this.result = result;
this.category = category;
this.advancementBuilder = new RecipeUnlockAdvancementBuilder();
}
// This method adds a criterion for the recipe advancement.
@Override
public SimpleRecipeBuilder unlockedBy(String name, Criterion<?> criterion) {
this.criteria.put(name, criterion);
return this;
}
// This method adds a recipe book group. If you do not want to use recipe book groups,
// remove the this.group field and make this method no-op (i.e. return this).
@Override
public SimpleRecipeBuilder group(@Nullable String group) {
this.group = Objects.requireNonNullElse(group, "");
return this;
}
// This method sets whether to show the notification toast when unlocking. If you want
// this value to be hardcoded, remove the this.showNotification field and this method.
public SimpleRecipeBuilder showNotification(boolean showNotification) {
this.showNotification = showNotification;
return this;
}
// Returns the id of the recipe when using `#save(RecipeOutput)`.
@Override
public ResourceKey<Recipe<?>> defaultId() {
// If the result is not an `ItemStackTemplate`, you will need to manually
// construct the `ResourceKey` using the result.
return RecipeBuilder.getDefaultRecipeId(this.result);
}
}
这样就有了配方构建器的基础。继续实现依赖具体序列化器的部分之前,先考虑使用什么作为配方工厂。本例直接使用构造函数较合适。其他情况下,可以使用静态辅助方法或小型函数式接口;一个构建器服务于多个配方类时,这一点尤其重要。
使用 RightClickBlockRecipe::new 作为配方工厂,并复用上面的 SimpleRecipeBuilder 类,可以为 RightClickBlockRecipe 创建以下构建器:
public class RightClickBlockRecipeBuilder extends SimpleRecipeBuilder {
private final BlockState inputState;
private final Ingredient inputItem;
// Since we have exactly one of each input, we pass them to the constructor.
// Builders for recipe serializers that have ingredient lists of some sort would usually
// initialize an empty list and have #addIngredient or similar methods instead.
public RightClickBlockRecipeBuilder(ItemStackTemplate result, RecipeCategory category, BlockState inputState, Ingredient inputItem) {
super(result, category);
this.inputState = inputState;
this.inputItem = inputItem;
}
// Saves a recipe using the given RecipeOutput and key. This method is defined in the RecipeBuilder interface.
@Override
public void save(RecipeOutput output, ResourceKey<Recipe<?>> key) {
// Create the recipe.
RightClickBlockRecipe recipe = new RightClickBlockRecipe(
RecipeBuilder.createCraftingCommonInfo(this.showNotification),
new RightClickBlockRecipe.BlockBookInfo(
RecipeBuilder.determineCraftingBookCategory(this.category),
this.group
),
this.inputState,
this.inputItem,
this.result
);
// Pass the id, recipe, and the recipe advancement into the RecipeOutput.
output.accept(key, recipe, this.advancementBuilder.build(output, key, this.category));
}
}
现在可以在数据生成时,像其他构建器一样调用它:
@Override
protected void buildRecipes(RecipeOutput output) {
new RightClickRecipeBuilder(
// Our constructor parameters. This example adds the ever-popular dirt -> diamond conversion.
new ItemStackTemplate(Items.DIAMOND),
RecipeCategory.MISC,
Blocks.DIRT.defaultBlockState(),
Ingredient.of(Items.APPLE)
)
.unlockedBy("has_apple", this.has(Items.APPLE))
.save(output);
// other recipe builders here
}
提示 也可以将
SimpleRecipeBuilder合并到RightClickBlockRecipeBuilder(或自己的配方构建器)中,尤其是只有一两个构建器时。这里使用抽象,是为了展示构建器中哪些部分依赖配方、哪些部分不依赖。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。