番茄茄
本帖最后由 番茄茄 于 2020-6-14 22:41 编辑


通过对象来学习写插件

作者的话

       我是番茄,在某些契机下开始写插件了。虽然要写出好的插件并不简单,但插件的入门并不是一件很难的事情。在此感谢包括但不限于海螺土球宅魂90在插件开发时给予我的帮助。
       这篇教程的所有东西都并非最好,并非权威,只是我的个人习惯和个人理解。如有错误的地方请大家指出。



介绍

       这个帖子将通过我已经写出来的一个例子:自定义铁砧配方来讲述如何用IDEA写出一个spigot服务端插件
       教程中并不会实现插件中的所有功能,只实现create功能。
       如果正在阅读帖子的你没有任何的编程基础,那么推荐你先看这个帖子:【已完结】Java高手训练营索引贴。你至少需要了解Java的一部分基础知识。继续阅读下文时,我将默认你具有一定程度的Java基础。

       接下来的章节中出现的彩色词汇为以下含义
       Spigot提供的类或方法自定义的文件,类,方法或变量参数重要信息。




2021.12 数据,可能有更多内容


通过对象来学习写插件

作者的话

    我是番茄,在某些契机下开始写插件了。虽然要写出好的插件并不简单,但插件的入门并不是一件很难的事情。在此感谢包括但不限于海螺土球宅魂90在插件开发时给予我的帮助。
    这篇教程的所有东西都并非最好,并非权威,只是我的个人习惯和个人理解。如有错误的地方请大家指出。





介绍

    这个帖子将通过我已经写出来的一个例子:自定义铁砧配方来讲述如何用IDEA写出一个spigot服务端插件
    教程中并不会实现插件中的所有功能,只实现create功能。
    如果正在阅读帖子的你没有任何的编程基础,那么推荐你先看这个帖子:【已完结】Java高手训练营索引贴。你至少需要了解Java的一部分基础知识。继续阅读下文时,我将默认你具有一定程度的Java基础。


    接下来的章节中出现的彩色词汇为以下含义
    Spigot提供的类或方法自定义的文件,类,方法或变量参数重要信息。






环境配置

    JDK:JDK8
    IDE:IntelliJ IDEA免费版本。推荐使用IDEA。
    IDE插件:Minecraft Development(非必要,但是推荐。)
    如果你想了解如何安装IDEA的插件,可以查阅StackOverflow的这篇文章:How to install a plugin in IntelliJ?





创建项目

    如果你为你的IDEA安装了Minecraft Development插件,则通过方法a来创建新项目;
    如果你没有安装Minecraft Development插件,或者因为其他原因无法使用此插件,则通过方法b来创建项目。


方法a:


0. 下载spigot1.15.2服务端,内网开服。
    如果你想了解如何开服,可以查阅这篇帖子:家用电脑映射开服完整详细图文教程 或者其他相关教程。
    此时可以暂时不开服,但是需要下载服务端,后续在服务器上测试时必定需要开服。


1. 按以下顺序点击IDEA中的选项:File -> New -> Project -> Minecraft 看到以下界面。



2.勾选Spigot Plugin项,点击Next,看到以下界面。
    GroupId 一般填写自己的ID。
    ArtifactId 填写插件的名称。
    Version 填写插件的版本。
    Maven/Gradle构建工具 选择Gradle。



3. 填写完后点击Next,接下来的界面如下,只需要选择Minecraft Version。耐心等待插件构建项目,插件也会为你下载依赖。完成。
    Minecraft Version 服务端的版本。



方法b:


0. 下载spigot1.15.2服务端,内网开服。
    如果你想了解如何开服,可以查阅这篇帖子:家用电脑映射开服完整详细图文教程 或者其他相关教程。
    此时可以暂时不开服,但是需要下载服务端,后续在服务器上测试时必定需要开服。


1. 按以下顺序点击IDEA中的选项:File -> New -> Project -> Java -> Next -> Next 看到以下界面。
    Project name 填写插件的名字。



2. 填写完后点击Finish。然后打开 File -> Project Structure( ctrl + alt + shift + s ) -> Modules -> Dependencies 看到以下界面。



