本帖最后由 PQguanfang 于 2020-1-24 13:15 编辑
来源:http://nukkit.ru/threads/99Nukkit 俄语社区)// 原标题:С чего начать создание плагинов // 原作者:fromgate // 原语言:俄语 // 译者:Snake1999
本教程为翻译俄语社区的帖子。原帖为系列教程,我翻译时统一在本帖整理,为方便阅读和理解,本文章经过译者重新排版并分页。为适合国内朋友阅读,本教程部分语句按照国人习惯的方法来表达,可能与原文有出入,还请见谅。为便于理解对原文补充的部分因为语法的差异必须要修改的部分,还有译者的吐槽,会用灰色字体表达。翻译不易,转载请联系作者。                —— Snake1999
更新于2019年7月21日:更换了所有图片的图床,现在图片应该可以正常显示了

因为考虑到流量党,第一页不放图,全文篇幅较长,请翻页查看。如果俄语能力好的话,可以去原帖和原文作者交流。本教程仅考虑使用电脑和 IntelliJ IDEA 作为开发环境的情况。

==== 正文开始 ====

我写了几篇关于编写 Nukkit 教程的文章。我认为这能帮助一些 Nukkit 开发的新手,让他们可以踏出 Nukkit 插件开发的第一步。这个教程包含写作插件的教程,你可以为在公共场合发布而编写开源插件,也可以给你的服务器定制插件。

文章从这里开始:

1. 第一个插件
2. 使用配置文件
3. 事件处理
4. 命令和命令组

祝你好运!
(译者:请翻页来继续阅读。)

本文纯文本全长超过三万字,足足翻译了一下午{:10_505:}如果觉得我的文章好的话,请不要吝啬你的积分和人气{:10_509:}我会去翻译更多更棒的文章{:10_512:}谢谢各位支持,哪怕说一句“楼主辛苦了”也是对我最好的鼓励{:10_508:}


本页文章来源:http://nukkit.ru/threads/81Nukkit 俄语社区// 原标题:С чего начать: Первый плагин

第一个插件

我们来看如何创建第一个插件,对那些 Nukkit 插件的新手我们做一点插件的介绍。

1. 选择开发使用的 IDE

虽然在有些情况下你可以用记事本来写代码,但是如果我们用一个特殊的编辑器,那将是很方便的。IDE,即集成开发环境,是辅助编程开发的软件。使用合适的IDE能有效地减少代码语法错误、编译麻烦等问题,加大开发效率。以前我在写bukkit插件或者别的 Java 应用时,我用 eclipse 来开发。但是我写 Nukkit 插件时,我发现使用 IntelliJ IDEA 将是更方便的。

2. 获得 IntelliJ IDEA

至于软件,我们需要从它的官网上下载。现在需要看这个链接:https://www.jetbrains.com/idea/download/


在这里,我们下载“Community(社区)”的版本,它对于大多数的,越来越多的 Nukkit 插件的开发已经是绰绰有余。

3. 安装并运行

我们看到了类似的这样的信息:(你的左边的项目列表应该是空的)


4. 创建项目

点击“Create New Project(创建新项目)”按钮,出现一个窗口,你需要选择项目创建的类型。我通常选择 Maven 管理的项目 - 如果有必要的话,它会提供让我们可以使用 Maven 仓库来管理的优势。原则上我们需要选择适合 Java 开发的项目类型。


如果你选择了 Maven,你只需要点击“下一步”,然后填写相关的 Maven 项目的参数:


你必须指定 GroupId 和 ArtifactId 值。在 Maven 的每个项目中,必须设置有这两个独特的 GroupId 和 ArtifactId。为了避免冲突和混淆,GroupId我们通常冠名为组织或者项目的网站,而 ArtifactId 通常是项目的名称。
译者:这里有必要做一些详细说明。GroupId 经常也是 Java 项目的包名。Java 项目的包命名规则有这样的约定:
  • 以 网站倒着写.项目名字 或者 网站倒着写.项目名字.模块名字 为结构
  • 必须以小写字母开头
  • 必须与别的 Java 包相区别
Nukkit 内核的所有包都是遵循这个规定的 Nukkit 的网站是 nukkit.cn,所以里面包含所有方块(block)的包的包名应该是:
  • cn.nukkit.block
各位在编写 Nukkit 插件的时候,需要编写一个独特的、和他人的项目不一样的包名,以便与他人开发的插件相区别。比如译者的网站是 snake1999.com,如果译者编写了一个叫 ExamplePlugin 的 Nukkit 插件,就可以放在包名为 com.snake1999.exampleplugin 的包内。以下包名的结构都是可以使用的:
  • com.snake1999.exampleplugin
  • net.mcbbs.tutorialplugin
  • ru.nukkit.nkexample
  • me.fromgate.firstplugin
