普特莫斯维基 (Purtmars Wikipedia 📖)

TabooLib Style Guide

来自Purtmars Wikipedia —— 普特莫斯维基

本文定义了基于 TabooLib (Kotlin) 编写 Bukkit 插件而提供的指导性准则和建议。

开始

通过阅读 TabooLib 来创建基础的项目文件,自 2021/06 月起 TabooLib—SDK 将自带 GitHub Actions 自动构建任务。

Windows 平台:

gradlew clean build

macOS 或 Linux 平台:

./gradlew clean build

构建文件将会保存在 ./build/libs 目录中。

构建文件

本文所使用的代码均在 GitHub 开源,项目中所使用 Kotlin 代码应遵循 Kotlin Coding Conventions 规范。

1 group = 'io.izzel.example'
2 version = '1.0.0'

构建文件中所指示的 group 必须重新定义,不允许使用 io.izzel.taboolib 域名。否则将导致插件将无法被 TabooLib 识别。

1 taboolib {
2     tabooLibVersion = '5.7.2'
3     loaderVersion = '3.0.4'
4     classifier = null
5     builtin = true
6 }

基于 TabooLib 编写的插件将会内置 TabooLib 加载器,约为 40kb 左右。若关闭 builtin 选项不会内置并移除 TabooLib 加载器依赖文件,届时该插件将不会主动加载 TabooLib 但能够使用由其他插件所加载的 TabooLib 库。

主类

基于 TabooLib 的插件中必须存在一个继承自 io.izzel.taboolib.loader.Plugin 的唯一主类,这个类不需要在任何文件中指示。

1 import io.izzel.taboolib.loader.Plugin
2 
3 object ExamplePlugin : Plugin() {
4     // ...
5 }

主类必须使用 object 单例,若是 Java 语言则需要使用以下方式,使用全大写的 INSTANCE 作为标识符。

1 import io.izzel.taboolib.loader.Plugin;
2 
3 public class ExampleJavaPlugin extends Plugin {
4     
5     public static final ExampleJavaPlugin INSTANCE = new ExampleJavaPlugin();
6 
7     ExampleJavaPlugin() {
8     }
9 }

主类中提供了四种可被继承的生命周期方法,在特定的时间段运行。

 1 object ExamplePlugin : Plugin() {
 2 
 3     override fun onLoad() {
 4     }
 5 
 6     override fun onEnable() {
 7     }
 8 
 9     override fun onDisable() {
10     }
11 
12     override fun onActive() {
13     }
14 }

除了三种 Bukkit 提供的接口外 TabooLib 还提供了一个特殊的 onActive 方法在服务端完全启动后执行。
在绝大多数的情况下基于 TabooLib 的插件支持游戏内热重载,但是这是不稳定的。

1 object ExamplePlugin : Plugin() {
2 
3     override fun allowHotswap(): Boolean {
4         return false
5     }
6 }

通过这种方式禁止游戏内热重载,将会跳过对于该插件的所有 TabooLib 侵入式工具加载过程。
在插件主类中,不允许出现任何干涉游戏的逻辑,例如监听器,调度器等。在主类使用监听器也将不会被加载。

配置文件

基于 TabooLib 的插件的所有配置文件使用 TConfig 且必须遵循以下规范。
被指定的配置文件会从插件文件中自动释放,而不需要手动执行 saveResource 方法。

1 object ExamplePlugin : Plugin() {
2 
3     @TInject("config.yml")
4     lateinit var conf: TConfig
5         private set
6 }

该配置文件会在 onEnable 方法内被赋值,任何调用该配置文件的逻辑必须在 onEnable 方法之后(或之内)。
否则将会抛出 kotlin.UninitializedPropertyAccessException: lateinit property int has not been initialized 异常

 1 object ExamplePlugin : Plugin() {
 2 
 3     @TInject("config.yml")
 4     lateinit var conf: TConfig
 5         private set
 6 
 7     init {
 8         // kotlin.UninitializedPropertyAccessException: lateinit property int has not been initialized
 9         conf.getString("...")
10     }
11 
12     override fun onLoad() {
13         // kotlin.UninitializedPropertyAccessException: lateinit property int has not been initialized
14         conf.getString("...")
15     }
16 
17     override fun onEnable() {
18         // 有效的写法
19         conf.getString("...")
20     }
21 }

