Skip to content

Latest commit

 

History

History
327 lines (215 loc) · 12.3 KB

5-1.md

File metadata and controls

327 lines (215 loc) · 12.3 KB

5-1 自定义合成表

!> 注意兼容性
本节中的部分内容可能不与旧版本(1.12.2 及以下)兼容,请使用 1.13 或更高版本!

很多服务器中允许玩家使用两个泥土合成一个钻石块……啊不对,是两个(笑)。

好啦,不开玩笑,但是这样的功能确实可以做到。

物品的合成并不是在客户端进行处理的,客户端将玩家的合成表发送到服务端,服务端计算后返回合适的产物。

因此我们自然可以在上面动点手脚……

工作台合成配方(有序)

出人意料的是(也许只有我是这样),Minecraft 中的合成表表述很人性化,和小学的竖式填空很像。相同的符号代表相同的物品,空格代表不放。如果 a 代表圆石,b 代表木棍,那么石斧的摆放方式如下:

aa 
ab 
 b 

每一行的最后都有一个空格,虽然你看不见,但你可以用鼠标选中一部分,就能看到最后有一个空格。

很简单吧?

如果 c 代表煤炭,d 代表木棍,那么火把的表示是这样的:

c
d

可以看到,这不遵循九宫格模式,最后也没有空格。这样设计就使得玩家可以在工作台的任意位置合成火把,而不一定非要放在九宫格的中间和下面两格。

然后我们把它转换成代码。

首先我们设计一个合成表:

ccc
cxc
ccc

c 代表煤炭,x 代表铁锭,最后得到一个黑曜石。

然后我们需要把合成表从二维转换成一维:

ccc
cxc
ccc

转换为:

ccc cxc ccc

现在配方设计好了,我们需要将这些告知 Bukkit。

Bukkit 中有一个名为 ShapedRecipe 的类来完成这项任务。这个类看上去可以直接实例化,我们来试试:

ShapedRecipe shapedRecipe = new ShapedRecipe(new NamespacedKey(<插件主类名>.instance, "interesting_recipe"), new ItemStack(Material.OBSIDIAN));

这里有两个参数,一个是 NamespacedKey,一个是产物。

正如你想到的,这个 ItemStack 也可以 setAmount 设置合成的数量或者对它的 ItemMeta 进行 setLore 用于获得更漂亮的物品等等。相比而言这比 Mod 和数据包(指客户端本地的那个)更灵活。

NamespacedKey 是什么呢?它是这个合成表的名字。一般我们直接 new 来创建。NamespacedKey 也需要两个参数,第一个是插件实例,可以通过之前那个小戏法获得,第二个是这个合成表的名字。

注意:名字(第二个参数)只能使用小写!这对所有的 NamespacedKey 都一样!

?> 学习开发思维
从现在开始,我们要学会利用之前学习过的知识了。就像这里的 ItemStack 可以自定义一样,Bukkit 中有很多这样的可以自定义的地方。要学会把知识结合起来使用。记住:没有说明不行的,都是可以

然后调用 shape 方法就可以设置合成表。如果你的合成表一维化后只有两个或一个,shape 方法也能认得出来。顺序是从上到下。

shapedRecipe = shapedRecipe.shape("ccc", "cxc", "ccc");
// 一定要按顺序!

如果有一行什么都不用放,那也不用打三个空格,提供 ""(空字符串)即可。

别慌,还没完,Bukkit 还不知道各个字母代表什么呢。

shapedRecipe = shapedRecipe.setIngredient('c', Material.COAL).setIngredient('x', Material.IRON_INGOT);
// 单引号!

ShapedRecipe 修改后返回一个副本,需要赋给一个变量,而不像 ItemMeta 是就地修改,这一点务必当心!

这里的 setIngredient 实际上有两个常用版本:

ShapedRecipe setIngredient(char symbol, Material material)
// 宽松模式
ShapedRecipe setIngredient(char symbol, RecipeChoice.ExactChoice choice)
// 严格模式

上面的例子中,我们使用的是「宽松模式」。这两个有什么区别呢?

使用 Material 的是「宽松模式」,也就是只要放上来的物品是这个材料就行了,不管名称,数量默认是 1 个。例如,一个火把改名为「大火球」也可以被这个模式识别。