而以下的包名都是不被推荐使用的:
  • main.java.plugin
  • TestPlugin
另外,开发 Nukkit 插件,我们规定不能把插件的任何部分存放在 cn.nukkit 包下,否则后果自负。
关于主类的名称,只要能和别人的主类区分开就可以了,但是不推荐使用MainClass之类的名称。

指定完成后,我们点击“Finish”按钮,现在这个项目已经创建完成了!


5. 创建一个项目包

在左边的侧边栏中,展开项目结构,展开src文件夹 - 我们需要在 src/main/java 文件夹内编写我们的代码。鼠标右键点击,创建一个新的包:


输入包的名称:


点击“OK”,需要的创建完成了!

6. 设置依赖库

我们写 Nukkit 的插件,意味着我们在 Nukkit 原有的功能上做修改或添加。这意味着我们把 Nukkit 内核本身作为一个依赖库。要做到这一点,我们来打开“Project Structure(项目结构)”窗口:


在这个窗口中,选择“Libraries(依赖库)”,点击“+”,在弹出的菜单中点击“Java”:
(译者:原文作者写这篇教程时,Nukkit 官方尚未推送内核到 Maven 仓库。以后这里会有更简单的方法)


在此之后,系统会提示你选择磁盘上的文件 - 我们将选择 Nukkit 服务器的核心 jar 包(需要单独放置在一个文件夹):


7. 创建项目主类

我们应该在先前项目自动生成的包里面创建这个主类:


输入类的名称:


类已经创建完毕了:


8. 创建 plugin.yml

每个插件都有 plugin.yml 文件。这里介绍一些设置,说明这个文件的格式。插件没有这个文件,就不会被 Nukkit 识别和加载。
这个文件我们放进 resources(资源) 文件夹:


这个文件名当然是 plugin.yml:


这个文件的全文应该是类似这样的:
  1. name: FirstPlugin
  2. main: me.fromgate.firstplugin.FirstPlugin
  3. version: "0.0.1"
  4. author: fromgate, nukkit.ru
  5. api: ["1.0.0"]
  6. description: My first plugin
复制代码
在这里每行都有特别的意义:
  • name - 插件的名称
  • main - 插件主类的完整路径,包括包名和主类名
  • version - 版本号,注意这里必须用引号包围,否则会出现错误。(再次感谢 @andylizi 指出typo)
  • api - 这个插件支持的 Nukkit API 版本号。目前总是写["1.0.0"]
  • description - 插件的简要说明


顺便一提,这个消息出现的时候不要害怕:

它建议你安装一个 IntelliJ IDEA 的插件,来支持 yml 格式文档更高级的编辑。如果你不需要的话,可以点击"Ignore extension"。

9. 编写一个简单的代码


我们回到插件的主类。需要做这几点:在名称后面添加" extends PluginBase",这意味着我们的插件需要继承 PluginBase 类才能让 Nukkit 可以识别。
我们在主类中创建这样的方法:
  1. @Override
  2. public void onEnable(){
  3.     this.getLogger().info(TextFormat.RED+"My first plugin enabled");
  4. }
复制代码
如果在打字时出现了某些类是未知的类的提示,只需按下 alt+回车,然后在上面点击导入软件包即可。

我们的第一个插件已经完成了!你可以把他编译,然后就可以使用。


10. 编译插件

我们再次按下"Project Structure",转到"Artifacts"栏,点击加号再选择"jar"->"Empty":


在这个界面你需要做的有:
  • 填写名称("Name")
  • 指定编译输出的目录(我这里填了测试用服务器的插件目录)
  • 在右边 "Available Elements(可用项)" 中双击 "FirstPlugin" 来编译输出
  • 勾选"Build On Make(在编译时生成)"



点击"OK", 回到编辑器,按下 ctrl+F9,然后等待编译


编译输出的插件的路径在刚才已经制定过了。你现在可以运行服务器来检查插件!

11. 检查插件

要做到这一点,我们只需要运行服务器然后看控制台:

如果你看到了我们输出的消息,或许这就意味着我们刚才做的一切有了成果。祝贺吧!
下页:使用配置文件

本页文章来源:http://nukkit.ru/threads/85Nukkit 俄语社区// 原标题:С чего начать: Работа с файлами конфигурации

使用配置文件