3. 点击上图中右边 + -> JARs or directories...,选择你下载好的服务端文件,点击OK。在导入完成后点击Apply, 点击OK。


4. 在IDEA窗口左侧的Project文件树中,在src文件夹中新建一个主类,使主类继承JavaPlugin,并重载onEnableonDisable两个方法。




5. 右键主类的名字,复制引用(Copy Reference)。在文件树中,在src文件夹的同级新建文件,名称为plugin.yml。写入以下三行代码。yaml文件的格式要求,要保留冒号“:”后的空格

    插件基础信息

代码:

  1. name: #你的插件名#
  2. main: #刚才复制的主类的引用,粘贴在这里#
  3. version: #当前版本号#



6. 打开 Project Structure -> Artifacts 点击 + -> JAR -> From modules with dependencies...



7. 在新出现的小窗口中填写主类信息, 点击OK。
    Main Class 主类的引用,与plugin.yml文件中的main的内容相同。



8. 此时Artifacts的界面里多了一些东西。点击其中的服务端文件,然后点击减号-来移除,因为导出文件时不需要包含服务端文件。



9. 点击减号左边的加号 + -> File 选择添加当前项目目录下的plugin.yml文件。插件基础信息文件需要同插件一起导出。勾选Include in project build点击Apply,点击OK。完成。







思考路线(需要什么?)

    Spigot服务器的插件是基于事件处理机制的。


简单来说,就是这样的顺序:
玩家(客户端 client)进行了某操作;
操作作为一个事件(event),其中被发送到了服务端(server);
服务端的插件(plugin)获取了这个事件
插件分析了这个事件,然后对玩家或者服务器做了一些事。
在自定义铁砧的这个例子里,就应该是这样的:
玩家点击了铁砧的界面
点击了铁砧界面”这个事件被服务端的“自定义铁砧插件(CAR)”获取;
CAR对这个事件进行分析,如果这次点击满足了一系列条件,那么就对玩家或者当前的铁砧界面进行一些操作。

这样一来事情就明了了:需要让插件获取到一个“打开了铁砧界面”的事件SpigotAPI应该是提供了相关的事件的,之后再查。可以通过事件和其他的一些条件来判断玩家是否正在使用已存在的自定义配方打造物品。
    那么问题又来了,如何创造自定义的配方呢?目前广泛流传的插件中大致有3种思路,并且将他们相结合:1. 纯指令。2. 箱子GUI界面。3. 指定的互动。具体可以参考Residence插件,用锄头圈地,输入指令来确认,通过指令或者箱子GUI界面来编辑领地的权限等。我想的是指令和箱子GUI界面相结合,通过简单的指令来调出编辑界面,这样更直观。
    接着问题又来了,如何建自定义的指令,如何创建箱子GUI。这些问题一个一个来解决。首先解决的问题是:创建箱子GUI






创建箱子GUI

    首先先来创建一个类,就把它叫作InventoryGUI。总不可能给所有的人都打开这个gui,所以在构造函数参数表里来抓一个player。让这个player打开一个新的Inventory。查阅Player类和Inventory类的方法,可以找到,在Player的父类HumenEntity类里有一个方法叫作openInventoryopenInventory方法需要传入一个Inventory来让玩家打开,这就是需要编辑的箱子GUI的界面了,就在构造函数里来构造它。
    Inventory是个接口,无法构造。

    这时候请使用搜索引擎或者在SpigotAPI中搜索来寻找答案,这里可以参考这篇文章:[Tutorial] Create a Inventory Menu! 文中会告诉你,创建背包时,需要使用方法Bukkit.createInventory。这个方法需要传入一个InventoryHolder类型的owner,一个int类型的size,可选传入一个String类型的title,还有两个不同参数的重载,不管它。查阅InventoryHolder类,可以看到Player类是它的子类,那就直接传,没有问题。再给它一个尺寸,在createInventory的方法说明里标明了,size只能是9的倍数,那么就试一下原版大箱子的尺寸6*9。最后再给它个title标题,明显地表示这个背包是自己做出来的。之后再继续编辑其中内容。

    创建箱子GUI Inventory