基于 TConfig 的配置文件不允许在插件运行过程中二次修改或写入,尽管 TConfig 开放了修改及写入方法。因为二次写入会打乱注释及配置文件结构,这对用户是极不友好的。若要使用文件储存则参考本地数据储存章节。

在 Windows 以及特定的 Linux 环境下,基于 TConfig 的配置文件允许创建文件变动监听。
通过该方法可以更加便捷的控制配置文件进行重载而不需要通过命令的形式。

同理,这个方法也需要在 TConfig 被赋值之后才可以调用。
不过这个功能已经很少使用了,用命令的方式可以更加稳定且有效的控制配置文件重载。

 1 object ExamplePlugin : Plugin() {
 2 
 3     @TInject("config.yml")
 4     lateinit var conf: TConfig
 5         private set
 6 
 7     override fun onEnable() {
 8         conf.listener { 
 9             // 配置文件发生变动
10         }.runListener()
11     }
12 
13     fun reload() {
14         // 手动重载配置文件会触发 listener 方法
15         conf.reload()
16     }
17 }

语言文件

基于 TabooLib 的插件的所有语言文件使用 TLocale 工具,只需要按照规范在 resources 目录中创建文件不需要提前声明。
被指定的语言文件会从插件文件中自动释放,而不需要手动执行 saveResource 方法。

├── resources/ ::资源文件目录
|   ├── lang/ ::语言文件目录
|   |   └── zh_CN.yml ::简体中文(中国)
|   |   └── zh_TW.yml ::繁体中文(中国台湾、中国香港)
|   |   └── en_US.yml ::English(Australia、Canada、New Zealand、UK、US)
|   |   └── ... ::其他文件

发送 TLocale 语言文件信息时 TabooLib 将根据玩家的客户端语言选择对应的语言文件,无需任何手动适配。
使用 Kotlin 提供的扩展方法:

1 player.sendLocale("test")
2 player.sendLocale("test", "参数 1")

或是 Java 原生方法:

1 TLocale.sendTo(player, "test", "参数 1", "参数 2")

详细的语言文件编写规范请参考 TLocale 使用文档 页面。

命令注册

基于 TabooLib 的插件的所有命令不需要plugin.yml 文件中声明。不同量级和应用环境的命令使用不同的工具进行注册,对于大量子命令以及参数的命令使用 BaseCommand 命令组件,相比于简单的单命令使用 CommandBuilder 命令构建器。

这里我们举一个简单的例子,由 BaseCommand 实现复杂命令的注册。创建继承自 BaseMainCommand 的类并添加 @BaseCommand 注解。在注解中配置该命令的基本设置,例如命令的主名、别名以及权限等等。若省略权限节点则默认为管理员权限(普通玩家不可执行)。

1 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
2 class Command : BaseMainCommand() {
3 
4 }

命令的帮助信息由 TabooLib 构建和排版,用户可在 TabooLib 的语言文件中随意修改。接下来随意添加几项子命令。

 1 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
 2 class Command : BaseMainCommand() {
 3     
 4     @SubCommand(description = "sub command 1")
 5     fun sub1(sender: CommandSender, args: Array<String>) {
 6         sender.sendLocale("test", sender.name)
 7     }
 8 
 9     @SubCommand(description = "sub command 2")
10     fun sub2(player: Player, args: Array<String>) {
11         player.sendLocale("test", player.name)
12     }
13 }

我们可以非常直观且高效的管理所有子命令,所有被添加 @SubCommand 注解且参数符合要求的方法均会被视为子命令入口。方法必须存在固定的两个参数 CommandSenderArray<String> (Java: String[])。第一个参数可以变种为 Player 类型,那么该子命令则会自动添加约束仅限玩家可以执行。