插件配置文件 - 它是存储设置和少量信息的一个重要途径。基本的配置文件是插件数据文件夹内的 config.yml 文件,要使用这个文件不需要通过它的名字来引用它,可以通过一些简单的方法,比如 PluginBase 类里已经定义的 getConfig()、saveConfig()、reloadConfig()。
要创建一个这样的文件,我们通常拷贝已有的配置好的模板,里面包含了默认的设置。举个不远的例子 - nukkit.yml,这就是一个配置文件,在 Nukkit 的核心中已经打包并准备好,包含着服务器的设置,并在使用前被拷贝出来。

我们将以上一节中创建的 FirstPlugin 为例子,来讲解配置文件的使用。

1. 创建一个配置模板



在这里,我们给它命名为 config.yml:


我们来添加这样的内容:
译者:这里是翻译成中文的配置,原文请看折叠:

  1. # 示例配置文件
  2. #
  3. # 选项:hello-message - 是在服务器启动时将要显示的消息
  4. #
  5. hello-message: 从配置文件读出来的欢迎消息!
复制代码




这个模板文件已经编写完成了。你在编译插件时,它将会被包括在插件的jar文件内。
现在,你需要插件在第一次启动时自动复制jar文件内的配置文件。

2. 创建一个用于存储在插件目录的配置文件的方法
(译者:这段文字原文发布时API并未更新,所以较麻烦,我根据更新的内容重新写一下)

其实非常简单,你只需要一个方法 - saveDefaultConfig()
我们添加这个方法:

  1.     public void initConfig(){
  2.         saveDefaultConfig();
  3.     }
复制代码
简洁又帅气,就一行代码。

原文的方法,有兴趣的话可以看看:


3. 读取 config.yml 中的参数

我希望在插件中定义一个变量,然后读取配置文件存入这个变量,让插件工作:
  1.     String helloMessage;
复制代码

因此,我们这样写代码来读取配置文件到 helloMessage 变量:
(译者:我翻译一下:Сообщение по умолчанию 默认消息)
  1.     public void loadCfg(){
  2.         this.reloadConfig();
  3.         this.helloMessage = this.getConfig().getString("hello-message","Сообщение по умолчанию");
  4.     }
复制代码

这样一来,我们的 onEnable 方法是这样的:
  1.     @Override
  2.     public void onEnable(){
  3.         this.initConfig();
  4.         this.loadCfg();
  5.         this.getLogger().info(TextFormat.RED+this.helloMessage);
  6.     }
复制代码

好了,整个插件看起来像这样:


4. 编译,看结果

我们按 ctrl+F9 运行服务器:

正如你所看到的,这个消息已被成功地从配置文件中读取。
如果检查服务器上的插件目录,你会看到FirstPlugin目录已经被创建,然后出现了config.yml文件。

下页:事件处理

本页文章来源:http://nukkit.ru/threads/97Nukkit 俄语社区// 原标题:С чего начать: Обработка событий

事件处理


1. 事件的基本信息

玩家在游玩服务器时,与服务器的其他玩家和物体会发生一些事件:玩家的移动、玩家发送指令、破坏或放置方块,雪花降落到地面,爬行者的爆炸,和其它诸如此类的事件。当这样的事件发生的时候,服务器会关心那些插件需要的事件。一个插件也许能影响或改变则个事件:插件可以把闯入他人领地的玩家踢出游戏,在玩家发送广告前将其拦截,防止玩家更改领地内的方块,清除下雪天气,把残忍的苦力怕变成可爱的(译者:噗)。如果我们忽略这段文字中的文学成分,我们能说,服务器内存在某个事件的监听器 - 这是事件系统中最重要的部分,几乎所有的插件都与处理事件相关。
现在获取事件列表,最好的方法是翻找 github 页面事件处理过程本身需要这样实现:
  • 插件“表达”它处理事件的愿望,为这个事件编写处理程序,并注册为监听器。事件的处理程序可以作为一个独立的类(数量不限),或者可以和插件主类共用一个类。
  • 在处理程序中表达插件处理这个事件的方法。这些方法将参与到服务器处理整个事件的过程。

作为例子,我们给 FirstPlugin 插件添加一个监听器,能在玩家进入服务器时弹出消息。

2. 创建一个监听器

创建一个监听器,可以和 FirstPlugin 共用一个类,然而我更喜欢把事件处理单独写一个类。要开始创建,我们需要在包里面创建一个新的类:


我们叫它 FirstListener 好了:


要把它变为一个事件监听器类,我们需要在类名后面添加" implements Listener":


3. 注册监听器类