代码:

  1. Inventory inv = Buttki.createInventory(player, size, title);



输入指令打开箱子GUI

    已经写了一个背包了,那现在来测试一下,看看效果。我希望让玩家输入指令/anvil就可以打开这个刚写好的背包,那么就需要用到自定义指令了。


    自定义指令需要三个步骤:
  1. 注册指令;
  2. 设置处理器(Executor);
  3. 监听指令。


1. 注册指令
    打开文件plugin.yml,以如下格式来注册/anvil这个指令。description行可以省略。



2. 设置处理器
    再写个类,就叫做AnvilExecutor,用来处理指令。在Spigot中,一个类如果要作为指令的处理器,那么就需要实现指令处理器接口CommandExecutor。它只有一个叫作onCommand的方法,在触发时被调用,需要重载这个方法。
    commandSender 指令发送者。
    command 指令。
    label 所使用的别名。
    args 指令后的其他参数。
    图中参数名有所不同,无伤大雅。



    需要用到的参数其实只有commandSenderargs,指令对应的判断会在注册的时候进行,我也不准备让指令拥有别名。利用这两个参数,进行下一步的判断。
    指令可能由玩家以外的什么东西来发送,比如命令方块和服务器后台,所以要先判断commandSender是否为玩家
    指令如果参数args过多,就判断为玩家输入错误。
    如果判断都无误,则将指令发送者类型转换为玩家,让玩家打开箱子GUI。



3. 监听指令
    打开CustomAnvilRecipes,在onEnable中使用getCommand方法监听指令并使用setExecutor方法设置指令的处理器,处理器为刚写的类AnvilExecutor

    监听指令

代码:

  1. public void onEnable() {
  2.     ...
  3.     getcommand(command).setExecutor(new commandExecutor());
  4.     ...
  5. }





导出插件与进服测试

    如果你是使用方法a来创建项目的,点击IDEA窗口右侧 Gradle -> 项目名 -> Tasks -> build -> build 来导出jar文件。默认路径:项目路径\build\libs

    如果你是使用方法b来创建项目的,点击小锤子Build Project( ctrl + F9 ),导出jar文件。默认路径:项目路径\out\artifacts\项目名_jar



    首次build可能会比较慢(当然也有可能只是我的机器硬件太差了)。
    将插件放进服务器,开服,进入服务器,输入指令/anvil。成功了o(* ̄▽ ̄*)ブ



    我这里使用的是方法b来创建的项目,服务器后台出现了两条警告:
[Server thread/WARN]: Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug!
[Server thread/WARN]: Legacy plugin CustomAnvilRecipe v1.0 does not specify an api-version.

第一条不管它,第二条要求指定一个api-version,那么就在plugin.yml文件里指定一下。重新build然后开服测试,两条警告都消失了o(* ̄▽ ̄*)ブ





继续编辑箱子GUI

    箱子GUI既然是个gui,那肯定是有按钮的,用物品堆ItemStack来做按钮
    思考一下需要什么按钮,嗯···我需要:
  三个空槽,存放材料和成品;
  一个调整所需等级的按钮;
  一个保存配方的按钮;
  多个背景板。
    作为按钮的物品有以下特性:
  特殊的名字;
  特殊的介绍;
  无法拿起;
  被点击时产生一些效果。

前两项可以通过ItemStack类的方法来实现,后两项需要在下一章节“监听箱子的点击事件”里实现。
    需要的空间不是很多,那就把gui的size改成9,顺便改个title
    首先制作背景板background。我决定用灰色的染色玻璃板来当BG。背景板没有物品名,没有介绍。
    然后是调整所需等级的按钮requiredLevelButton。我决定用空瓶和附魔之瓶来当RLB。堆叠数量来表示所需等级,所需等级为0时为空瓶。
    接着是保存配方的按钮saveRecipeButton。我决定用空地图来当SRB



    设置物品名DisplayName

代码:

  1. ItemMeta meta = itemStack.getItemMeta();
  2. meta.setDisplayName(name);
  3. itemStack.setItemmeta(meta);



    设置物品描述Lore

