MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针

如果在编辑的过程中遇到了什么问题,可以去讨论板提问。

为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证

MCBBS Wiki GitHub群组已上线!

您可以在回声洞中发表吐槽!

服务器状态监控。点击进入

本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>

用户:MashKJo/1.12.2模组开发教程/17.矿物词典和各类配方

来自MCBBS Wiki
MashKJo留言 | 贡献2025年1月20日 (一) 23:46的版本 (创建页面,内容为“给原版的工作台、熔炉等增加新的配方以服务于我们的模组,可以说是最常见的需求之一了。本节教程将详细阐述这些。 == 矿物词典 == 矿物词典(Ore Dictionary),是Forge的一个历史非常悠久的系统——它旨在让设定上为同一样东西,而实际上因是不同模组添加而无法混用的物品在许多场合表现得可以互相通用。如:几乎所有科技模组都会添加自己的…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

给原版的工作台、熔炉等增加新的配方以服务于我们的模组,可以说是最常见的需求之一了。本节教程将详细阐述这些。

矿物词典

矿物词典(Ore Dictionary),是Forge的一个历史非常悠久的系统——它旨在让设定上为同一样东西,而实际上因是不同模组添加而无法混用的物品在许多场合表现得可以互相通用。如:几乎所有科技模组都会添加自己的铜锭,如果这许多种铜锭不能互相通用,对多模组整合包玩家将会是一场灾难。

矿物词典提供的解决方案是:给这些设定上相同实际上不同的物品都统一给予一个词典名,如对于铜锭,其词典名为“ingotCopper”,在决定两个物品是否可以通用时,只需比较它们的词典名即可。词典名的命名没有强制规范,不过它有约定俗成的规范:

  1. 小写驼峰式命名
  2. 形态(种类)在前,质地(材料)在后

且,虽然名称叫矿物词典,但实际上它可不仅仅只能用于和矿物有关的物品,相反,它能应用于一切物品。且有些物品还不止一个词典名——如青金石既是gemLapis,又是dyeBlue。

Forge为许多原版物品已经内建了矿物词典名,这些都可以在OreDictionary#initVanillaEntries这个私有方法中找到。

注册矿物词典非常简单:

OreDictionary.registerOre(name, ore);

这样就可以了,name是词典名,而ore,可以是Item、Block或ItemStack。

此外,OreDictionary类下还有如下辅助用的静态方法:

  • static boolean matches(ItemStack target, ItemStack input, boolean strict):判断2个ItemStack是否在矿物词典中相匹配。那个boolean参数影响的是:2个ItemStack的meta值影不影响判断结果。
  • static boolean containsMatch(boolean strict, NonNullList<ItemStack> inputs, ItemStack... targets):判断两组ItemStack中是否有那么一对在矿物词典中相匹配。
  • static boolean doesOreNameExist(String name):检查矿物词典中某词典名有没有对应的物品。
  • static NonNullList<ItemStack> getOres(String name, boolean alwaysCreateEntry):获取某一词典名下所有符合条件的物品。

随着1.13更为强大易用的标签(Tag)系统的加入,矿物词典也正式退出了Forge Mod开发的历史舞台。

WILDCARD_VALUE

OreDictionary.WILDCARD_VALUE是Forge定义的一个常量,用于取代Mojang对于幻数的使用。这个幻数为Short.MAX_VALUE。你可能注意到了,initVanillaEntries()方法中,有些ItemStack的meta值这个位置填入的就是这个WILDCARD_VALUE。

meta值为WILDCARD_VALUE的ItemStack,实际上是一个抽象的概念了,其代表一类meta值不定的ItemStack——即“不在乎”ItemStack的meta值到底是多少,只要物品类型和堆叠数量一致了,就可以在词典中成功匹配。这个常量除了能应用在代码中,也能应用在简单工作台配方的.json文件中。

工作台配方

简单工作台配方:写JSON文件

1.12开始,简单的原版工作台配方并非要在代码中指定,而是已经数据驱动化了——Minecraft会自动读取所有位于assets/[modid]/recipes路径下的所有.json文件,并自动将这些.json文件代表的合成表载入游戏。

这属于原版的数据包知识(尽管1.12时期数据包还并未被正式加入),参见:配方#合成配方 - 中文 Minecraft Wiki。注意:Wiki上关于这部分的阐释,很多内容都是高版本才有的,如:标签(Tag)、物品组件(DataComponent),请自行甄别。

这里笔者对原版的JSON格式只做简要阐述了。例如,对于我们之前添加的红宝石和红宝石块,我们可以添加从块到宝石的转换配方——显然,是无序配方:

{
   "type": "minecraft:crafting_shapeless",
   "group": "tutorial_mod:example_group",
   "ingredients": [ { "item": "tutorial_mod:ruby_block" } ],
   "result": {
       "item": "tutorial_mod:ruby",
       "count": 9
   }
}

type为minecraft:crafting_shapeless代表这是一个无序合成配方,group读者可以随便填写。ingredients和result分别代表输入和输出,前者是一个数组。 物品格式不仅仅有item和count这两种属性,还可以有data这个属性——用于指定metadata。将data设定为32767,即可无视meta进行匹配——也即WILDCARD_VALUE所起到的作用。

那么有序合成呢?

{
   "type": "minecraft:crafting_shaped",
   "group": "tutorial_mod:example_group",
   "pattern": ["XXX", "XXX", "XXX"],
   "key": { "X": { "item": "tutorial_mod:ruby" } },
   "result": { "item": "tutorial_mod:ruby_block", "count": 1 }
}

其中pattern是一个三元字符串数组,代表该有序合成配方的“图案”,若有地方不需要材料填充则留空。而key这一属性则阐明图案中用于占位的字符到底都代表什么具体的物品(严格来说是ItemStack)。

以上所述的是原版的JSON格式,鉴于矿物词典的存在,Forge在原版的基础上提供了对矿物词典的支持——type这一属性新增了2种:forge:ore_shapelessforge:ore_shaped,用于指代有矿物词典机制参与时的无序合成和有序合成。而词典物品的格式为:

{
   "type": "ore_dict",
   "ore": <an existing ore dictionary name>
}

这样就行了。

注意:在工作台配方中,矿物词典不能被滥用。一来,如果一个配方的输入和输出物品全都是你自己的模组的物品,那么绝大多数情况下你都不该用矿物词典;二来,矿物词典是仅针对输入的物品的,你不能将其用于配方的输出物品。

高级工作台配方:重新实现IRecipe

所有工作台配方在代码层面都实现了IRecipe接口,这个接口规定了一系列工作台配方所必需的方法。用JSON添加有序和无序合成配方的实质,是Minecraft会先读取这些JSON文件中的信息,再构造ShapedRecipes和ShapelessRecipes这两个IRecipe实现类。 然而,对于复杂的、动态的配方,JSON文件便不能胜任。如,我们可能想要操纵ItemStack的损害值和附加NBT,输出的ItemStack的损害值和附加NBT和输入的ItemStack有着一定的对应关系,这时JSON文件便爱莫能助了,我们必须重新实现IRecipe。典型的例子就是原版的工具修复配方(RecipeRepairItem)和皮革套染色配方(RecipesArmorDyes)。

//和其他那些注册项不同,IRecipe本身是一个接口,没法继承IForgeRegistryEntry.Impl<IRecipe>这个类,因此我们只能在这里继承了……
public class MyCustomRecipe extends IForgeRegistryEntry.Impl<IRecipe> implements IRecipe{

   //这个方法用于检验合成界面中放入的ItemStack是否符合该配方的条件——如果不符合则不能合成出对应物品。
   @Override
   public boolean matches(InventoryCrafting inv, World worldIn) {
       //通常是用这样一个循环来遍历所有格子中的ItemStack。
       for(int i = 0; i < inv.getSizeInventory(); i++){
           ItemStack stack = inv.getStackInSlot(i);
           ...
           //对于比较静态、不变的配方,如果其中有哪怕一个物品不符合要求,则直接return false即可。
           //对于非常动态的配方,你也可以选择弄一个List<ItemStack>,用循环先写入所有ItemStack,再对List中的元素进行你自己想要的判断……
       }
       return true;
   }

   //这个方法用于决定:matches方法返回true之后,合成出来的ItemStack,即“合成结果(Crafting Result)”。
   @Override
   public ItemStack getCraftingResult(InventoryCrafting inv) {
       //坏消息是:对于非常动态的配方,合成产物和输入的物品强相关,但你没法直接从上一个方法中拿到输入的物品。
       //且由于IRecipe实例是享元对象,因此将输入的物品信息存进实例变量也不可行,你基本上只能再重复一次上一个方法中的循环。

       return new ItemStack(...);
   }

   //用于描述“当前合成界面容纳得了当前这个配方与否”,想想看背包中的合成界面和工作台中的合成界面各自的尺寸,就不难理解了。
   @Override
   public boolean canFit(int width, int height) {
       ...
   }

   //这是一个很奇怪的方法,“RecipeOutput”意为“合成配方的输出”,但这个“输出”仅仅是抽象意义上的、合成目标物品成功后的一个“Callback”(?)
   //原版大部分高级配方都在这里返回ItemStack.EMPTY。
   //输出和getCraftingResult方法一样的结果是不可行的,因为这个方法没有InventoryCrafting类型的形参。
   //除非你重新实现一个IRecipeFactory,以从JSON文件中读取你的自定义配方,这时候你便可以通过构造器来接受读取后的配方的各方面信息,包括输出结果,你才能输出非ItemStack.EMPTY的结果。
   //实际上原版的ShapedRecipes和ShapelessRecipes这两个IRecipe实现类就是这么干的,这个方法也是专门为这两种配方服务的。
   @Override
   public ItemStack getRecipeOutput() {
       return ItemStack.EMPTY;
   }

   //IRecipe接口中的默认方法,用于获取:合成结束后留在合成界面中的ItemStack。
   @Override
   public NonNullList<ItemStack> getRemainingItems(InventoryCrafting inv)
   {
       //默认实现:留存所有输入的ItemStack的containerItem。
       return ForgeHooks.defaultRecipeGetRemainingItems(inv);
   }

   //IRecipe接口中的默认方法,用于获取:该合成表的原料(Ingredient)列表,Ingredient就是对不定数量的ItemStack的抽象。
   @Override
   public NonNullList<Ingredient> getIngredients()
   {
       //默认实现:返回一个空集合,原因同getRecipeOutput,实际上这是专门为ShapedRecipes和ShapelessRecipes服务的。
       return NonNullList.<Ingredient>create();
   }

   //IRecipe接口中的默认方法,用于标识:该合成配方是否为动态的。
   @Override
   public boolean isDynamic()
   {
       //默认实现:并非动态的。
       return false;
   }

   //IRecipe接口中的默认方法,用于给合成配方划分组别。实际上这个的意义并不是很大。
   @Override
   public String getGroup()
   {
       //默认实现:返回长度为0的字符串。
       return "";
   }
}

IRecipe是IForgeRegistryEntry<IRecipe>,因此,它的注册也是走Forge的注册表的:

   @SubscribeEvent
   public static void registerCraftingRecipes(RegistryEvent.Register<IRecipe> event){
       event.getRegistry().register(new MyCustomRecipe().setRegistryName(TutorialMod.MODID, "my_custom_recipe"));
   }

熔炉配方和燃料热值

熔炉配方在1.12并未数据驱动化,而是仍然采用硬编码来指定。所有熔炉配方都被放在了FurnaceRecipes这个类当中,暴露出的添加熔炉配方的公开方法为addSmelting和addSmeltingRecipe,我们还可以拿到该类的单例,理论上这就足够我们添加新的熔炉配方了。 不过这实际上是不推荐的做法,因为Forge提供了一个方法:GameRegistry#addSmelting,通过它我们就能便捷地添加熔炉配方。

GameRegistry.addSmelting(input, output, xp);

其中xp代表烧炼后获取的经验,为float类型;output是一个ItemStack;input可以是Item、Block以及ItemStack。

而指定某个物品是燃料,以及指定该物品的燃烧时间,Forge提供了2种方案。 第1种方案是实现一个IFuelHandler,该接口为函数式接口,根据当前的ItemStack信息返回一个int整数——即燃烧的tick数,再通过GameRegistry#registerFuelHandler注册该IFuelHandler:

GameRegistry.registerFuelHandler(stack -> <a certain int value>);

第2种方案是覆写Item#getItemBurnTime + 监听FurnaceFuelBurnTimeEvent事件。前者用于决定你自己的物品的燃料情况,后者用于决定原版和其他模组的情况。burnTime为一个正数代表其可燃,且单位为游戏刻tick;0则代表它不是燃料;-1则代表:移交给原版Minecraft处理相关游戏逻辑。

这两种方案,实际上更推荐大家用第二种,第一种实际上已经被Forge不推荐使用(@Deprecated)了。虽然第一种方案实现起来比较简单,但它实质上就是往GameRegistry的一个静态ArrayList<IFuelHandler>中添加新元素的过程,这样如果多个模组同时针对同一种物品实现IFuelHandler,那么最终结果到底是怎样的,恐怕要看FML加载模组的顺序了——这显然不是我们所期望的;而FurnaceFuelBurnTimeEvent是一个可取消的事件,配合着事件优先级来操作,这种问题就可以得到有效的缓解。

酿造台配方

原版的酿造台配方是由Forge接管的。如果你只是想添加一些简单的酿造配方:

BrewingRecipeRegistry.addRecipe(input, ingredient, output);

其中input指的是位于水瓶槽位的输入的ItemStack,output则是最终的产物,同样也是一个ItemStack。而ingredient则比较有趣,它指的是炼药消耗的材料,例如地狱疣、金萝卜,但addRecipe有两个重载,区别就在于这个ingredient的类型——其中之一为ItemStack,还有一个为String。这也就说明,Forge为添加炼药配方提供了矿物词典支持。

如果我想实现更高级的酿造配方,该怎么做?类似于原版工作台配方的IRecipe,Forge定义了IBrewingRecipe接口,以代表抽象的酿造台配方。 原版的酿造台配方被抽象为了VanillaBrewingRecipe这个类,它直接实现IBrewingRecipe接口,VanillaBrewingRecipe的一个单例即包含原版中所有的酿造配方,且在BrewingRecipeRegistry的静态初始化块中即被addRecipe。

IBrewingRecipe接口规定了isInput、isIngredient和isOutput这三个方法,自己按需实现即可。

还有一个半成品:AbstractBrewingRecipe<?>,它允许ingredient可以是任意类型,比如一个List<ItemStack>,泛型参数即为ingredient的类型。多数时候我们不必直接实现IBrewingRecipe,直接继承它就好。BrewingRecipeRegistry#addRecipe的两个重载,原理其实分别是new了BrewingRecipe和BrewingOreRecipe而已,这两个类就是分别继承于AbstractBrewingRecipe<ItemStack>和AbstractBrewingRecipe<List<ItemStack>>。

相关事件

PotionBrewEvent

有2个子事件:Pre和Post。Pre可取消,发生在药水炼制出来之前,取消即可彻底接管原版的逻辑;Post不可取消,但你也可以从这里拿到相关的信息。

PlayerBrewedPotionEvent

这个事件的名称可能会有迷惑性,它实际发布的时机实际上是玩家从酿造台的输出位取出药水的时候。从这个事件中,我们可以拿到EntityPlayer对象,以及取出的ItemStack。

铁砧配方

原版并没有标准化的铁砧配方这一概念,因为原版当中铁砧的作用也就是敲附魔、修理装备以及重命名物品而已,这些是完全写死在ContainerRepair中的。但Forge依然给我们提供了一个事件:AnvilUpdateEvent,当玩家向其中放入物品时,该事件即会触发。我们可以在这里判断放入的ItemStack,从而设定铁砧的输出位(event.setOutput)。