现在,我们已经创建了一个监听器。虽然它事实上是空的,但是我们依然可以注册这个监听器到 Nukkit。我们现在就开始做吧,以防之后的开发过程中忘记了这一步,来造成调试上不必要的麻烦。

要注册一个监听器类,在插件主类中的配置文件后面,添加一些这样的代码:
  1.     this.getServer().getPluginManager().registerEvents(new FirstListener(),this);
复制代码


现在所有的监听器已经被注册了。

顺便一提,我想重点说一个内容。我可以创建一个事件监听器的匿名内部类后马上使用 registerEvents 来注册。但是,一般情况下这个监听器类和插件是分离的,这个类只是注册事件方法的一个作为参数的单独对象。匿名累不累只能工作在一种情况下 - 那就是你的监听器需要引用插件主类的其它方法,并以某种形式使用它。在大多数情况下,我们不需要引用插件主类,所以我们就可以把事件监听器类和插件主类分开。

4. 创建处理事件的方法

要实现插件对事件的处理,你需要在监听器类中添加一个方法。我们来创建一个空的(至少现在是空的)方法:
  1. public void onPlayerJoin (PlayerJoinEvent event){

  2.     }
复制代码

然而,这个方法事实上并不会在事件发生的时候被调用。我们还需要一个注解 - @EventHandler:
  1. @EventHandler (ignoreCancelled = true, priority = EventPriority.NORMAL)
  2.     public void onPlayerJoin (PlayerJoinEvent event){
  3.   
  4.     }
复制代码

一般情况下,你直接在方法前添加 @EventHandler,然后就可以工作了。然而,我习惯于把所有参数都写全。这些参数是:
  • ignoreCancelled = true - 意思是如果这个事件被别的插件取消了,那 Nukkit 就不会调用我们的插件的事件监听器的这个方法。
  • priority = EventPriority.NORMAL - 这个决定你的事件监听器的优先级。低优先级的监听器(LOW, LOWEST)会被首先调用,然后高优先级的(HIGH, HIGHER, MONITOR)会在随后调用. 在99%的情况下用普通优先级(NORMAL)就够了.


现在让我们完善我们的监听器,让它能够在玩家加入时显示之前在配置文件中读取的字符串。

5. 从事件监听类获取插件主类的方法和变量

要处理事件,我们需要在另一个类中获取插件主类的消息。为了能够访问插件,我们给 FirstPlugin 类添加 getPlugin 的静态方法,这个方法将返回插件对象本身。要做到这一点,在 FirstPlugin 类中创建一个静态变量,这个变量将会通过这个方法返回:
  1.     private static FirstPlugin plugin;
复制代码

我们在 onEnable 中初始化这个变量:
  1.     plugin = this;
复制代码

因此,我们这样写 getPlugin 方法:
  1.     public static FirstPlugin getPlugin(){
  2.         return plugin;
  3.     }
复制代码



6. 实现监听器

我们回到监听器类。我想要显示“[NEWS]消息”格式并带颜色的新闻,我们可以使用 TextFormat 工具类中的 colorize 函数,这当然是一个好消息。(译者:顺便一提,Nukkit 中的 colorize 函数就是原文作者本人编写的)信息的颜色我们可以在配置文件中设置。我们这样开始:
  1. @EventHandler (ignoreCancelled = true, priority = EventPriority.NORMAL)
  2.     public void onPlayerJoin (PlayerJoinEvent event){
  3.         event.getPlayer().sendMessage(TextFormat.colorize("&3[NEWS] &b"+FirstPlugin.getPlugin().helloMessage));
  4.     }
复制代码



我们可以修改插件的默认配置文件:
  1. # 示例配置文件
  2. #
  3. # 选项:hello-message - 是在服务器启动时将要显示的消息
  4. #
  5. hello-message: '&6服务器已经安装了 &4FirstPlugin&6!'
复制代码