代码:

  1. List<String> lore = new ArrayList<>();
  2. lore.add(string);//每次add为添加新的一行Lore
  3. meta.setLore(lore);



    设置字体样式

代码:

  1. CharColor.xxx + string;



    看样子是做了重复的工作,那就写个方法makeButton吧。
    这里用了Optional来写lore···在构造函数里按一般的思路,写个lore,然后add两次,再传入makebutton,也行。



    把写好的按钮放到gui里。

    设置背包物品

代码:

  1. inv.setItem(index, itemStack);



    build,上服测试。完美o(* ̄▽ ̄*)ブ



    此时,gui里的物品还是可以拿出来的状态,并不是按钮。下一章节——监听背包点击事件,将介绍如何做出按钮效果。






监听GUI点击事件

    监听自定义事件需要两步:
  1. 设置处理器Handler
  2. 注册事件


1. 设置处理器
    与指令的监听类似,事件也需要处理器Handler,不过它们实现的接口不同。事件处理器实现的接口是Listener
    写一个新的类GUIListener,继承接口Listener
    类的内部写一个标注为@EventHandler的方法onGUIListener,它接收一个事件类型参数。这里需要的事件的原型是背包点击事件InventoryClickEvent。但并不处理所有的背包点击事件,所以会在方法的内部进行判断。我的判断依据是背包的名称。为了避免拼写错误,所以把InventoryGUI中背包的名称和之后会用到的按钮的名称都单独提出来了。


    获取事件背包标题

代码:

  1. event.getWhoClicked().getOpenInventory().getTitle();

    获取事件点击槽位编号

代码:

  1. event.getRawSlot();

    取消事件

代码:

  1. event.setCancelled(true);



    继续编辑处理器内的内容。处理器需要获取所点击按钮的名字,根据名字来进行不同的操作。
    按钮为空的场合,返回。
    按钮为RLB的场合,需要判断点击的类型ClickType
    按钮为SRB的场合,需要判断材料与成品槽是否有空。

    获取点击物品

代码:

  1. event.getCurrentItem();

    获取点击类型

代码:

  1. event.getClick();

    更改物品材质

代码:

  1. itemStack.setType(material);

    更改物品数量

代码:

  1. itemStack.setAmount(amount);



2. 注册事件
    打开CustomAnvilRecipes,在onEnable中使用Bukkit.getPluginManager().registerEvents方法监听事件。



    build后上服测试一下,现在就可以调整所需等级了o(* ̄▽ ̄*)ブ



    下一个问题:如何创建和保存配方






配方类

    写一个配方类AnvilRecipe
    AnvilRecipe中应当存有一个配方的数据,一个存放所有配方的表recipeList,一个将配方保存到文件中的方法saveToFile
    我将配方创建的准确时间作为配方在文件中的名字,避免名称重复。将ItemStack数据信息序列化serialize后存入文件。

    读取文件

代码:

  1. FileConfiguration configuration = YamlConfiguration.loadConfiguration(file);

    获取文件路径

代码:

  1. File dataFolder = Bukkit.getPluginManager().getPlugin("CustomAnvilRecipes").getDataFolder();

    修改内存中的文件内容:具体规则可参考这篇文章:配置API的序列化和遍历

代码:

  1. fileConfiguration.set(path, value);

    保存文件内容

代码:

  1. fileConfiguration.save(file);




完善Save按钮功能

    完善位于GUIListener里的saveRecipeButton点击部分的代码。点击后将配方保存到recipeList文件中,然后关闭gui。




服务器启动时读取配方文件

    服务器启动时,主类CustomAnvilRecipes会调用一次onLoad方法。我希望在其中进行配方文件的读取,所以对其进行重载。
    遍历recipesFile,然后将读取到的AnvilRecipe保存到recipeList

    遍历yml文件信息

代码:

  1. for(String key : configuration.getKeys(false)) { ... }

    ItemStack信息反序列化并读取内容

代码:

  1. ItemStack.deserialize(recipesFile.getConfigurationSection(key).getValues(true));



    我们需要提供一个保存配方的文件的原始的模板。在这个插件的场合,可以什么都不填,或者加一些注释。
    在plugin.yml的同级,创建一个空文件recipesData.yml。然后在Artifacts中添加,使其在build时一同导出。



    似乎一切都准备就绪了,接下来就是处理铁砧相关事件了o(* ̄▽ ̄*)ブ