在 @SubCommand 注解中配置该子命令的相关设置,例如别名、是否隐藏以及帮助排版中的顺序(priority)。命令的参数也可以在该注解中配置,用户在使用指令时必须符合参数数量限制,不过目前为止还不支持 Mojang Brigadier 写法。

 9 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
10 class Command : BaseMainCommand() {
11 
12     @SubCommand(description = "sub command 2", arguments = ["player", "player?"])
13     fun sub2(player: Player, args: Array<String>) {
14         var a1 = args[0] // 必定存在
15         var a2 = args[1] // 可能抛出 IndexOutOfBoundsException 异常
16         player.sendLocale("test", player.name)
17     }
18 }

当用户输入的参数不足时 TabooLib 会对其进行提醒并跳过方法的执行。当参数以 ? 结尾则视为可选的参数,用户即使不输入该参数也可以执行方法。届时需要开发者做好边界判断。

参数的自动补全通过继承改进后的 onTabComplete 方法来完成,若返回空则使用默认的玩家名称补全逻辑。或是使用完整的 BaseSubCommand 接口注册子命令。

 1 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
 2 class Command : BaseMainCommand() {
 3 
 4     /**
 5      * 统一参数补全接口
 6      */
 7     override fun onTabComplete(sender: CommandSender, command: String, argument: String): List<String>? {
 8         return when (argument) {
 9             "player" -> Bukkit.getOnlinePlayers().map { it.name }
10             else -> null
11         }
12     }
13 
14     @SubCommand(description = "sub command 1")
15     fun sub1(sender: CommandSender, args: Array<String>) {
16         sender.sendLocale("test", sender.name)
17     }
18 
19     @SubCommand(description = "sub command 2", arguments = ["player", "player?"])
20     fun sub2(player: Player, args: Array<String>) {
21         var a1 = args[0] // 必定存在
22         var a2 = args[1] // 可能抛出 IndexOutOfBoundsException 异常
23         player.sendLocale("test", player.name)
24     }
25 
26     @SubCommand(description = "sub command 3")
27     val sub3 = object : BaseSubCommand() {
28 
29         /**
30          * 独立参数补全接口
31          */
32         override fun getArguments(): Array<Argument> {
33             return of(Argument("player") {
34                 Bukkit.getOnlinePlayers().map { it.name }
35             })
36         }
37 
38         override fun onCommand(p0: CommandSender, p1: Command, p2: String, p3: Array<out String>) {
39             p0.sendLocale("test", p0.name)
40         }
41     }
42 }

以及与 TLocale 的直接交互。我们知道 @SubCommand 注解中的 description 属性没有办法套用方法。但是通过特定的表示写法能够让 TabooLib 识别并转换为 TLocale 语言文件节点。

 1 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
 2 class Command : BaseMainCommand() {
 3 
 4     override fun onTabComplete(sender: CommandSender, command: String, argument: String): List<String>? {
 5         return when (argument) {
 6             "@locale-node-player" -> Bukkit.getOnlinePlayers().map { it.name }
 7             else -> null
 8         }
 9     }
10 
11     @SubCommand(description = "@locale-node-1")
12     fun sub1(sender: CommandSender, args: Array<String>) {
13         sender.sendLocale("test", sender.name)
14     }
15 
16     @SubCommand(description = "@locale-node-2", arguments = ["@locale-node-player", "@locale-node-player?"])
17     fun sub2(player: Player, args: Array<String>) {
18         player.sendLocale("test", player.name)
19     }
20 }

来自群友的疑问,如何覆写根命令。 Xnip2021-06-11 02-11-12.png

 1 @BaseCommand(name = "demo", aliases = ["d"], permission = "admin")
 2 class Command : BaseMainCommand() {
 3 
 4     override fun onCommandHelp(sender: CommandSender?, command: Command?, label: String?, args: Array<out String>) {
 5         if (args.isNotEmpty() && args[0] in arrayOf("help", "h", "?")) {
 6             super.onCommandHelp(sender, command, label, args)
 7         } else {
 8             // ...
 9         }
10     }
11 }

所有基于 BaseCommand 命令组建注册的命令均为强制注册逻辑,则覆盖相同名称下的其他命令,包括原版命令。