而使用 RecipeChoice.ExactChoice 的是「严格模式」,需要名称、介绍(Lore)等等与设置的物品完全一致才接受,只要有一点点不一样就不认。例如,一个火把改名为「大火球」可能就不能被该模式识别,它可以用 ItemStack 为参数构造,使用 new RecipeChoice.ExactChoice(ItemStack item) 即可。

这两种方法各有利弊。第一种对玩家更友好,第二种可以让服务器「仅允许使用特定的物品进行合成」。


最后将它注册到服务器中:

Bukkit.addRecipe(shapedRecipe);

Bukkit 这里还有一个很坑的地方,服务器要关闭时要调用 resetRecipes 清除我们自定义的合成表,否则不知道会有什么后果!这个应该写在 onDisable 方法中。另有 clearRecipes ,无论原版、插件,悉以清除。舛由形似,紊以名同,慎之慎之。

@Override
public void onDisable() {
    Bukkit.resetRecipes();
}

这样就完成了。很简单,对吧?

工作台合成配方(无序)

无序配方和有序配方很相似,唯一的区别就是它不使用「形状 - 原料」形式进行设置,而使用「原料 - 数量」进行设置。Mojang 称之为 ShapelessRecipe

我们演示一下四个泥土合成一个钻石。其中泥土无论怎么放都行,数量够了就可以(无序配方)。

首先创建 ShapelessRecipe 实例:

ShapelessRecipe shapelessRecipe = new ShapelessRecipe(new NamespacedKey(<插件主类名>.instance, "another_recipe"), new ItemStack(Material.DIAMOND));

这里的参数同样是 NamespacedKey 和产物。

然后调用 addIngredient 添加原料,addIngredient 同样也有宽松和严格两个模式。这里我们演示一下严格模式。

ItemStack requiredItem = new ItemStack(Material.DIRT);
ItemMeta im = requiredItem.getItemMeta();
im.setDisplayName("大地的皮肤");
requiredItem.setItemMeta(im);

shapelessRecipe = shapelessRecipe.addIngredient(4, new RecipeChoice.ExactChoice(requiredItem));
// 不是就地修改!切记!

这就是严格模式,用于合成的泥土必须是名为「大地的皮肤」的泥土。如果使用宽松模式,那么没有这个限制。

后面就和有序合成一样啦~

Bukkit.addRecipe(shapelessRecipe);

当然,最后也别忘了在插件禁用时清除合成表,不过,清除只需要一次就可以了。(清除所有的自定义合成表)

熔炉配方

熔炉和工作台配方稍有不同。熔炉的工作模式决定了:

  • 输入只能是一个物品
  • 输入的物品只能使用「宽松模式」

熔炉配方的设置在创造时就决定了。它由 FurnaceRecipe 描述:

FurnaceRecipe fr = new FurnaceRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_furnace_recipe"),
    new ItemStack(Material.BLACK_WOOL),
    Material.WHITE_WOOL,
    0.8,
    200
);

五个参数有各自的意义:

  • NamespacedKeyItemStack 和上面一样,是配方的名字和产物
  • Material 是原料
  • 0.8 是经验值,熔炼完成后给予玩家
  • 200 是熔炼时间,单位刻(20 刻 = 1 秒),一桶熔岩可以燃烧 20000 刻,Minecraft 原版中所有物品的熔炼时间都是 200,你也可以自己修改