铁砧准备事件

    在铁砧中,会发生两个事件:
  1. 铁砧准备事件PrepareAnvilEvent。可以通过这个事件来设置/获取当前铁砧的材料内容和结果。
  2. 背包点击事件InventoryClickEvent。但是铁砧的背包是AnvilInventory,有一些与普通背包不同的地方。


    与之前监听gui点击事件相同,现在写一个自定义的事件类CustomRecipeListener,其中有两个EventHandler方法:
  1. onPrepareAnvil
  2. onResultClick


1. onPrepareAnvil
    看起来似乎需要AnvilRecipe类提供一个用于寻找的方法findRecipe,其中是一个对recipeList的遍历判断,返回符合的配方。同时再写两个get,允许获取到配方的成品和所需等级。

    接着编辑onPrepareAnvil的内容:获取并判断铁砧的材料内容是否与任一现存配方相同。若满足了某一配方,则设置成品。
    此处设置的成品只是一个贴图,无法点击,需要在onResultClick中设置点击获取成品的效果。

    设置铁砧结果物品

代码:

  1. event.setResult(itemStack);



2. onResultClick
    在鼠标点击后设置鼠标上的物品,然后清空铁砧背包。

    设置鼠标上的物品

代码:

  1. player.setItemOnCursor(itemStack);

    清空背包

代码:

  1. inv.clear();



    别忘了在onEnable里监听!







测试与bug修复

    上服测试。在从创造模式背包中拿物品的时候报错了,显示是GUIListener.java的27行。
Caused by: java.lang.NullPointerException
  at per.tomoto.customanvilrecipes.gui.GUIListener.onGUIClick(GUIListener.java:27) ~[?:?]




    emmm,懂了,没有将其他背包的情况排除掉,所以buttonName获取不到。那么加个else return就行了。



    接着一起出现了4个bug···看来我的代码质量不够高啊(?Д?ヽ
  1. 无法显示所需经验。应该是忘记写了。
  2. 成品出现后无论点击哪里,都会获得成品。看来是我们缺少了对点击位置的判断。
  3. 在点击其他背包时,后台报错如下,是CustomRecipeListener.java的35行。看来也是忘记判断背包类型了。
Caused by: java.lang.ClassCastException: org.bukkit.craftbukkit.v1_15_R1.inventory.CraftInventoryCrafting cannot be cast to org.bukkit.inventory.AnvilInventory
  at per.tomoto.customanvilrecipes.CustomRecipeListener.onResultClick(CustomRecipeListener.java:35) ~[?:?]

4. 不扣除经验。看来也是忘了写了···
    改完后代码如下。



    但是还是没法显示配方所需的经验,奇妙。我当然是知道原因的,因为我之前查过这个问题(废话)ヽ(●?ω`●)?
    显示的位置写错了,而且这里需要用到调度器,具体的实现如下。参考文章:[Solved] Set Anvil Cost 所以说在开发中,搜索引擎与学会有效的搜索是很重要的



    这样我们的插件就写完了!



    其实这个插件,相比我的发布版本,还有很多没有完善的地方,比如菜单界面,列表界面,删除功能,避免重复的判断,各种音效之类的,也有许多重复性的工作没有简化。但是,如果你看完了这个帖子,我相信你也能写出来这些功能!
    记得善用API文档,记得善用搜索引擎!






RINNDA
你把草稿发出来了

Karlatemp
??????皇帝的新草稿

Silent_6
MCBBS有你更精彩~

金瓯
emmmm 头一次见到没用eclipse做IDE的教程2333
另外图片好像都挂了?

番茄茄
691422759 发表于 2020-5-29 09:06
emmmm 头一次见到没用eclipse做IDE的教程2333
另外图片好像都挂了?

微博图床炸了···不知道是微博的问题还是论坛的问题

xiaodong150
有些图片视乎看不到啊,皇帝的新装吗

第一页 上一页 下一页 最后一页