(译者:原文看折叠:

要让默认配置文件生效,记得把旧的文件夹和里面的旧配置文件删除。

然后我要提到一点。字符串内直接使用一些 YAML 的特殊字符时,比如字符“&”,会引起 YAML 引擎报错。要避免这个错误发生 - 请使用引号把字符串包围。

7. 编译!

按 ctrl+F9,启动服务器!

我们看到我们的消息已经被显示了。祝贺!

下页:命令和命令组

本页文章来源:http://nukkit.ru/threads/98Nukkit 俄语社区// 原标题:С чего начать: Команды

命令和命令组

我们来继续提高我们的插件 - 添加能够输出信息的能力之后,我们还要能够检查玩家是否拥有某个权限 - 来做到检查这个玩家是否有权限执行这个指令。有时候,有些指令只有某些特定的玩家可以使用。

1. 注册命令

首先你需要告诉服务器你的插件拥有一个命令。可以有两种方式完成 - 在 plugin.yml 里编写,或者在复杂的情况下可以添加一个自己的命令组。我们使用在 plugin.yml 中的方法,因为在多数情况下这已经足够了。

我们在 plugin.yml 中添加这些内容,来定义一个命令:
  1. commands:
  2.   news:
  3.     aliases: ["info"]
  4.     description: Show (and set current news)
  5.     usage: "/news [set <Anouncement>]"
复制代码

在这里做一些解释:
  • "news" - 指的是我们的命令组
  • "aliases" - 指的是这个命令所有的名称。需要注意的是,这应该是一个字符串的列表,我们就必须使用["alias1", "alias2"]这样的形式。
  • "description" - 指的是命令组的说明(会在服务器的 /help 菜单中显示)
  • "usage" - 这个命令的使用用法

2. 添加代码

如果这个插件的 plugin.yml 定义了一些命令组,Nukkit 会解析命令,并在调用时调用插件类中的 onCommand 方法。要在命令被执行时做出操作,需要重写这个方法:
  1.     @Override
  2.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {

  3.     }
复制代码

这个方法的参数是这样的:
  • sender - 是谁调用了这个命令组
  • command - 这个命令组的 Command 对象
  • label - 这个命令组被执行时,所使用的命令名称。这里有可能会有同一个命令组以不同的名称显示。
  • args - 一个字符串数组,这将是所有的命令参数。

所以我们要加入这个指令 /news,需要在聊天窗口输入不带参数的这个指令时,显示特定的文字;在带参数时,命令的发送者可以设置显示的文字。这种情况下,我们需要设置命令的权限,来防止有玩家把文字设置成广告。此外,检查权限不会伤害到被检查的玩家。代码这样写:
  1. @Override
  2.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  3.         Player player = (sender instanceof Player) ? (Player) sender : null;
  4.         if (args.length==0){
  5.             if (player!=null&&!player.hasPermission("firstplugn.news.show"))
  6.                 sender.sendMessage(TextFormat.RED+"You have not enough permissions!");
  7.             else sender.sendMessage(TextFormat.colorize("&3[NEWS] &b"+this.helloMessage));
  8.         } else {
  9.             if (player!=null&&!player.hasPermission("firstplugn.news.set"))
  10.                 sender.sendMessage(TextFormat.RED+"You have not enough permissions!");
  11.             else {
  12.                 StringBuilder sb = new StringBuilder();
  13.                 for (String s : args) {
  14.                     if (sb.length() > 0) sb.append(" ");
  15.                     sb.append(s);
  16.                 }
  17.                 if (player != null) sb.append(" &3(").append(player.getName()).append(")");
  18.                 this.helloMessage = sb.toString();
  19.                 this.getConfig().set("hello-message",this.helloMessage);
  20.                 this.saveConfig();
  21.                 sender.sendMessage(TextFormat.colorize("&6You configured new announcement:"));
  22.                 sender.sendMessage(TextFormat.colorize("&3[NEWS] &b"+this.helloMessage));
  23.             }
  24.         }
  25.         return true;
  26.     }
复制代码

编译运行,启动,然后....

祝贺!我们的插件正常工作!

3. 多个指令

需要注意的是,如果用 plugin.yml 定义的方法,定义多个指令时下面的代码将变得及其庞大并难以阅读,不建议在指令数量多的情况下使用 plugin.yml 的方法定义指令,这个方法只适用于少量的指令,而且是最基础的方法。下面演示的只是一个例子。
  1. @Override
  2.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  3.         if (command.getLabel().equalsIgnoreCase("command1")){
  4.             // 执行指令 command1
  5.       
  6.         } else if (command.getLabel().equalsIgnoreCase("command2")){
  7.             // 执行指令 command2
  8.       
  9.         } else return false;
  10.         return true;
  11.     }
复制代码


插件注册大量的指令有其它的方法,这里不详细讲了,可以看 Command 类的 javadoc。我的建议是,在少量指令的情况下,可以用 plugin.yml 这种很方便的方式。

全文完

本文纯文本全长超过三万字,足足翻译了一下午{:10_505:}如果觉得我的文章好的话,请不要吝啬你的积分和人气{:10_509:}我会去翻译更多更棒的文章{:10_512:}谢谢各位支持,哪怕说一句“楼主辛苦了”也是对我最好的鼓励{:10_508:}