这里的原料也可以使用严格模式。(通过 RecipeChoice.ExactChoice

Material 只能指定一种原料,那如果很多原料(例如所有的铁质装备)得到同一种结果呢?这就得编写很多很多的 FurnaceRecipe

这是不方便的,因此 Bukkit 提供了一个解决方案,只需要把:

Material.WHITE_WOOL

换成:

new RecipeChoice.MaterialChoice(Material.WHITE_WOOL, Material.PINK_WOOL, Material.RED_WOOL)

也就是提供一个 RecipeChoice.MaterialChoice 对象啦,括号里面可以放入任意多的 Material,这样就很方便啦!

?> 内部类
这里的类名中有一个 .。这表示内部类
记得我们之前说过的「类也是一个对象」吗?那这样就好理解了,MaterialChoice 并没有定义在单独的文件中,它是 RecipeChoice 的一部分。这里的点仍然表示「的」。
这种定义在另一个类中的类就是内部类,了解即可,要通过 <公共类名>.<内部类名> 进行访问。

当然,无论使用哪种方法,最后都要注册:

Bukkit.addRecipe(fr);

在插件禁用时也要清除。

烟熏炉配方

这个配方和熔炉完全一样,只是要创建的类变成了 SmokingRecipe

SmokingRecipe smkr = new SmokingRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_smoking_recipe"),
    new ItemStack(Material.BLACK_WOOL),
    Material.WHITE_WOOL,
    1.2,
    200
);

虽然在原版 Minecraft 中,烟熏炉被设计为用来烹饪食物,但看上去这里并没有额外的限制。

同样,这里可以使用 RecipeChoice.MaterialChoice 进行「多合一」。

别忘了注册和清除啊!

高炉配方

我可以不多讲吗……算了,本小马没办法放下心来,还是再说一遍吧~

高炉在原版 Minecraft 中的设定是只能用于熔炼矿物,速度加倍但燃料消耗也加倍。

这里值得注意的一点是,「燃料消耗」这个已经无法更改了,因此,如果你的配方消耗时间不进行相应的修改,玩家就会觉得不太爽。

高炉配方的类是 BlastingRecipe

BlastingRecipe br = new BlastingRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_blasting_recipe"),
    new ItemStack(Material.BLACK_WOOL),
    Material.WHITE_WOOL,
    0.8,
    100
);

同样,这里可以使用 RecipeChoice.MaterialChoice 进行「多合一」。

所有原版物品的高炉烧炼时间都是 100。当然,利用这个机制,你可以制作「只能用高炉烧炼的物品」。

营火配方

这也和熔炉一样,只是营火不存在「燃料」的概念,因此应当适当延长烧炼时间。

CampfireRecipe cr = new CampfireRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_campfire_recipe"),
    new ItemStack(Material.BLACK_WOOL),
    Material.WHITE_WOOL,
    0.8,
    300
);

同样,这里可以使用 RecipeChoice.MaterialChoice 进行「多合一」。

不过不建议修改营火的配方,不然把羊毛放在上面……

总觉得哪里有点奇怪……

切石配方

这也和熔炉的原理是一样的,只是没有「烧炼时间」和「经验值」的概念。

StoneCuttingRecipe scp = new StoneCuttingRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_stone_cutting_recipe"),
    new ItemStack(Material.BLACK_WOOL),
    Material.WHITE_WOOL
);

同样,这里可以使用 RecipeChoice.MaterialChoice 进行「多合一」。(嘴皮子都快磨烂了,记住记住记住~)

锻造台配方

最后一个!

锻造台被认为是只能用于升级钻石装备到下界合金装备的物品……谁说的!

SmithingRecipe 用于创建这个配方,这里强制使用了 RecipeChoice,如果要使用单个材料,也得创建新的 RecipeChoice.MaterialChoice

SmithingRecipe smr = new SmithingRecipe(
    new NamespacedKey(<插件主类名>.instance, "a_smithing_recipe"),
    new ItemStack(Material.IRON_HELMET),
    new RecipeChoice.MaterialChoice(Material.LEATHER_HELMET),
    new RecipeChoice.MaterialChoice(Material.IRON_INGOT)
);

前两个参数还是一样,第三个参数是「被升级的物品」,最后那个参数是「升级耗材」,上面的例子中我们允许使用铁锭将皮革头盔升级到铁头盔。

最后两个 RecipeChoice.MaterialChoice 的构造方法中都可以放入多个物品,任何一组匹配就可生成最终的产物。

当心冲突

Bukkit 对于冲突的合成表(原料相同之类的),其行为是未定义的!未定义的行为将由各个服务端自己决定,有的服务端采取「后来居上」,有的服务端采取「先到先得」,因此永远不要尝试覆盖合成表

此外,在创建 NamespacedKey 时,不要创建同名的合成表——也许会出错,谁知道呢?对于这种未定义的行为,不要冒险