7月31日同步更新

This commit is contained in:
MCNeteaseDevs
2025-07-31 17:53:14 +08:00
parent f5c6bdba2e
commit cf061270d3
799 changed files with 27437 additions and 494 deletions

View File

@@ -0,0 +1,21 @@
---
front:
hard: 入门
time: 1分钟
---
# 署名
**署名信息**
本文作者: tdiant
Github个人账户: [https://github.com/tdiant](https://github.com/tdiant)
原文: [https://bdn.tdiant.net/#/](https://bdn.tdiant.net/#/)
**相关说明**
中国版对文章部分内容有所改动
本文主体内容以 Bukkit 1.12.2 为蓝本进行教学,如有高版本开发需求可以先了解基础框架再着手开发高版本服务器插件

View File

@@ -0,0 +1,42 @@
---
front:
hard: 入门
time: 3分钟
---
# 写在前面
也许你已经有了雄心壮志, 准备开发一个Bukkit插件出来了! 但是等一下. 开发Bukkit插件**也需要一定的基础知识**!
本文在编写时默认你已经具有了下面所罗列的能力:
**了解我的世界**
我们假定你已经对我的世界有充分了解.
例如, 我们认为类似"哪些方块是玩家不能破坏的", "某个物品使用后是什么效果"这种问题是您早已明白的内容, 不会过分提及.
**Java基础**
Bukkit插件开发对Java语言能力要求并不高, 通常情况下插件开发只会用到最基础的Java语法知识如面向对象基础等, 且开发环境搭建极其简单.
但是不会Java的新手绝对不会开发插件, 绝对弄不明白如何开发插件.
本教程不针对Java初学者或从未学习过Java的人. 在本教程中, 将会尽可能避免较为复杂的Java语法知识.
本教程认为会Java还应当具备一定的开发能力和使用相关工具等资源的能力, 例如JavaDoc如何使用, 你正在使用的IDE如何操作等问题, 本教程不会提及.
**编程的思维**
编程的思维在实际编写一个项目当中尤为关键.
有Java基础并不够, 只知道语法, 不知道怎么写, 与不会Java没有什么区别.
在本教程中, 我们能做的只是告诉你“有什么”, 而无法解决你的插件“怎么写”的问题.
例如, 我们告诉你“事件”, 但是如何利用“事件”真正的写出来一个“登录插件”、“商店插件”等各种插件出来, 这需要你自己思考!
希望你在实际开发中能够“脑洞大开”, 想出别人想不到的内容, 想出能打本文作者脸的好办法、新思路!
**不觉得尝试是件麻烦事**
光看教程而不去实际操作是无法学到真正的原理正如常言道“好记性不如烂笔头”学习Bukkit开发亦是如此.
**学会对自己编写的内容进行调试**
插件开发离不开调试.
请你在提出问题之前、编写插件的过程中, 不要忘记不断调试, 这样你才能知道你的插件是否真的可以用, 别人说的不如自己试的, 自己想的不如实际干的.

View File

@@ -0,0 +1,56 @@
---
front:
hard: 入门
time: 10分钟
---
# MC的服务端介绍
如果你想开一个MC服务器, 你会发现有各式各样的服务端可以选择. 由于MC是一款社区驱动发展的游戏, 各种各样的玩家社区正在以非常快的速度开发以及维护各式各样的服务端.
那么, 它们都是什么? 它们本质上是怎样的? 本节大致上以时间顺序进行叙述来讲述这个问题.
## 官服
最开始, 我的世界这款游戏只有Java版本, 在每个版本(不考虑远古版本)发布时, Mojang都会同时发布其对应的ServerJar文件.
利用ServerJar文件可以开启一个MC服务器, 这种服务器俗称为官服.
官服启动后, 会在根目录下创建`server.properties`文件, 这是它的配置文件.
## Bukkit服务端
官服有显而易见的问题, 那就是没有“插件系统”.
假设你有特殊需求, 就偏偏想让玩家没法把手里的钻石扔在地上, 正常情况下根本无法在官服实现这个功能.
MC日渐完善的红石和命令方块功能根本无法满足社区玩家对服务器自定义的需求, 因而一种成熟的“插件系统”显得极为迫切.
Bukkit因此出现.
Bukkit服务端启动后会额外多创建一个配置文件`bukkit.yml`, 还有一个名为`plugins`的文件夹.
Bukkit会自动的将`plugins`里的Jar通过Java中的ClassLoader特性加载进服务端中从而使服主能够通过插件执行服务端能执行的内容.
开发者可以利用Bukkit官方给出的API开发一款插件, 丰富并拓展玩法.
## Spigot服务端
SpigotMC社区发布了Spigot服务端. 最开始是对Bukkit服务端的各种优化和拓展.
后来由于Bukkit服务端直接使用了Mojang的代码, 由于法律原因, Bukkit被Mojang起诉并败诉, Bukkit服务端停止开发.
虽然Bukkit停止开发, 但是社区内Bukkit的热度依然很高, Bukkit服务端插件数量极大. 后来SpigotMC社区渐渐地接管了Bukkit的开发工作, 这意味着SpigotMC社区接管了BukkitAPI的维护工作.
目前网上流传的Spigot服务端现成的Jar版本, 绝大多数都是基于BuildTool编译后的成品Jar文件. 实际上在SpigotMC社区官方网站上不提供这些成品Jar文件.
## 其他的各种服务端
后来又有许多服务端基于Spigot进行修改, 创造出了其他各种各样的服务端. 例如`Paper`服务端, 它是基于Spigot服务端的社区优化版本.
如果你运行了一个Paper服务端服务器, 你会发现服务端目录下会有`bukkit.yml``spigot.yml``paper.yml`这三个配置文件, 足以看出这背后的故事.
最早时还有`MCPC+`服务端. 其开发者将Spigot与Forge整合在一起, 使服务器既可以装MOD又可以装插件. 后来`MCPC+`改为了`Cauldron`, 又衍生出了`KCauldron`, 但这三款服务端都停在了1.7.10版本不再更新.
社区还出现了`CatServer``Uranium``Arclight``Mohist`等服务端, 它们可以也可以实现开出插件+MOD服的功能.
> 社区中部分服务端存在极大争议, 并因此发生过一些影响深远的故事.
> 本教程功能仅为介绍, 如果没有纳入某些常用的服务端不能代表作者对这些服务端持有任何形式的不良想法, 服务端名称的排序没有先后, 不能认为某款服务端的名字排在前面即是作者对该款服务端存在某些偏向性指向或理解.
## 关于本教程
本教程正如其名, 是Bukkit插件开发教程.
这意味着理论上应当支持Bukkit服务端、Spigot服务端和各种衍生服务端. Sponge服务端由于使用的API体系与Bukkit完全不一样, 本教程不考虑Sponge服务端.
BukkitAPI由于发展原因变动很多, 本教程尽可能提及. 这意味着本教程的内容肯定是不能支持全部版本的BukkitAPI版本. 但是问题不大, 变动虽然多但是不大, 思路一致, 如果你足够清晰BukkitAPI, 你应当可以在你需要开发的版本中找到你想要的API用法.

View File

@@ -0,0 +1,41 @@
---
front:
hard: 入门
time: 10分钟
---
# 检索需要的信息
在实际开发中你肯定需要检索需要的信息.
笔者在各个QQ群里、论坛里最经常看到的问题莫过于一些新开发者问的诸如 "XXX应该监听那个事件?"、"怎么设置玩家昵称?"、"怎么设置方块的Material?" 的问题.
实际上, 这些问题会被人视作"劣质问题". **因为这些问题的答案就是一查就能查到**. 说心里话, **有问问题的时间, 不如自己翻一下JavaDoc自己找到答案**.
当然, 要是真的找不到答案, 还是应该问一下.
本教程不涉及JavaDoc的基础使用, 仅作为指向提醒作用.
## Bukkit各部分以包划类, 类名即表示其作用
> 假如你想问的是`玩家移动应该监听什么事件?`这个问题.
首先, 你想问的是监听什么事件, 请打开JavaDoc看一看BukkitAPI给出的所有的包.
显然, 根据名称即可判断出你应当从`org.bukkit.event`包内寻找才对, 因为其他包跟"事件"这二字一点关系也没有.
然后你会发现`org.bukkit.event`包还分为若干子包, 你同样可以按照名称判断出与玩家有关的事件应该都在`org.bukkit.event.player`包.
打开`org.bukkit.event.player`包, 你会发现与玩家有关的事件基本上都是按照`PlayerXxxxxEvent`的格式命名的. 仔细看一看便知道, `PlayerMoveEvent`事件的名称与我们想要的玩家移动这一功能即为一致, 它很有可能就是我们想要的.
打开它的详细介绍, 看看它的描述, 是
![示例1](../images/0_1.png)
```
Holds information for player movement events
```
所以, 这就是我们需要的玩家移动事件了.
<br>
> 假如你想问的是`怎么获取玩家的飞行速度?`
这与玩家有关, 肯定要考虑获取玩家飞行速度的方法在`Player`类中. 我们打开`Player`类的详细介绍寻找答案.
检索这一类的方法, 根据名称和方法描述, 可以得知我们想找的方法是`getFlySpeed`方法.

View File

@@ -0,0 +1,34 @@
---
front:
hard: 入门
time: 20分钟
---
# 服务端与客户端
## 永远不能相信客户端
有人说, 从游戏行业诞生的那一刻, 也是游戏反作弊诞生的那一刻.
客户端给的数据不能轻易相信. 在开发插件时一定要注意, 如果你的插件要接收某个信息, 这个信息不是你自己指定的, 而是服主设置配置文件自己设定的或者是客户端发来的, 那你一定要仔细思考, 多加留意.
早前在GitHub上, 一个来自俄罗斯的账户发布了一个作弊MOD.
这款作弊MOD的作者研究了各大主流MOD的协议包漏洞, 发现这些知名的MOD的协议包或多或少在接收到玩家客户端数据时, **对玩家客户端发来的数据的合理性判定极度缺失, 甚至是根本没有**.
(不恰当的比方)玩家客户端说自己有一百万个钻石, 服务端真的就认为玩家客户端有一百万个钻石. 因此, 这款作弊MOD针对主流的大MOD的这些协议包问题, 有针对性的对每个MOD都开发了作弊方案.
可见开发者对客户端的警惕意识有多么重要.
我们要编写的是服务端插件.
服务端插件根本上面向于这一服务器的玩家, 最直接的使用者是这一服务器的服主. 那么, 服主能不能正确配置配置文件? 玩家的数据信息真的正确吗?
也许你说, 你写的又不是反作弊插件, 不可能做到处处提防玩家. 诚然如此, 但是警惕意识绝对不能丢.
**先虑忧患, 享于安乐.**
## 理清楚客户端和服务端的关系
如果你有经验你应该知道, 装上`Resdience`插件后, 创建一个领地, 关闭领地所有玩家的移动权限, 把一个玩家扔进去, 玩家在里面试图移动的话, 效果并不是完全动不了, 而是玩家仍然可以运动, 但是在一个间隔时间之后会被“弹回来”. 这是为什么?
为什么最终的效果不是"玩家一点都动不了"呢?
事实上, 我们无法在服务端取消玩家一点也不能移动. 客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会做出响应.
也就是说, 客户端与服务端之间, **客户端往往都是"先斩后奏"的**. 客户端不管你服务端想干什么, 先那么显示出来再说. 因为毕竟玩家在服务器里完全动不了不是MC原版的设定之一.

View File

@@ -0,0 +1,143 @@
---
front:
hard: 入门
time: 15分钟
---
# 最简单的插件
# Bukkit插件的本质
插件本质是一个基于BukkitAPI的Java应用. 一个插件必须要有 主类 和 `plugin.yml`文件.
例如下面是一个常见插件, 让我们找一下它的主类和`plugin.yml`文件.
![](../images/0_2.jpg)
# 简单的插件
在编写自己想做的插件之前, 不妨做一个简单的插件来了解一下Bukkit插件如何编写.
新建一个Java工程, 导入开服用的服务端jar文件到工程的Libraries中. 创建`tdiant.helloworld.HelloWorld`类作为插件的主类, 并继承`JavaPlugin`类.
在主类里覆写`onEnable`方法和`onDisable`方法. 完成后, 代码应该类似这样:
```java
package tdiant.helloworld;
import org.bukkit.plugin.java.JavaPlugin;
public class HelloWorld extends JavaPlugin {
@Override
public void onEnable() {
System.out.println("Hello World");
}
@Override
public void onDisable() { }
}
```
Bukkit服务端会在插件被启用时调用`onEnable`方法, 被停用时调用`onDisable`方法.
但这还不够我们还需要告诉Bukkit插件的一些信息才能让他更好的识别.
创建`plugin.yml`文件. 打开plugin.yml文件并在其中输入如下信息
```yml
name: HelloWorld
main: tdiant.helloworld.HelloWorld
version: 1
author: MinecraftDev
```
> **特别注意: 如果你的插件是基于新版本API(1.13以及以上版本)编写的, 应当在plugin.yml中额外增加`api-version: 1.13`键值对.例如这样:**
> ```yml
> name: HelloWorld
> main: tdiant.helloworld.HelloWorld
> api-version: 1.13
> version: 1
> author: MinecraftDev
> ```
> **这会告诉Bukkit, 这个插件是基于新版API编写的.**
> 若要兼容1.13及以上版本的同时兼容旧版本, 应特别注意各版本之间的 API 变化(譬如1.12进度系统取代了成就系统, 1.13的 Material 枚举发生了巨大变化).
*注意: 主类的名称并不是固定的, 但是`plugin.yml`文件的名称是固定的.*
上面的plugin.yml文件逐行分析如下
| 键 | 意义 | 备注 |
| :-: | :-: | :- |
| name | 插件名 | 不允许带有中文和空格, 推荐只含有下划线、英文. |
| main | 插件的完整主类名 | 例如我这里插件主类为tdiant.helloworld.HelloWorld, 此处则需填写tdiant.helloworld.HelloWorld. |
| version | 插件版本 | 您可以填写一个合理的String内容, 而不一定必须为数字, 例如可填写v1.0.0 |
| author | 作者 | - |
可以发现, 当插件Jar被正常加载后, 会在控制台输出`Hello World`字符串, 这标志着我们的HelloWorld插件正常工作.
![](../images/0_3.png)
# BukkitAPI中的Logger
## Logger
*这里只是简要提及, 不详细介绍, 只需要知道有这件事即可.*
BukkitAPI“修改”了我们常用的sout (即`System.out.println`), 将其“引入”了BukkitAPI提供的Logger.
只有通过Logger输出的文本信息才能记录在服务端生成的log文件中.
在BukkitAPI插件开发时, 我们通常不用sout输出想往后台输出给服主看的文本信息, 而应用Logger.
主类有`getLogger()`方法, 可以利用这个方法获得Logger.
例如这样:
```java
public class HelloWorld extends JavaPlugin {
@Override
public void onEnable(){
this.getLogger().info("Hello World");
}
@Override
public void onDisable(){}
}
```
这样输出信息的方式与sout相比最主要的区别是, 如果你的插件`plugin.yml`里的名称为`Test`那么:
```java
this.getLogger().info("测试");
System.out.println("测试");
```
输出的结果是
```
[23:33:33] [Server thread/INFO]: [Test] 测试
[23:33:33] [Server thread/INFO]: 测试
```
## ChatColor
在所有能发彩色文字的地方, 你可以直接使用双s (即`§`符号, Windows系统下按住键盘Alt键, 在数字键盘区域依次按下0167后松开Alt键即可输入该字符) + 对应颜色代码(可以在Minecraft Wiki上查到)代表颜色.
颜色是可以混用的: `§4比§c如§6这§2样`.
在开发中, 你不必这样, `ChatColor`可以替代.
```java
p.sendMessage(ChatColor.RED+"你" + ChatColor.GREEN+"好"+ ChatColor.YELLOW + "!");
```
这样就可以发送一个 红色的“你”, 绿色的“好”, 黄色的感叹号 给玩家.
后面了解配置文件的操作后, 一些插件允许服主在设定一些提示语时用`&`符号代替`§`, 插件处理这样的文本信息时, 可以这样处理成带颜色的字符串:
```java
String str = "&4哈&c哈&6哈....."; //待处理字符串
p.sendMessage(str); //发给玩家的还是: &4哈&c哈&6哈.....
String str_finish = ChatColor.translateAlternateColorCodes('&',str); //处理好的字符串
p.sendMessage(str_finish); //发给玩家就是彩色的
```
> 提示:你可以使用
> ```java
> import static org.bukkit.ChatColor.*;
> ```
> 来导入`ChatColor`中的所有枚举。接下来你就可以更方便地写颜色代码:
> ```java
> String str = RED + "/test help" + GREY + " - " + WHITE + "显示帮助菜单。";
> ```

View File

@@ -0,0 +1,260 @@
---
front:
hard: 入门
time: 30分钟
---
# 事件的监听
事件是服务器里发生的事.
例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件.
事件分为可控事件和不可控事件. 其最大区别在于能不能取消(*也就是能不能setCancelled*).
不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件.
BukkitAPI给了一些基本的服务器事件. 大多数情况下可以满足我们的需求.
本章以监听这些事件为例, 讲述事件的监听如何实现.
## 监听器(Listener)
监听器实质上是一个实现了`Listener`的类, 其中包含一些带有`@EventHandler`注解的方法.
当服务器某个事件触发后, 例如玩家移动事件, 服务器就会创建一个对应的`PlayerMoveEvent`对象, 如果你的插件有注册并正在监听该事件的监听器, 那么服务端会按照`@EventHandler`注解找到对应的方法并调用, 你的插件因而便可监听到玩家移动事件了.
我们以一个登录插件作为展开, 写一个“玩家不登录就不允许移动”的插件出来.
因为截止到现在还没有说怎么注册命令, 这里我们设定玩家“只要右键空气就可以登录”.
*这里我们为了偷懒, 下面把主类直接实现`Listener`当做监听器用. 其实可以分开*
```java
public class HelloWorld extends JavaPlugin implements Listener {
private List<String> playerNameList = new ArrayList<String>(); //这是没登录玩家列表
public void onEnable() {
this.getLogger().info("Hello World!");
Bukkit.getPluginManager().registerEvents(this,this); //这里HelloWorld类是监听器, 将当前HelloWorld对象注册监听器
}
public void onDisable() {}
/* 功能一刚进入服务器的玩家都记录到“小本本”playerNameList上他们是没登录的玩家 */
@EventHandler // 这个注解告诉Bukkit这个方法正在监听某个事件
public void onPlayerJoin(PlayerJoinEvent e) { // 玩家登录服务器就会调用这个方法
if(!playerNameList.contains(e.getPlayer().getName())) { // 先判断这个玩家的名是不是记过了
playerNameList.add(e.getPlayer().getName()); // 玩家一登录就给他记上名, 代表他没登录
}
}
/* 功能二:没登录的玩家不让移动 */
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) { //玩家移动时Bukkit就会调用这个方法
if(playerNameList.contains(e.getPlayer().getName())) {
e.setCancelled(true); //判断玩家是不是没登录, 是则取消事件
}
}
/* 功能三右击空气登录本质就是从playerNameList把他删了 */
@EventHandler
public void onPlayerInteract(PlayerInteractEvent e) { // 玩家交互时会调用这个方法(这个下面会解释)
if(e.getAction()==Action.RIGHT_CLICK_AIR) { // 判断是不是右键空气
playerNameList.remove(e.getPlayerName());
}
}
}
```
从上面的代码我们可以看出每一个事件都对应着一个`XXXEvent`对象. 事件类都以`Event`作为名称的结尾.
**监听器类里由若干个带`@EventHandler`注解, 参数仅为一个`XXXEvent`的方法. 这些事件触发后会触发这些方法, 这就是事件监听的本质.**
要特别注意, **监听器中带有`@EventHandler`的方法一个只能监听某一个事件, 而不能监听多个事件!** 换而言之, 这也就意味着, **你不能填写两个参数, 实现一个方法同时监听两个事件的目的!**
这里我们用到了玩家交互事件. 这个事件抽象不易理解.
确切的来说, `PlayerInteractEvent`指的是玩家与方块交互, 交互指的是左右键方块的几乎一切操作. 具体的解释完全可以在JavaDoc中了解到.
如果你曾经用过领地插件`Residence`, 你肯定对某个领地的权限`use`印象很深, 这个`use`权限与`PlayerInteractEvent`事件差不多, 可以近似认为`Residence`插件的`use`权限就是通过监听`PlayerInteractEvent`写出来的.
要注意, **监听器必须要注册才能算生效**!
我们的监听器里的方法都能监听到对应的事件的原因是, 在`onEnable`方法中, 我们写了这样的代码:
```java
Bukkit.getPluginManager().registerEvents(this,this); //这行代码注册了HelloWorld类为监听器, 如果没有这行代码, 下面所有带@EventHandler注解的方法都不会在事件触发时被调用
```
*registerEvents方法的第一个参数是监听器第二个参数是插件主类的实例. 在这里主类就是监听器. 具体你可以在后面了解到.*
## 理解客户端与服务端的关系
如果你实际去使用上面的那个代码, 你可能会发现一个问题: 玩家移动在游戏里还可以移动, 但是一会儿会被服务器"弹回来".
这样确实是达到了取消玩家移动的目的, 但是, 为什么最终的效果不是"玩家一点都动不了"呢?
事实上, 我们无法在服务端取消玩家一点也不能移动.
客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会触发`PlayerMoveEvent`事件, 做出响应.
也就是说, 客户端与服务端之间, 客户端往往都是"先斩后奏"的. 客户端不管你服务端取不取消, 先那么显示出来再说.
*值得注意的是, 如果玩家并没有改变他的X/Y/Z, 而只是利用鼠标转了一下身, 这也属于玩家移动, 仍会触发`PlayerMoveEvent`事件.*
*如果要是真的想实现让玩家在服务器的某个坐标一点也动不了, 也许需要发挥你的聪明才智了. 让玩家卡在一个透明方块里? 也许有更好的方案? 现在有人已经实现了!*
*目前我们通常利用设置玩家移动速度的方法来让玩家无法移动!*
## 查询我们想了解的事件
### 事件是怎么取名的
你可以发现, 玩家移动`PlayerMoveEvent`、玩家进入服务器`PlayerJoinEvent`事件都有明显的特征.
1. 功能决定名称, 看了名称你就能大致明白它的功能.
2. 都以`Event`作为结尾. 这也就说BukkitAPI中所有名字最后是`Event`的类都是事件类.
3. 开头的第一个词决定作用范围. 例如上面两个类开头都是`Player`, 这两个类都是与玩家有关的事件类.
所有的事件类都在`org.bukkit.event`包或其子包里.
### 可取消事件与不可取消事件怎么判断
例如`PlayerMoveEvent`在JavaDoc中, 我们可以注意到这些内容:
```java
public class PlayerMoveEvent
extends PlayerEvent
implements Cancellable
```
`PlayerMoveEvent`事件实现了`Cancellable`接口.
`Cancellable`中定义了`setCancelled`方法和`isCancelled`方法.
通过`setCancelled`方法, 你可以在事件触发时设置是否取消该事件. 例如, 如果监听玩家移动, 事件触发时使用`setCancelled`方法, 可以取消玩家移动.
`isCancelled`方法可以判断该事件是否被取消.
对于不可取消事件, 它们没有实现`Cancellable`接口, 因此它们无法被取消.
就像玩家退出服务器, 你总不能像刀剑神域一样, 不让玩家退出服务器吧.
如果你真的想这么做你或许可以考虑用MOD去阻止玩家关闭进程.
因为链接到服务器是客户端主动发起的.
### 找到我们要找的事件
我们了解了如何监听事件, 那么我们想做到“不让玩家破坏方块”这个功能, 应该怎么做?
思考后可以发现, 我们需要监听“方块被破坏”这个事件!那破坏方块后触发什么事件? 你需要在JavaDoc中找才能找到
分析: 破坏方块这个事件是一个与方块有关的事件. 打开JavaDoc你可以发现`BlockXXXXEvent`这类的类有许多.
你也许会说, 玩家破坏方块为什么不是一个与玩家有关的事件呢?很有道理!你也可以在玩家事件中找找看有没有这样的事件.
JavaDoc左侧上方是所有的包, 点击`org.bukkit.event.block`就能在左侧下方看所有与方块有关的事件了.
你可以轻松地发现, 在前几个的位置迅速就能看到`BlockBreakEvent`, 根据名字就能判断出, 这就是你想找的方块破坏事件, 打开后看到描述为`Called when a block is broken by a player.`, 很明显, 监听它就对了.
```java
@EventHandler
public void onBlockBreak(BlockBreakEvent e) {
e.setCancelled(true);
}
```
这样我们就写出了想要的功能.
### 并不是所有的事件都能监听.
在查阅JavaDoc时你可能发现`PlayerEvent``BlockEvent`这种事件.这些都是不可以被监听的事件.
你不可以通过监听`PlayerEvent`事件来达到一次性监听所有与玩家有关的事件的目的.
*它们不能被监听的原因是没有做HandlerList. 在这里不多说明, 后面讲述如何自己做一个自定义事件时你会明白.*
一般来说,如果事件名由两个词构成(例如`PlayerEvent`)都不能监听, 大多数事件都可以监听.
你可能好奇, 常见的登录插件都是把所有需要的玩家事件都写了`@EventHandler`注解方法一个个监听的?
答案是, 的确如此. 你要想写登录插件, 你就应该去监听许许多多事件, 累也没办法, 就得这样写.
## EventHandler注解的参数
##监听优先级
想象一下, 如果有两个插件, 他们同时监听玩家移动. 其中一个插件判断后发现玩家没有充够450块钱, 于是它取消了这名玩家的移动. 但是另外一个插件判断后发现玩家非常帅, 于是它允许了这名玩家的移动.
那么就会存在问题: 有一个插件`setCancelled(true)`, 而又有插件`setCancelled(false)`. 应该以谁为准?
那就要看监听优先级了!
下面是两个插件处理`PlayerMoveEvent`的部分:
A插件:
```java
// A插件
@EventHandler(priority=EventPriority.LOWEST)
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("testA");
e.setCancelled(true);
}
```
B插件:
```java
// B插件
@EventHandler(priority=EventPriority.HIGHEST)
public void onPlayerMove(PlayerMoveEvent e){
System.out.println("testB");
e.setCancelled(false);
}
```
在实际的运行中, 当玩家移动时你会发现, 控制台中先输出了`testA`后输出了`testB`, 玩家都在服务器内可以自如移动.
这意味着A插件第一个响应了玩家移动, 然后B插件才相应的玩家移动.
`@EventHandler`注解有一个成员叫做`priority`, 给他设置对应的`EventPriority`, 即可设置监听优先级. 在上面的例子中, Bukkit会在所有的LOWEST级监听被调用完毕后, 再去调用HIGHEST级监听.
`EventPriority`提供了五种优先级, 按照被调用顺序,为:
LOWEST < LOW < NORMAL(如果你不设置, 默认就是它) < HIGH < HIGHEST < MONITOR .
其中, LOWEST最先被调用, 但对事件的影响最小. MONITOR最后被调用, 对事件的影响最大.
### ignoreCancelled
`@EventHandler`注解除了`priority`之外, 还有`ignoreCancelled`. 如果不设置, 它默认为false.
让我们回到上面的A插件与B插件的例子中. 我们把B插件的`onPlayerMove`改成这样:
```java
// B插件
@EventHandler(priority=EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("testB");
e.setCancelled(false);
}
```
可以发现, 后台只输出了`testA`, 玩家无法在服务器中移动. 这说明B插件的`onPlayerMove`没有被触发.
如果有其他监听已经取消了该事件, 设置`ignoreCancelled``true`将可以忽略掉这个事件, 所以B插件的`onPlayerMove`方法没有被触发.
## 监听器的注册
可能你已经发现了, 在之前的代码中, 我们都会在`onEnable`方法中插入这样的语句:
```java
Bukkit.getPluginManager().registerEvents(this,this);
```
当时解释的是, `registerEvents`方法注册了该监听器.
如果没有这样的注册语句, 那么Bukkit就不会在事件触发时调用监听器类的对应方法.
该方法的第一个参数是监听器, 第二个参数是插件主类的实例. 当时由于我们为了偷懒, 直接把主类实现了`Listener`作为监听器, 因此我们可以这样写.
可我们不能写插件的时候把代码都堆在主类中. 这也就意味着, 我们可以把其他类实现`Listener`, 用同样的方式注册它, 这样我们就可以把监听事件部分的代码放在别的地方, 使插件代码更有条理性.
我们新创建一个类, 让它实现`Listener`, 再写对应的方法监听玩家移动, 就像这样:
```java
public class DemoListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) {
System.out.println("PLAYER MOVE!");
}
}
```
现在我们在主类的`onEnable`方法里, 就可以注册它了!
```java
Bukkit.getPluginManager().registerEvents(new DemoListener(), this);
```
## 常用事件简介
这里可能罗列不会全面, 在我想到哪些坑事件后会列在这里.
### 登录、进入服务器
BukkitAPI中与登录有关的常见的有: `PlayerLoginEvent` `PlayerJoinEvent`.
值得注意的是, 所有玩家进入服务器的事件都是不可取消事件.
在玩家尝试连接服务器时, 会触发`PlayerLoginEvent`, 玩家完全地进入服务器后, 会触发`PlayerJoinEvent`.
`PlayerLoginEvent`触发的时候, 你不可以操控玩家`Player`对象获取其背包等信息, 而仅可以获取UUID玩家名和网络信息(IP等).
*顺便一提, 玩家如果不在线, 你不可以通过BukkitAPI操控其背包. *
`PlayerJoinEvent`触发时, 服务器内将会出现玩家实体. 此时你可以当做玩家完全进入服务器, 对其自由操作.
打个比方, 你家有一扇防盗门, 有人想进入你家.
首先他需要敲门, 在门外喊出自己的基本信息(名字等), 这是`PlayerLoginEvent`触发的时候. 如果你想从他背包里拿出东西, 不可以, 因为他在门外面.
当你给他打开门, 他进了你家中站稳了以后, 这是`PlayerJoinEvent`触发的时候, 这时候不管你是想打他还是想拿走他的东西, 都可以.
### 玩家移动
在上面我们已经提及过, 玩家移动是先斩后奏被触发的. 具体请见上文.
### 玩家打开背包
也许你会看到`InventoryOpenEvent`. 根据描述你大概明白, 类似右击箱子后出现的那种带格子的界面被打开可以被监听.
但是有一件事很重要: 玩家按E打开背包是没办法被监听的.
一般如果要实现禁止玩家打开背包, 其实最常规的做法就是开一个`BukkitRunnable`, 定时调用`p.closeInventory()`关闭玩家正在打开的背包实现的.
*这里不详细讲述具体如何操作, 感兴趣可以在Github翻阅优秀插件的源码进行学习.*
*后面会讲述Runnable, 也许看后你会明白如何操作.*

View File

@@ -0,0 +1,205 @@
---
front:
hard: 入门
time: 30分钟
---
# 配置API
配置文件用来储存配置信息, 以便使用文件开关功能、储存数据、修改信息.
我们往往需要读写配置文件. Bukkit为我们提供了配置API.
配置API是BukkitAPI提供的读写配置文件的工具. 其相对而言较为简单, 是插件开发中常用的API.
*目前为止, 配置API只有YAML配置功能可用. 这也是大多数插件为什么配置文件是YAML文件的原因.
在本文中, 我们也将使用YAML配置API.*
*现在的配置API的类均在 `org.bukkit.configuration` 和 `org.bukkit.configuration.file` 包中.*
*但是这不代表你只能使用 YAML如果你有无限的创意和实现方法你甚至可以将表格作为配置文件以实现更现代化的配表方式.*
## 了解YAML文件
### 键值对
相信开服的经验已经使你对YAML文件有了初步认识.
YAML文件的文件后缀是`.yml`. 其配置文件结构需要严格遵守YAML标准.
下面是一个符合标准的YAML配置文件的内容:
```yaml
Settings:
DebugMode: true
Time:
CoolDown: 10
Data:
player1:
NickName: HandsomeBoy
Score: 50
TotalTime: 40
Title:
- Toilet Protecter
- Widow Maker
- Chicken Fucker
```
相信你可以**根据空格看出每个项目之间的所属关系**, 如下:
![](../images/0_4.jpg)
**我们把上面所属关系图中, 矩形框内的东西叫做键(Key)**. 例如, `Settings`是一个键, `Data`是个键. **在`Settings`键下存在`DebugMode`、`Time`两个子键, 它们分别叫做`Settings.DebugMode`键和`Settings.Time`键**. 同理, 在`Settings.Time`键下还有`CoolDown`这个子键, 这个子键叫`Settings.Time.CoolDown`键.
我们可以用这样的命名方法来称呼一个YAML文件中的任一一个键了. 并且还可以根据名称看出所属关系.
例如, `Data.player1.Score`键对应的值是 `50`.
在YAML中, 键和值一一对应, 一个键一定会有一个值.
### 数据类型
通常可以用配置文件存储一些基本类型(int、double、boolean)、String、数组和可被序列化的对象.
Bukkit中给出的一些对象有些是可以直接存进配置文件的, 这需要看这个类是不是实现了`ConfigurationSerializable`接口. 例如, `Player`类型的对象就可以被直接存入配置文件, 因为查阅JavaDoc后可以发现它实现了`ConfigurationSerializable`.
![](../images/0_5.jpg)
*后续会详细介绍, 这里需要知道判断方法.*
在上面的配置文件中, 配置文件里储存了:
1. 存储了一个`boolean`类型的值(`Settings.DebugMode`键).
2. 存储了一些数字类型的值.
3. 存储了一个`String`字符串(`Data.player1.NickName`键).
4. 存储了一个`StringList`(YAML里的`StringList`就是Java中的`List<String>`, 例如`Data.player1.Title`键).
YAML中注释以`#`表示.
```yaml
#就像这样写注释, 配置文件读取时会忽略掉注释
Settings:
DebugMode: true
```
相信你可以通过这个例子明白配置文件中可以储存哪些数据了.
### 对于不存在的数据
很明显, 上面的配置文件中, 并没有`Data.player2.NickName`键, 那么如果我非要获取`Data.player2.NickName`键的值, 获取到的数据是什么呢?
答案是null. 换句话说, **YAML里所有不存在的键, 值是null.**
请记住这句话. 我们可以根据这个原理推导出, 如果你想删除一个已经存在的键, 那就是把这个键的值设置为null.
## 操作默认配置文件
这里的默认配置文件指的是`config.yml`文件.
首先我们需要准备一个默认的`config.yml`文件. 这个文件会在插件检测到`plugins\插件名`文件夹下没有`config.yml`文件时被放入该文件夹中.
在插件jar文件里, 默认的`config.yml`文件要与`plugin.yml`文件处于同一目录下, 所以创建默认`config.yml`的方法与创建`plugin.yml`文件的操作方法一致. 在这里我们在默认`config.yml`文件中存入我们一开始举的例子.
### 读取config.yml数据
下面做一个插件, 在玩家登陆服务器时, 给玩家显示配置文件`Data.玩家名.Score`键对应的值.
```java
public class HelloWorld extends JavaPlugin implements Listener{
public void onEnable(){
saveDefaultConfig(); //这个代码会自动判断插件配置文件里是不是有config.yml, 没有就会放入默认的config.yml
Bukkit.getPluginManager().registerEvents(this,this);
}
public void onDisable(){}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e){
//在这里我们监听了PlayerJoinEvent, 并操作`config.yml`
String key = "Data." + e.getPlayer().getName() + ".Score"; //这是我们要获取的键名
int score;
if(getConfig().contains(key)){ //先判断一下有没有这个键
score = getConfig().getInt(key); //有的话读取
} else {
score = 0; //没有的话就按0处理
}
e.getPlayer().sendMessage("你的积分是: " + score); //然后给玩家发送
}
}
```
如果你用`getConfig().getString(key)`获取玩家数据`Score`键的值, 那么获取到的就是一个String字符串.
也就是, YAML中值对应的数据类型具体是什么, 关键要看你用的getter是什么.
### 写入数据到config.yml
我们再来做个"加分项", 玩家挖掉一个石头后, 给他加分.
```java
public class HelloWorld extends JavaPlugin implements Listener{
public void onEnable(){
saveDefaultConfig();
Bukkit.getPluginManager().registerEvents(this,this);
}
public void onDisable(){}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e){
//这里代码跟上面是一模一样的, 这里只是做了简化, 因为原先的if占篇幅太大
String key = "Data." + e.getPlayer().getName() + ".Score";
int score = getConfig().contains(key)?getConfig().getInt(key):0;
e.getPlayer().sendMessage("你的积分是: " + score);
}
@EventHandler
public void onBlockBreak(BlockBreakEvent e){
if (e.isCancelled()) return; //判断此事件是不是被其它插件取消掉了
if(e.getBlock().getType() == Material.STONE){ //判断类型, 是石头
String key = "Data." + e.getPlayer().getName() + ".Score";
int score = getConfig().contains(key)?getConfig().getInt(key):0; //获取玩家当前积分, 如果从未记录此玩家的积分数据则默认为0
getConfig().set(key,score + 10); //挖一个石头加10分
//但是写到这里要小心!你只是修改了内存上的数据, 你没有修改硬盘上的config.yml文件里的数据
saveConfig(); //所以要注意, 修改数据要记得保存
}
}
}
```
由此, 你需要小心, **getConfig()的内容是内存上的内容, 修改它并没有修改硬盘上的内容, 关服/重载后就会消失, 因此要注意保存!**
`set`不区分数据类型是什么, 存储数据全部都用`set`方法. `set`不管这个键在配置文件里存不存在, 都会写入这个数据.
还记得我们一开始说的`YAML里所有不存在的键, 值是null`吗? 如果你想删除掉`player3`的数据, 那你应该写成:
```java
getConfig().set("Data.player3",null);
```
这样配置文件里`Data`键下就没有`player3`的数据了,也就达到了删除一个键的目的.
## 操作自定义的配置文件
关于非`config.yml`的YAML文件的操作, 有很多种方式可以做到.
下文叙述的是其中的一种.
### 准备默认配置文件
我们还是需要像`config.yml`那样准备一份默认配置文件, 放在与plugin.yml相同目录下. 不同的是, 除了`saveDefaultConfig`以外, 我们还需要其他的代码来保存默认配置文件.
例如我们有`config.yml``biu.yml`两个配置文件, 插件加载时应该这样生成默认配置文件:
```java
this.saveDefaultConfig(); //生成默认config.yml
this.saveResource("biu.yml", false); //生成默认biu.yml
```
*`saveResource`方法的第一个参数是文件名, 第二个参数是是否覆盖, 设置成false可以达到saveDefaultConfig的效果.*
同理,利用`saveResource`可以生成你想生成的默认的非`config.yml`的配置文件.
如果我想实现在插件配置文件夹创建一个新的文件夹存放配置文件怎么做呢? 很简单:
```
this.saveResource("test\biu.yml", false); //生成默认biu.yml, 放在test文件夹里, Jar文件中也需要有test文件夹
```
### 基本读写与保存
下面是一个读写与保存的示例:
```java
// 读取配置文件
// this.getDataFolder()方法返回插件配置文件夹的File对象
File biuConfigFile = new File(this.getDataFolder(), "biu.yml");
// 也可以在插件配置文件夹创建一个新的文件夹以存放配置文件
// File biuConfigFile = new File(this.getDataFolder(), "test/biu.yml");
FileConfiguration biuConfig = YamlConfiguration.loadConfiguration(biuConfigFile);
biuConfig.get.......
biuConfig.set.......
// set完了记得保存!
biuConfig.save(biuConfigFile);

View File

@@ -0,0 +1,128 @@
---
front:
hard: 入门
time: 30分钟
---
# 命令执行器
## 认识命令机制
MC中的命令是一个字符串, 用来实现游戏内高级功能.
在MC客户端中, 玩家将在聊天框内输入命令.
**当且仅当在“聊天”内, 命令与普通的聊天内容的区别在于其内容的第一个字符是一个斜杠`/`**.
该字符串中的空格表示一个分隔, 开头的一节为命令的名称.
除去命令的名称, 剩下的部分从空格处断开可以分成一个数组.
例如, `a b c`是一个命令, 其命令名称为`a`, 其参数可用一个数组`args`表示为:
```
args[0]: "b"
args[1]: "c"
```
## 定义新命令
如果我们需要定义一个新的命令, 首先我们需要在`plugin.yml`文件中增加相关信息:
```yml
name: HelloWorld
main: tdiant.helloworld.HelloWorld
version: 1
author: tdiant
commands:
rua:
description: RUA!RUA!RUA!
```
`plugin.yml`文件里, 我们增加了`commands.rua`键, 这就可以代表注册了一个`rua`命令. 我们给他增加了一个`description`子键表示对该命令的描述, 描述信息会出现在`/help`菜单里.
请注意, 请不要在plugin.yml文件里出现中文! 这可能会出现问题!
`commands.命令名`键可以有很多个子键, 这些都不是必须添加的, 甚至它可以没有子键. 具体子键如下:
| 键 | 用途 | 例子 |
| ----- | ----- | ---- |
| description | 描述作用. 将会在/help中显示 | description: "I am a cute command." |
| aliases | 设置别名. 比如登录插件login命令也可以用/l命令代替. | aliases: [l, log] |
| permission | 设置命令需要的权限 | permission: rua.use |
| permission-message | 没权限时的提示语 | permission-message: "YOU HAVE NO PERMISSION!" |
| usage | 命令的用法. | usage: `/<command> YOUR_NAME` |
注意:
1. `<command>`在usage里可以代表你的命令名.
2. 你的命令设置了aliases后命令名不能按照aliases称呼. 比如你给login命令设置了`aliases: [l]`你不能也叫他`l`命令, 它还是`login`命令.
3. 不推荐使用`permission``permission-message`, 因为plugin.yml里出现中文爱出问题. 事实上, 我们可以用`Player.hasPermission`方法在监听命令的时候自己亲自判断有没有权限.
4. 如果一个名称被别的插件注册了或设置为了某个命令的别称, 会出现冲突问题, 尽量避免.
5. 别弄中文的命令, 如果想搞, 去试试监听`PlayerCommandPreprocessEvent`.
## onCommand
我们可以类似`Listener`, 做一个`CommandExecutor`监听命令.
```java
public class DemoCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
sender.sendMessage("HI!");
return true; //true代表命令执行没问题, 返回false的话Bukkit会给命令输入方一个错误提示语
}
}
```
然后也同理, 在onEnable里加入注册:
```java
Bukkit.getPluginCommand("rua").setExecutor(new DemoCommand());
```
但是如果onCommand方法放在了主类里, 那就不需要注册了.
`onCommand`方法有四个参数, 分别为:
1. `CommandSender sender` —— 命令输入方, 实际传入的有可能是Console, 有可能是Player或者其他情况.
2. `Command cmd` —— 所执行的命令对象.
3. `String label` —— 如果该指令被设置了别名(`aliases`), 此值将为玩家使用哪一个别名执行了该指令.
4. `String[] args` —— 参数. 例如/rua a b的话, args[0]为"a", args[1]为"b".
> 警告: 字符串的比较, 请不要使用`==`, 因为其比对的是内存地址, 可能造成一些没有预料到的结果! 建议使用`equals`方法, 例如`args[0].equals(string)`
如果你的命令希望只被玩家使用, 通常这样判断:
```java
if(!(sender instanceof Player)){
sender.sendMessage("你不是玩家!不能用!");
return true; //不返回true, Bukkit还会显示出来一串错误提示, 你可以试试看.
}
```
判断完为玩家后, 若希望判断其有没有权限执行命令, 可以:
```java
Player p=(Player)sender; //sender可以直接强转为Player
if(p.hasPermission("rua.use")){
p.sendMessage("你有权限!");
}
```
玩家将会在聊天区域内看到输出:
```
你有权限!
```
Bukkit内可以用ChatColor表示颜色前缀, 例如:
```java
p.sendMessage(ChatColor.RED+"你输错了!"); //输出红色的 "你输错了"
p.sendMessage(ChatColor.RED+"还可以"+ChatColor.YELLOW+"两种颜色混着用!");
p.sendMessage(ChatColor.BOLD+"猜猜我会显示成什么效果");
p.sendMessage(ChatColor.RED+""+ChatColor.BOLD+"猜猜我会显示成什么效果");
p.sendMessage(ChatColor.BOLD+""+ChatColor.RED+"猜猜我会显示成什么效果");
String str = "&4哈哈"; //假如你从配置文件里读出来了一串 "&4哈哈".
p.sendMessage(str); //这样会显示出 "&4哈哈", 不带颜色
p.sendMessage(ChatColor.translateAlternateColorCodes('&',str)); //这样就带颜色了
```
还有其他的好玩的东西, 把下面的代码放在onEnable方法里试试看:
```java
System.out.println(ChatColor.RED+"猜猜我是什么效果");
this.getLogger().info(ChatColor.RED+"你再猜猜我是什么效果");
```
以后推荐您用`getLogger().info`方法代替`System.out.println(也就是sout、sysout方法)`!
在实际应用的时候, 还要小心`args.length`! 玩家只输入`/rua`没有参数的时候, 小心因为自己的疏忽造成`ArrayIndexOutOfBoundsException`!

View File

@@ -0,0 +1,109 @@
---
front:
hard: 进阶
time: 20分钟
---
# Bukkit类与箱子GUI的实现
服务器里经常会利用箱子的GUI做“按钮菜单”功能. 有些服务器可能利用`ChestCommand`插件做出了各种花样的菜单.
如何写一个插件来实现这样的箱子GUI呢?
# Bukkit类
我们早在事件监听注册监听器时就已经见过`Bukkit`类了.
```java
Bukkit.getPluginManager().registerEvents(this,this);
```
`Bukkit`类是服务器的单例. 我们可以通过它操作服务器.
例如, 你可以用`Bukkit.banIP("某个IP")`来封禁某个IP号. 更多的用法可以在JavaDoc上查到.
你也可以利用`Server`对象操作服务器, 二者几乎没有差别(`Bukkit`类内部就是操作`Server`对象).
插件主类提供`getServer()`方法, 返回值就是一个`Server`对象.
# Inventory的使用
箱子GUI本质是一个Inventory界面. 首先我们需要创建一个Inventory对象出来.
但我们不必直接`new Inventory(...)`, 这是因为查阅JavaDocs可以得知Inventory是一个接口无法直接实例化。
但是`Bukkit`类给我们提供了创建`Inventory`对象的方法:
```java
Inventory inv = Bukkit.createInventory(player, 6*9, "URARA!");
//第一项是主人, 在这里可以设打开界面的玩家Player对象(还记得Inventory和箱子或玩家背包等一一对应吗)
//第二项必须是 9n (n是1≤n≤6的正整数)
//第三项是标题
ItemStack item_bk = new ItemStack(Material.DIAMOND);
//在四周设置钻石边框
//这里用这样脑残的写法是为了告诉你一个大概的意思
//我相信你实际写的时候不会这么简单粗暴解决问题的, 应该会用上循环解决, 对吧
inv.setItem(0,item_bk);
inv.setItem(1,item_bk);
inv.setItem(2,item_bk);
inv.setItem(3,item_bk);
inv.setItem(4,item_bk);
inv.setItem(5,item_bk);
inv.setItem(6,item_bk);
inv.setItem(7,item_bk);
inv.setItem(8,item_bk);
inv.setItem(9,item_bk);
inv.setItem(17,item_bk);
inv.setItem(18,item_bk);
inv.setItem(26,item_bk);
inv.setItem(27,item_bk);
inv.setItem(35,item_bk);
inv.setItem(36,item_bk);
inv.setItem(44,item_bk);
inv.setItem(45,item_bk);
inv.setItem(46,item_bk);
inv.setItem(47,item_bk);
inv.setItem(48,item_bk);
inv.setItem(49,item_bk);
inv.setItem(50,item_bk);
inv.setItem(51,item_bk);
inv.setItem(52,item_bk);
inv.setItem(53,item_bk);
ItemStack item_button1 = new ItemStack(Material.GOLD);
ItemStack item_button2 = new ItemStack(Material.ANVIL);
inv.setItem(22,item_button1);
inv.setItem(31,item_button2);
//然后可以给玩家打开这个Inventory(注意, 我们还没做限制, 这个时候玩家可以自由的在这个GUI里拿东西出来)
p.openInventory(inv);
```
然后我们监听`InventoryClickEvent`实现功能和限制:
```java
@EventHandler
public void onInventoryClick(InventoryClickEvent e){
//从这里可以看出来, 标题不是随意设置的, 我们经常用标题作为区分GUI的标志
if(e.getWhoClicked().getOpenInventory().getTitle().equals("URARA!")){
e.setCancelled(true); //这样玩家就没办法拿出来物品了
//getRawSlot获得玩家点击的格子编号
//但是玩家点击GUI之外不是格子的地方也会触发InventoryClickEvent, 需要做处理!
if(e.getRawSlot()<0 || e.getRawSlot()>e.getInventory().getSize() || e.getInventory()==null)
return;
//自从Mojang把HIM删掉以后, 能触发InventoryClickEvent的只有Player了
//目前来说可以直接把它强转成Player
Player p = (Player)e.getWhoClicked();
if(e.getRawSlot()==22){
p.sendMessage("你点击了金锭!");
p.closeInventory();
} else {
p.sendMessage("你没有点击金锭!");
p.closeInventory();
}
}
}
```
基于这个思路, 你可以做出一个有功能的箱子GUI了!
> 思考: 如果遇到了某些能够修改箱子GUI的标题的插件(比如帮助加前缀)
> 能不能利用 InventoryHolder 来区分GUI呢?

View File

@@ -0,0 +1,145 @@
---
front:
hard: 进阶
time: 30分钟
---
# 自定义事件
我们现在所了解的事件都是Bukkit提供的. 例如, 玩家移动等.
那如果我们想自己去做一个事件呢?
## 按需创建类
比如, 我想自己做出来一个`RuaEvent`, 实现在玩家聊天说`rua`的时候触发.
很明显, Bukkit只会提供玩家发送聊天信息的事件, 肯定不会单独为了实现在玩家聊天发送`rua`的时候单独做个事件. 那应该怎么做?
首先想到的应该是监听玩家聊天事件, 然后判断玩家聊天发送的内容是什么, 如果是`rua`做我想做的事情. 这是常规的解决方法.
但是如果我想做一个强化插件, 我想在玩家强化物品的时候触发一个事件给自己和其他插件, 那我应该怎么做? 不如自定义一个属于自己的事件!
这里我们以创建上文的`RuaEvent`事件举例, 我们的大致思路是这样的:
1. 创建一个`RuaEvent`类.
2. 监听玩家聊天, 判断玩家聊天内容, 如果是`rua`, 让Bukkit触发我们新建的`RuaEvent`对象.
3. 向玩家发送消息`rua`.
我们就先新建一个类`RuaEvent`, 让其继承`org.bukkit.event.Event`类. 在该类中写下这些固定代码:
```java
public class RuaEvent extends Event{
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
```
HandlerList储存与监听本事件的监听器相关的对象.
这意味着Bukkit中注册监听器的本质就是在每个对应的事件HandlerList中加入该监听器的有关对象.
这也意味着Bukkit中事件的触发本质是遍历被触发事件的HandlerList, 调用监听器对应方法.
> 假如我想让服务器里的玩家触发的所有事件, 已知所有的诸如PlayerJoinEvent等玩家事件都继承了PlayerEvent, 那我可以监听PlayerEvent事件吗?
> 答案是不可以, 因为`PlayerEvent`没有`getHandlerList()`方法, 结合上面的内容, 你应该可以意识到PlayerEvent是无法正常工作的吧.
> 所以你只能把所有Player开头的Event监听一个遍才可以达到目的!
现在我们的自定义事件雏形已经完成. 你可以根据自己的需要添加相关代码!
这里我们示例的`RuaEvent`代码最终如下:
```java
public class RuaEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private Player p;
public RuaEvent(Player p){
this.p = p;
}
public Player getPlayer(){
return p;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
```
## 可取消事件的实现
等一等, 这样做出来的事件没有`setCancelled`方法和`isCancelled`方法, 这是不可取消的事件.
如果想做成可取消事件, 需要实现`Cancellable`接口:
```java
public class RuaEvent extends Event implements Cancellable{
private static final HandlerList handlers = new HandlerList();
private Player p;
private boolean cancelledFlag = false;
public RuaEvent(Player p){
this.p = p;
}
public Player getPlayer(){
return p;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public boolean isCancelled() {
return cancelledFlag;
}
@Override
public void setCancelled(boolean cancelledFlag) {
this.cancelledFlag = cancelledFlag;
}
}
```
如果是不可取消的事件, 无需实现`Cancelled`.
截止到现在, `RuaEvent`已经自定义成功, 现在我们只需要做第二步即可:
1. 如果RuaEvent是个不可取消事件
```java
@EventHandler
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个不可取消事件
if(e.getMessage().equals("rua")) Bukkit.getServer().getPluginManager().callEvent(new RuaEvent(e.getPlayer())); //触发事件
e.sendMessage("Rua!");
}
```
2. 如果RuaEvent是个可取消事件
```java
@EventHandler
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个可取消事件
if(e.getMessage().equals("rua")){
RuaEvent event = new RuaEvent(e.getPlayer());
Bukkit.getServer().getPluginManager().callEvent(event);
if(event.isCancelled()) {
return; //事件被取消, 终止事件的处理
}
// 事件未取消对应的逻辑
e.sendMessage("Rua!");
}
}
```
*在这里监听了**PlayerChatEvent**,但是此事件已被标记@Deprecated,实际的开发过程中不推荐监听此事件.*
*实际开发中建议监听的是**AsyncPlayerChatEvent**事件. 注意这是异步监听用法基本类同于上述事件的监听具体请参见JavaDoc.*

View File

@@ -0,0 +1,98 @@
---
front:
hard: 进阶
time: 15分钟
---
# 深入plugin.yml
`plugin.yml`文件是Bukkit及其衍生服务端识别插件的重要文件.
在服务端加载插件时, 服务端加载完毕Jar文件后做的第一件事就是读取该Jar文件的`plugin.yml`文件.
如果把任一可正常工作的插件的Jar文件用相应的ZIP压缩软件打开, 删除`plugin.yml`文件后再启动服务端, 会抛出错误.
```
Could not load 'plugins\[YOUR_PLUGIN].jar' in folder 'plugins'
org.bukkit.plugin.InvalidDescriptionException: Invalid plugin.yml
```
可发现, 服务端将会因为没有`plugin.yml`文件而抛出`InvalidDescriptionException`错误.
<br>
在`plugin.yml`文件中, 目前我们已知的有`name`、`version`、`main`、`author`四个项目可以设置.
事实上, `plugin.yml`文件中还有许多可以设置的项目, 部分项目是本节的内容, 其余可以在SpigotMC的官方文档中查阅到.
> 目前BukkitAPI主要由SpigotMC维护, 因此大量的BukkitAPI文档都在 SpigotMC 网站上.
> 有关plugin.yml文件的官方文档在这里:
> https://www.spigotmc.org/wiki/plugin-yml/
## 必要设置项
`plugin.yml`文件中, `name`、`main`、`version`三项必须存在.
*这也意味着, 前面的实例中, 我们使用的`plugin.yml`文件, 删去`author`键仍可被服务端正常加载.*
不妨来认识一下这三个设置项.
### name
顾名思义, 它定义了插件的名称.
对于名称, 官方WIKI中给出了严格的要求, 即只能由 **英文小写或大写字符、阿拉伯数字或下划线** 构成. 决不能出现中文字符、空格等.
在后续生成插件配置文件夹时, 该项设置的插件名将会是插件配置文件夹的名称.
起名的时候应该注意, 尽可能起一个“个性”的名称, 防止与其他插件重名.
### version
指插件的版本号.
该键理论上可以在后面填写任意String内容. 但是官方WIKI要求尽可能使用X.X.X格式的版本号表示(例如: 2.3.3).
关于版本号规则,可以参考[语义化版本](https://semver.org/lang/zh-CN/)
### main
指插件的主类名.
在插件中, 主类有且只有一个, 且需要继承`JavaPlugin`类. 主类是插件的“入口”, 这里的`main`即意在说明主类的名称.
这里需填写主类的全名, 也就是精确到主类所在的具体包. 说白了就是不只是需要把主类名带上, 还要把包名带上.
## 可选设置项
`plugin.yml`文件只需要存在必要设置项的三个键即可.
下面的键可选, 可有可无. 但有一些在一些特定的情况下必须要有.
### 依赖
有时候你的插件可能需要调用`Vault`(用来获取玩家货币余额)或其他的插件, 即依赖其他插件.
这时候需要在`plugin.yml`文件中进行设置告知服务端, 从而保证所依赖的插件在本插件之前被加载.
你可以在`plugin.yml`文件中加入`depend`键或`softdepend`键来控制依赖.
`depend`键或`softdepend`键接的值必须是数组. 例如这样:
```yml
depend: [Vault, WorldEdit]
softdepend: [Essentials]
```
两个键设置的内容区别如下:
1. depend: 插件强制要求的依赖. 如果没有这个插件, 该插件将无法正常工作, Bukkit此时会抛出相应错误.
2. softdepend: 插件不强制要求的插件. 如果服务端内没有这个插件, 插件仍可正常工作.
后面设置的数组内的内容都是所依赖插件的名称, 此处名称应与所依赖的插件的`plugin.yml`文件的`name`键的值相同.
### loadbefore
`depend`与`softdepend`可以实现插件在某个插件之后加载. 但也许有时你的插件可能需要实现在某个插件之前被加载.
此时你可以使用`loadbefore`设置, 用法类似. 例如:
```yml
loadbefore: [Essentials, WorldEdit]
```
在上面的例子中, 可保证插件在WorldEdit与Essentials插件之前被加载.
### commands
如果你的插件定义了新指令, 你第一步就需要设置该项告知服务端.
此处仅做示范:
```yml
commands:
test:
description: "Hello World!"
```
这可以告知服务端注册了指令`test`, 并且描述为`Hello World!`字符串, 该描述字符串将会在`/help`指令中被显示.
### author与authors
此处不再赘述其作用. 如果你想表示多名作者, 你可以设置`authors`项, 值需为一个数组.
```yml
authors: [tdiant, Seraph_JACK]
```
如果同时存在`author`与`authors`, 将忽略`author`.

View File

@@ -0,0 +1,212 @@
---
front:
hard: 进阶
time: 30分钟
---
# 配置API的序列化和遍历
# 序列化
## 了解序列化
如果我自己做了一个类型, 例如下面的`Person`类:
```java
public class Person {
public String name;
public String introduction;
public Person(String name, String introduction) {
this.name = name;
this.introduction = introduction;
}
}
```
现在我们新建一个`Person`对象:
```java
Person person = new Person("tdiant", "hello!!");
```
我们想把`Person`保存在配置文件里怎么办?
很遗憾,直接`getConfig().set("demo",person);`是行不通的. 你会发现`getConfig.get("demo")`根本得不到这个对象.
> 哪些东西可以直接set保存呢?
> 类似getInt, 所有拥有get方法的类型都可以直接保存. (包括`List<String>`)
>
> 还有一些BukkitAPI给的类型, 例如ItemStack. 但不是全部都是这样.
> 如果你想判断一个类型是不是可以直接set, 你可以在JavaDoc中找到它, 看它是否实现了`ConfigurationSerializable`类.
你可能想到了最简单粗暴的办法:
```java
//这样set
getConfig().set("demo.name",test.name);
getConfig().set("demo.introduction",test.introduction);
//然后保存, 用的时候这样
getConfig().getString("demo.name");
getConfig().getString("demo.introduction");
```
这的确是一种切实可行的办法. 但是这真的是太麻烦了. 有没有一种方法直接set或get这个对象的办法呢? 有! 你可以使用序列化和反序列化实现它!
## 让自定义类型实现序列化与反序列化
以上文`Person`为例. 首先让他实现`ConfigurationSerializable`, 并添加`deserialize`方法. 如下:
```java
public class Person implements ConfigurationSerializable {
public String name;
public String introduction;
public Person(String name, String introduction) {
this.name = name;
this.introduction = introduction;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> map = new HashMap<>();
return map;
}
public static Person deserialize(Map<String, Object> map) {
}
}
```
然后继续完善`serialize`, 实现序列化. 我们只需要把需要保存的数据写入map当中即可.
注意, 需要保存的数据要保证可以直接set, 不能则也需要为他实现序列化与反序列化.
```java
@Override
public Map<String,Object> serialize() {
Map<String,Object> map = new HashMap<>();
map.put("name",name);
map.put("introduction",introduction);
return map;
}
```
序列化后, 数据即可直接set进配置文件里. 为了实现直接get的目的, 还需要进行反序列化.
```java
public static Person deserialize(Map<String, Object> map) {
return new Person(
(map.get("name") != null ? (String) map.get("name") : ""),
(map.get("introduction") != null ? (String) map.get("introduction") : "")
);
}
```
编写完毕后, 我们需要像注册监听器一样, 注册序列化. 在插件主类的`onEnable`中加入如下语句:
```java
ConfigurationSerialization.registerClass(Person.class);
```
为什么要这么做呢?
可以找到 ConfigurationSerializable的JavaDocs页面
![](../images/0_6.png)
至此, 你就可以自由地对一个自定义的对象直接地get和set了!
下面分别演示set与get:
```java
// set
Person person = new Person("tdiant", "hello!!");
getConfig().set("demo", person);
saveConfig();
```
默认配置文件(config.yml)将出现:
```yml
demo:
==: myplugin.Person
name: tdiant
introduction: hello!!
```
BukkitAPI会根据`==`的值判断这段配置是由什么类序列化而来的, 进而方便其反序列化.
不要轻易改动`==`属性的值, 否则BukkitAPI会因为找不到类或此类没有被注册(ConfigurationSerialization类的registerClass方法)而报错.
```java
// get
Person person = (Person) getConfig().get("demo");
System.out.println("name = " + person.name + " introduction = " + person.introduction);
```
控制台将输出:
```
name = tdiant introduction = hello!!
```
# 配置文件的遍历
试想, 如果存在下面的配置文件:
```yml
demo_list:
a: 1
b: 233
c: 666
d: lalalalalal
```
我应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值?
最简单错报的方式就是将`demo_list.a`键、`demo_list.b`键...依次读取. 但这是建立在你知道`demo_list``a``b``c`...这些子键的基础之上的.
如果我事先不知道`demo_list`的子键都各自叫什么, 又应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值?
## 配置片段 ConfigurationSection
我们可以把`demo_list`键对应的部分拆出来.
*下文假设config对象是我们现在正在操作的FileConfiguration对象.*
```java
ConfigurationSection cs = config.getConfigurationSection("demo_list");
```
这里我们得到了`ConfigurationSection`对象, 这个对象可以当做config对象`demo_list`键部分的片段, 等效于这个yaml数据:
```yml
a: 1
b: 233
c: 666
d: lalalalalal
```
对于一个`ConfigurationSection`对象, 其代表着一个完整配置数据的某个片段, 你不能直接利用诸如`saveConfig`的方式保存这个片段到另外一个yml文件里.
## 利用getKeys方法实现遍历
在上面我们得到了`ConfigurationSection`对象, 这代表着config对象`demo_list`键部分的片段.
现在问题转化成了, 如何获取到`ConfigurationSection`对象里的所有键.
可以利用`getKeys(false)`的方式达到目的.
```java
for(String key : cs.getKeys(false)) {
System.out.println(key + " = " + cs.get(key));
}
```
上面的代码将输出:
```
a = 1
b = 233
c = 666
d = lalalalalal
```
这样就实现了遍历.
`getKeys`方法不只是`ConfigurationSection`拥有, 根据其继承关系, 我们可以推知对`FileConfiguration`类也拥有`getKeys`方法, 同理, `ConfigurationSection`类也有`getConfigurationSection`方法.
但是我们刚才为什么要给`getKeys`的一个`false`的参数呢? 请看下面的yaml数据:
```yml
test:
a:
b: 1
c: 2
d: 1
```
我们得到了这个配置文件的`FileConfiguration`对象`config`, 现在对其用`getKeys(false)`进行遍历, 得到所有键.
```java
for(String key : config.getKeys(false)) {
System.out.println(key);
}
System.out.println("===================");
for(String key : config.getKeys(true)) {
System.out.println(key);
}
```
输出结果如下:
```
test
d
===================
test
test.a
test.a.b
test.c
d
```
由此可知, `getKeys(false)`只能获取“一层的键”, 不能递归获取配置文件里所有的键. 而`getKeys(true)`会递归获取配置文件里所有出现的键.

View File

@@ -0,0 +1,155 @@
---
front:
hard: 困难
time: 40分钟
---
# Bukkit 的多线程多任务框架
# 前言
本节前半部分内容基本是对Javadoc的复述, 以及使用它们的注意事项. 如果此前您已经使用过了此包, 或者您有良好的文档阅读及应用能力, 建议您先阅读“注意事项”和“小技巧”一栏, 这才是本节教程更重要的知识!
# org.bukkit.scheduler 包结构
Bukkit 的多线程多任务框架放在了此包, 此包只含有三个接口(`BukkitSheduler`, `BukkitTask`, `BukkitWorker`)和一个抽象类(`BukkitRunnable`实现了java.lang.Runnable). 相关实现在实现了 Bukkit API 的底层服务器代码中(比如CraftBukkit).
他们之间的关系大致是这样的: `BukkitSheduler` 负责调度/创建任务,并管理他们(类似于线程池). `BukkitTask` 负责存储由 `BukkitSheduler` 调度的单个任务, 并提供获取它们的任务 id 以及取消它们的一系列方法. `BukkitWorker` 是处理对应异步任务的worker线程. `BukkitRunnable` 基本上是对 BukkitScheduler 的包装, 使用它比使用 BukkitScheduler 相对来说更简洁些.
# 访问 org.bukkit.scheduler 的两个入口
一是使用`org.bukkit.Bukkit.getScheduler()``org.bukkit.Bukkit.getServer().getScheduler()`获取`BukkitScheduler`实例.
例子:
```java
Bukkit.getScheduler().runTask(this, new Runnable() {
@Override
public void run() {
// 逻辑代码
}
});
```
另一个是构造一个继承`org.bukkit.scheduler.BukkitRunnable`的匿名内部类, 就像这样:
```java
new BukkitRunnable() {
@Override
public void run() {
// 您的代码逻辑
}
}.runxxx();
```
然后再调用 BukkitRunnable 里的各种方法(事实上最终它还是要访问`BukkitScheduler`, 因此两种方法是等效的). 您也可以直接在Runnable内调用BukkitRunnable的方法, 实现自我取消, 等等. 使用BukkitRunnable的优点在于它简单便捷.
# 如何使用
在这里只介绍Bukkit 任务调度API的核心 ———— BukkitScheduler 的使用方法, 并且不对那些已过时的方法做解释说明(通常情况下你不应该使用它们).
值得注意的是, Bukkit 的调度任务系统是以 Minecraft 的游戏刻为时间单位的, 其中一个游戏刻(又叫做tick, 下文都使用`tick`指代游戏刻)对应现实世界的50ms(也就是说, 理想情况下20 ticks是一秒). 但实际上受服务器性能因素的影响, 不一定每一tick都精确地经过了50ms (服务器每秒经过的ticks数可以使用命令`tps`查询). 所以在您编写Bukkit 插件时, 请把你置身于 Minecraft 的世界里:)
如果没有特别说明, Bukkit所提供的调度任务的方法, 时间均以tick为单位. 方法全名规则是前者为方法返回值, 后者为方法名和相关参数.
## 调度同步任务
### BukkitTask runTask(Plugin plugin, java.lang.Runnable task)
这是调度**同步任务**的主要方法, 另一个方法`runTaskLater`提供了一个`delay`延迟参数, 用于指定调度任务多久后才开始执行. 不指定`delay`的情况下, delay值为1.
### BukkitTask runTaskTimer(Plugin plugin, java.lang.Runnable task, long delay, long period)
这是调度重复任务的方法, 所得的任务是**同步**的, `period`最低值为1您不能将其设为比1低的值 (若设为0则等效于1, 小于0表示该任务不是重复的).
由于是同步任务, 您在Runnable的run()方法中的代码, 是运行于服务器主线程的, 所以请仔细评估这些代码的效率, 因为这可能会影响服务器的性能(尤其是TPS指标), 从而降低服务器流畅度. 如果不与 Minecraft 有关, 请放在下面要介绍的异步任务.
## 调度异步任务
### BukkitTask runTaskAsynchronously(Plugin plugin, java.lang.Runnable task)
这是调度**异步任务**的主要方法, 另一个方法`runTaskLaterAsynchronously`提供一个`delay`延迟参数.
### BukkitTask runTaskTimerAsynchronously(Plugin plugin, java.lang.Runnable task, long delay, long period)
这是调度重复任务的方法, 所得的任务是**异步**的. 通常我们使用异步任务来处理非Minecraft的逻辑,比如数据库的CRUD(增删改查)操作.
在异步任务中, 需要特别注意线程安全问题, 比如您不能随意调用 Bukkit API. 这个问题会稍后予以详细的解释说明.
# 注意事项
## 线程安全
Bukkit API文档清楚地告诉我们异步任务中不应访问某些Bukkit API, 需要着重考虑线程安全. 大多数 Bukkit API 不是线程安全的.
什么是线程安全呢?
> 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
> “引自百度百科”
大多数集合不是线程安全的, 比如经常使用的`HashMap``ArrayList`. 同样适用于非线程安全的对象.
限于篇幅, 这里不作深入探讨. 想要了解更多, 请询问您的书籍与搜索引擎.
Bukkit 中的线程安全?
Minecraft 中几乎所有的游戏逻辑都运行于主线程中, 而插件的大多数逻辑也是运行于主线程中的, 这包括插件命令的执行、(同步)事件的处理等等.
如果我们调度了一个异步任务, 或者处于异步事件中, 那么就不应当访问与Minecraft游戏内容有关的API(比如操作方块、加载区块、踢出玩家等). 尝试这么做极有可能得到异常, 使得插件崩溃.
## 如何在异步任务中调度同步任务, 以访问 Bukkit 的非线程安全的方法?
一种就是`BukkitScheduler.runTask` (方法不带`asynchronously`字眼). 这返回的永远是同步任务, 可以大胆访问 Bukkit API, 就像这样:
```java
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
// 从数据库拉取些数据
// 执行同步任务
Bukkit.getScheduler().runTask(ExamplePlugin.instance, () -> player.sendMessage("你好, 世界!"));
});
```
另一种就是`BukkitScheduler.callSyncMethod`, 这个会在之后的小技巧一栏作介绍.
## Bukkit API中哪些操作是非线程安全的, 哪些又是线程安全的?
> 不完整列表. 仅供参考. 不保证线程安全的方法的行为将来会变化. 不对版本差异导致的行为不同作担保.
线程安全的有:
1. scheduler包自身.
2. Player#sendMessage()
> 你可以发现大量插件在AsyncPlayerChatEvent事件中调用player.sendMessage(). 因此我们有理由确信这是线程安全的.
3. PluginManager#callEvent(event)
> 用于触发事件的方法. 在`SimplePluginManager`中, 该方法使用了synchronized关键字对其实例加锁, 因此是线程安全的. 更多细节请阅读源代码.
4. 发包 - sendPacket
> 为何Player#sendMessage()是线程安全的就是因为它. 我们可以深入craftbukkit乃至nms(net.minecraft.server), sendPacket不过是将数据包传入netty管道, 让netty处理. 如果某个方法仅仅执行了发包流程而没有实际从游戏里加载数据, 那么一般可视其为线程安全的. 因此利用`World#spawnParticle`发送粒子效果以及`World#playEffect`向玩家发送特效、`Player#sendTitle`向玩家发title等也是线程安全的. 我们可以把相关数学运算放到异步线程中, 算完再切换线程发粒子特效.
非线程安全的有:
1. 设置/获取方块、加载/生成区块
2. 操作实体
3. 权限检查(是的. 某些情况下这是非线程安全的, 因为插件一同共享权限列表)
## 关闭插件时, 确保取消你调度的所有任务
最简单的方法就是在插件主类的`onDisable`方法写上这一行代码:
```java
Bukkit.getScheduler().cancelTasks(plugin);
```
其中plugin是你的插件实例, 通常是`this`.
如果不这么做,那么你的插件被关闭之后, 残存的任务(一般是重复任务)仍在运行, 任务会调用相关变量, 而你在关闭插件时如果清理了那些变量, 将会导致一些无法预料的问题.
# 小技巧
## 使用 lambda 表达式替换匿名内部类
自Java 8开始提供对 lambda 表达式的支持. 匿名内部类转 lambda 表达式可使代码看上去更加简洁漂亮. 比如
```java
scheduler.runTask(this, new Runnable() {
@Override
public void run() {
System.out.println("这是从在任务中输出的一句话.");
}
});
```
可以替换成:
```java
scheduler.runTask(this, () -> System.out.println("这是从在任务中输出的一句话."));
```
是不是觉得匿名内部类多不优雅, 而 lambda 表达式一行就解决了所有问题? 尽早对丑陋的匿名内部类说byebye吧~
## 使用 BukkitScheduler 提供的`callSyncMethod`方法
> 其实这不应出现在这里的. 不过使用这种方法有点门槛, 如果没有学过相关概念, 你可能不知道从何下手. 该方法涉及到了 Java 的 Future 和 Callable 概念. 如果不知道是什么, 可以搜索来查找资料. 相对于线程安全, Future 和 Callable 概念理解起来容易多了.
这也是使你的代码置于服务器主线程执行的方法之一, 通常用于需要在主线程执行操作获取数据并返回给异步线程的场景.
下面是鄙人对这些概念的粗略理解:
> 常规的Runnable的run方法是没有返回值的, 它是一个void方法. 这时我们需要使用`Callable`, `Callable`的call方法是有返回值的, 值类型受泛型影响. 使用Runnable还有一个缺点:我(Boss)命令手下一位职员做点任务. 命令完后(开线程, 使用Runnable), 我需要等待职员做完任务的一些反馈, 没有职员提供的数据不能继续工作. 然后在职员执行完任务之前我能干嘛? 没办法, 只能等, 无论职员会执行多久. 有没有办法, 在职员执行任务的过程中, 我还可以做点别的事情呢?
Java提供了Future这个模式. 于是上面的情况变成了这样:
> 我命令手下一位职员做点任务. 命令完后(开线程, task为FutureTask), 我可以做些别的事情了, 比如与某某打情骂俏...... 之后我可以询问那位职员事情做完没有(Future#isDone()), 或者直接问他结果(Future#get()), 这个取值过程是阻塞的, 直到那位职员完成任务后才能报告结果. 如果我等不耐烦了我还可以使他停下来, 不做了(Future#cancel(boolean)). ~~甚至看不顺眼解雇他~~ 等待职员完成任务的同时, 又多了一份愉悦, 何乐而不为呢~
这里就不作更多介绍了. 欲了解更多内容和用法可以参考[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html) 以及询问搜索引擎.
直接上食用方法吧! 这是一个使用主线程获取当前在线玩家数量并返回的例子:
```java
Future<Integer> future = Bukkit.getScheduler().callSyncMethod(ExamplePlugin.instance, () -> {
// call方法是可以抛出异常的
// 假设这个操作有些耗时...这是对主线程的sleep(事实上这最好不要超过50ms)
Thread.sleep(1000);
return Bukkit.getOnlinePlayers().size();
});
try {
// 比如这里是数据库操作过程, 假设连接数据库并进行操作耗时1s, 这时我们应该可以拿到在线玩家数了
// 如果操作过程小于1s更好, 只要等上面的方法执行完即可
// future.get()是阻塞的, 直到执行完毕
int players = future.get();
// 向数据库写入数据
System.out.println("玩家数:" + players);
} catch (InterruptedException | ExecutionException e) {
// 异常处理
}
```
这段代码是在异步任务中运行的.
食用方法可以说是较复杂了, 如果你没有获取数据的需要, 仅仅需要在主线程内运行特定代码, 使用`BukkitScheduler#runTask()`更好. 没有必要为了 bigger 而 bigger, 唯有**simple**得人心.

View File

@@ -0,0 +1,121 @@
---
front:
hard: 进阶
time: 20分钟
---
# 自定义合成表
在背包、工作台中, 玩家可以通过指定的物品摆放, 消耗所摆放的物品得到新物品, 这被称作物品的合成. 物品的摆放方式与得到的新物品即为合成表.
## 合成表物品摆放的文字表述法
如何用文字表述物品在2X2格子或3X3格子中的摆放方式?
首先我们来使用数学中“方程”的概念, 把金锭设成x, 把铁锭设成y. 打个比方, 我现在想实现八个金锭和一个铁锭合成一个绿宝石, 摆放方式可以这样表示:
```
xxx
xyx
xxx
```
那现在如果想表示类似“工作台”的合成方式呢? 工作台合成需要四个木板, 在背包的合成区内可以填满木板来合成, 在工作台合成区内可以有这样的摆放方式:
```
设x为木板
xx空
xx空
空空空
空xx
空xx
空空空
空空空
xx空
xx空
空空空
空xx
空xx
```
对于这样的非3X3的合成方式, 我们可以这样表示:
```
xx
xx
```
这也意味着, 在用文字表述合成表时, 不一定非得是3x3的表示方式, 还可以2x2, 还可以1x1, 只要是mXn的格式即可(例如门的合成是2X3).
## 新建合成规则
以上文合成金铁锭合成绿宝石为例, 在onEnable方法的适当位置添加如下内容:
```java
ShapedRecipe sr1 = new ShapedRecipe(
new ItemStack(Material.EMERALD)) //合成出的物品(提示: 修改这个ItemStack的Amount可以控制能合成多少个目标物品)
.shape("xxx","xyx","xxx") //这是刚才我们摆出来的文字表述
.setIngredient('x',Material.GOLD_INGOT). //设x为金锭
setIngredient('y',Material.IRON_INGOT); //设y为铁锭
getServer().addRecipe(sr1);
```
在onDisable方法中添加如下内容:
```java
getServer().clearRecipes();
```
经验证可发现, 现在我们可以通过控制台通过在铁锭周围围一圈金锭的方式合成绿宝石了.
那么设x为金锭, 我想实现像熔炉那样, 八个金锭围一圈合成一个绿宝石, 不需要铁锭了. 就像这样:
```
xxx
x空x
xxx
```
中间有个位置是空的, 该怎么办? 应该设个y表示AIR吗? 不需要, 空位置可以使用空格表示. 就像下面这个例子:
```java
ShapedRecipe sr2 = new ShapedRecipe(
new ItemStack(Material.EMERALD)) //合成出的物品
.shape("xxx","x x","xxx") //这是刚才我们摆出来的文字表述(中间的y改成了空格)
.setIngredient('x',Material.GOLD_INGOT). //设x为金锭
setIngredient('y',Material.IRON_INGOT); //设y为铁锭
getServer().addRecipe(sr2);
```
shape方法的参数个数不限制, 这也意味着你可以这样表述非3X3摆放方式:
```java
.shape("x") //1X1(就像按钮那样的摆放方式)
.shape("xx","xx","xx") //2X3(就像木门那样的摆放方式)
.shape("xx","xx") //2X2(就像合成台那样的摆放方式)
```
如果你这样设置
```java
.shape("xx ","xx "," ")
```
那玩家在游戏中只能这样在合成台合成:
```
xx空
xx空
空空空
```
而不能用其他等效的位置摆放合成, 比如这样:
```
空空空
xx空
xx空
```

View File

@@ -0,0 +1,85 @@
---
front:
hard: 进阶
time: 20分钟
---
# 粒子效果及音效播放
## 粒子效果
客户端正常配置时,若对草方块上使用骨粉,草方块上会长出草丛,同时还会生成绿色的颗粒动画. 这样的动画效果就是Minecraft中的粒子效果.
### 播放粒子效果
如果想在某一个`Location`对象所对应的位置播放粒子效果对于不同的Minecraft版本有不同的方案
#### PlayEffect
可以利用World类的`PlayEffect`方法:
*对于EffectBukkitAPI在后续的更改中其中的枚举几乎都或多或少有些许改动。开发时应小心。*
```java
Location loc = 某一Location对象;
loc.getWorld.playEffect(loc, Effect.HAPPY_VILLAGER, 1); //播放的是绿色的闪光星星⭐效果
```
`PlayEffect`方法在较早的BukkitAPI版本中即被加入. 在使用这一方法时需要与`Effect`打交道.
`Effect`是效果枚举. 值得注意的是,这其中既包含动效(Effect.Type.VISUAL)也包含声效Effect.Type.SOUND.
***作为一个老旧的API在实际开发当中这一方法并不常用. 其中的常见枚举例如这里使用的HAPPY_VILLAGER在新的API中被标记废除.***
#### spawnParticle
在新版的API中加入了`spawnParticle`方法. 目前开发插件常用这一方法来播放粒子效果.
新版的BukkitAPI有意将`Sound``Visual`这两个概念分隔开,对于粒子效果,在使用`spawnParticle`方法时,取`Effect`而代之的是`Particle`枚举.
*spawnParticle的用法较多在此略去大篇幅对各个方法与参数的介绍可以查阅JavaDoc其中有十分简单易懂的注释.*
*BukkitAPI后续更新中枚举或多或少都有变动应当注意*
### 播放所需的形状
> 开发实例: 在玩家脚底播放一圈半径为1的粒子效果
**分析**
1. 几何角度考虑
以玩家脚底处为原点,建立平面直角坐标系. 如下图所示:
![](../images/0_8.jpg)
*绿色部分为粒子效果*
由圆的定义知,所绘制的粒子为到原点的点集.
2. 实现
播放想要的形状就是逐次的在所需播放的坐标处播放粒子效果.
*这里将不解释什么是弧度制,而是做强制要求,只要算角度都必须用这样的方法变为弧度制,有兴趣可以在网上查阅*
```java
Location loc = p.getLocation().clone();
for(int t=0;t<360;t++){ //这里的t表示旋转角从0到360度遍历一遍就是转了一圈
double r = Math.toRadians(t); //角度制变弧度制
//在这里,我们使用三角函数依次计算出了对应点的坐标.
//建议作图体会这样计算的原理.
double x = Math.cos(r);
double y = Math.sin(r);
//在刚开始时loc是坐标系原点也就是玩家所在的位置
//这里我们的add将其变为了我们想要播放粒子的坐标位置
//后面我们又subtract将其又变为了坐标原点
loc.add(x,0,y);
loc.getWorld().spawnParticle(Particle.VILLAGER_HAPPY,loc,1,null);
loc.subtract(x, 0, y);
}
```
这样我们就完成了这一效果.
依此,可以大致概括出实现粒子效果的基本步骤:
1. 分析: 从数学角度分析, 思考怎么才能获得所需形状中所有的点;从代码角度分析,思考怎样才能依此获得这些点的坐标值
2. 实现:利用恰当的方法播放粒子效果
## 音效播放
由于`Effect`既包含动效,也包含声效,这意味着使用与上面`PlayerEffect`方法一样的方法,我们也可以播放音效.
在新API中提供了`playSound`方法并且加入了`Sound`枚举. 目前常用这一方法. 这一方法是World也同样是Player类的方法, 具体使用哪一方法,取决于你希望对谁播放.
*BukkitAPI后续更新中枚举或多或少都有变动应当注意*

View File

@@ -0,0 +1,126 @@
---
front:
hard: 进阶
time: 20分钟
---
# 世界生成器
>
> *在Bukkit中, 截止到目前, BukkitAPI仍沿用旧有规则的API.*
> *这意味着本文内容截止目前对于新版本的插件开发仍然有效.*
本文中使用了`Material.GRASS_BLOCK`, 这是1.13版本的新用法.
在旧版API中, 应该使用`Material.GRASS`.
## 简述世界生成
Minecraft中, 一个世界(World)按照一定的大小被分为多个区块(Chunk).
MC会自动地按照一定的规则卸载无人Chunk, 在需要的时候加载所需的Chunk到内存, 以此来保证一个World被加载到内存, 这样不至于整个World都需要加载到内存以备调用.
世界的生成同样以Chunk为单位.
Minecraft游戏中, 世界生成分为两个阶段, 分别为 Generation 与 Population.
Minecraft生成一个World, 首先进入 Generation 阶段. 这一阶段主要是绘制地形等.
1. Minecraft首先会获取该Chunk中包含的所有生物群系. 然后会根据特定的生物群系绘制基本的地形. 地形的绘制依靠了一些特殊的算法, 游戏通常会以高度63作为水平面, 通过这些特殊算法绘制基本的地形. 绘制完毕后, 整个世界只有空气、水和石头.
2. 接着会在高度0-5范围内生成基岩, 并逐个对各个生物群系添加特有的方块. 例如, 对平原添加草方块和泥土, 对沙漠添加沙子和沙石等.
3. 再然后会生成特殊地形. 这里的特殊地形指的是涉及到多个区块的大型地形, 例如规模很大的洞穴、村庄、矿井等.
4. 最后会进一步处理, 做最后的准备收尾工作, 至此Generation阶段完毕.
Generation阶段完成, 意味着该世界的整体结构已经定型. 但是这个世界上还缺少“点缀”. 这个世界上仍然没有树、生物、沼泽上的荷叶、水边的甘蔗等. 此时进入 Population 阶段.
1. 首先会对该世界的实体进行完善, 并生成各种各样的特殊的方块(指的是箱子等方块实体, 这些方块与其它方块相比复杂许多).
2. 然后会生成小型地形. 比如一些地表小水坑、地表岩浆池、地下地牢等.
3. 然后会在地下按照一定的规则生成矿物.
4. 最后增加地面点缀, 生成水边的甘蔗、沼泽上的荷叶、地面大蘑菇和树木等物, 并增加一些生物群系特定物, 生成一些基础生物(比如牛、鸡、羊等).
待 Population 阶段结束后, 该Chunk的数据便会存储起来, 显示出来.
## 干涉Population
Bukkit中, 在世界初始化前会触发`WorldInitEvent`事件. 监听该事件, 我们可以对该世界生成的 Population 阶段进行干涉.
在下面的案例中, 我们将在Chunk的 Population 阶段, 在世界的草方块上人为的添加许多钻石块(DIAMOND_BLOCK).
```java
public class WorldListener implements Listener {
@EventHandler
public void onWorldInit(WorldInitEvent e){
if(e.getWorld().getName().equals("World"))
e.getWorld().getPopulators().add(new RuaPopulator());
}
}
class RuaPopulator extends BlockPopulator {
@Override
public void populate(World w, Random r, Chunk c){
final int maxn = 16; //一个区块的X或Y范围是0-16
for(int i=0; i<12; i++){ //这里打算一个区块生成12个
int x = r.nextInt(maxn), z = r.nextInt(maxn);
for (int y = 125; y > 0; y--) {
if (c.getBlock(x, y, z).getType() == Material.GRASS_BLOCK && c.getBlock(x, y + 1, z).getType() == Material.AIR) {
c.getBlock(x, y + 1, z).setType(Material.DIAMOND_BLOCK);
break;
}
}
}
}
}
```
最终效果如下:
![](../images/0_9.jpg)
可以发现, 生成的world中, 按照我们的设定, 在地表草方块上零散的分布了钻石块.
这说明在Bukkit中, 你可以创建一个BlockPopulator对象, 在世界初始化时添加为某一World的Populator, 依此来干涉Population阶段.
*Bukkit中的Populator只有BlockPopulator一种.*
*但是你可以以此类推, 通过这种方式实现在地面随机生成某种建筑等其他效果.*
值得注意的是, 在自定义的Populator中, populate方法的参数中有一个传入的Random对象.
这是为了让随机数的生成符合World对应的种子. 在需要生成随机数的时候, 应尽可能使用方法参数中的Random对象.
## 控制Generation
通过控制一个世界的Generation, 我们可以控制世界的大体地形.
下面我们将在插件加载时, 生成一个新的世界`RuaWorld`, 这个世界是一个超平坦世界, 第一第二层为基岩, 第三层为草方块.
```java
public class Main extends JavaPlugin {
public void onEnable(){
Bukkit.getPluginManager().registerEvents(new WorldListener(),this);
Bukkit.createWorld(new WorldCreator("RuaWorld").generator(new RuaChunkGenerator()));
}
public void onDisable(){
//
}
}
class RuaChunkGenerator extends ChunkGenerator {
@Override
public ChunkData generateChunkData(World w, Random r, int x, int z, BiomeGrid b) {
ChunkData chunkData = createChunkData(w); //创建区块数据
//下面这行方法调用参数中, 前三个参数代表一个XYZ对, 后面又是一个XYZ对.
//这两个XYZ对是选区的意思, 你可以结合Residence插件圈地、WorldEdit选区的思路思考.
//提醒: 一个Chunk的X、Z取值是0-16, Y取值是0-255.
chunkData.setRegion(0, 0, 0, 16, 2, 16, Material.BEDROCK); //填充基岩
chunkData.setRegion(0, 2, 0, 16, 3, 16, Material.GRASS_BLOCK); //填充草方块
//将整个区块都设置为平原生物群系(PLAINS)
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
b.setBiome(i, j, Biome.PLAINS);
}
}
return chunkData;
}
}
```
![](../images/0_10.jpg)
![](../images/0_11.jpg)
我们进入`RuaWorld`世界, 可以发现世界按照我们所需要的地形生成了.

View File

@@ -0,0 +1,15 @@
---
front:
hard: 进阶
time: 3分钟
---
# Title与ActionBar的显示
>
> *主观上在1.12.2后我们发送ActionBar和Title.*
> *Title只需要通过Player中的sendTitle方法.*
> *而ActionBar发送方式则是 Player 对象封装了内部对象 spigot(),其中里面则有静态方法 sendMessage 通过传入ChatMessageType枚举参数便可以发送ActionBar*
> *用Spigot的sendMessage得将内容转换成TextComponent*
但是Bukkit就没有能够直接发送ActionBar的发送方法.
所以我们就要通过NMS底层去自行封装一个方法.

View File

@@ -0,0 +1,146 @@
---
front:
hard: 高级
time: 20分钟
---
# 认识 NMS 和 OBC
nms即 Java 包 `net.minecraft.server`,存放的是 Minecraft 服务端游戏逻辑代码,这篇教程将会采用 `Spigot` 作为 Bukkit API就你们日常开发用的API 体系的例子CraftBukkit 也是 SpigotMC 维护,且基本所有 Bukkit 分支都是基于 Spigot 开发)
BukkitAPI 真的涵盖了不少东西,但是 BukkitAPI 并不是十全十美的,有时候我们确实需要使用 NMS。
~~Forge其实也有个net.minecraft.server但他和Spigot的没半点关系~~
## 使用 NMS 之前
md_5*Wait!* 你真的需要使用 NMS 吗?
> NMS不是API。当你遇到什么想做的事情的时候你不应该第一时间去考虑 NMS 或者 发包
令人费解的是,我们几乎每天都看到人们专门使用 NMS 做一些简单的事情,如 ScoreBoard、BossBar 或粒子。但是实际上自从Mojang添加了这些东西之后Spigot/Bukkit-API 早就有这些功能了。
每当你考虑使用 NMS 时,请思考以下问题:
1. 我是否需要NMS来做这个
2. 是否有一个API来实现这个功能
3. 我可以为这个贡献/创建/ ***提议*** 一个API吗
对NMS的滥用造成的后果非常严重。
- 1. 插件将失去版本迁移的能力(针对单个版本而言)
- 2. 阻碍了 API 的发展并且树立了一个坏榜样。
如果你确实想清楚确实没有现有的 API 能帮到你,那么来...
# 怎么使用 NMS
NMS 里的内容太多,故本教程**不会**教授NMS有什么东西但是可以教你怎么玩。
## 开发环境准备
由于DMCA的原因Bukkit不会直接提供NMS您需要将构建好的服务端代码引入项目中
如果没有且你正在使用 `Gradle` , `Maven` 这样的依赖管理器,考虑如下方法(图文):
这边以Gradle为例
1. 在项目根目录创建libs文件夹用于放第三方库
![](../images/0_12.png)
2. 将构建好的服务端直接拖入libs文件夹中
![](../images/0_13.png)
3. 配置Gradle
![](../images/0_14.png)
4. 同步Gradle配置
![](../images/0_15.png)
5. 你已经成功引入NMS
## 获取到一个来自 API 背后的对象
*当你从控制台直接输出一个 `Player` 对象时,会发生什么?*
你会得到一个 `CraftPlayer{name=玩家名}`而不是NMS里面的`EntityPlayer`。这是因为在Bukkit-API背后还有一层他叫`OBC`,也就是`org.bukkit.craftbukkit`
OBC 是 Bukkit API 的实现其本质是NMS的封装因此我们并不需要太关心它。
最简单的一个说法就是实际上我们操作NMS封装API也是OBC所做的事情。
就上文问题,怎么拿到一个 NMS 里面的 `EntityPlayer`?
首先我们需要使用反编译工具当然了你也可以通过IDEA直接查看。
来看 `CraftPlayer``EntityPlayer` 做了什么..
![](../images/0_16.png)
CraftPlayer 将 EntityPlayer 传给了他的父类构造器,我们接着追踪..
![](../images/0_17.png)
而 CraftPlayer 的父类 CraftHumanEntity 依然将 EntityPlayer 传入父类 CraftLivingEntity我们继续翻阅到 CraftLivingEntity 中
![](../images/0_18.png)
经过一条不 是 很 长的继承链后,我们找到了 `CraftEntity`,看来 `EntityPlayer` 最后是被传道这里了!
> 注意OBC里面的类 `implements` 的都是BukkitAPI的实现不要搞混了!
然后往上翻,看看 entity 是什么情况。
![](../images/0_19.png)
他的修饰符是 `protected`这意味着只有继承树内或者同一个包里面才能访问到它而这在NMS/OBC中是常有的事情。
## 反射!
我们很容易就可以写出这样的代码来获取到这个 entity 对象。
而这就需要用到Java的特性 —— 反射
```java
public static final String serverVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
try{
Player player = ....; // Who cares ?
Class<?> clazz = Class.forName("org.bukkit.craftbukkit."+serverVersion+".entity.CraftEntity");
Field f = clazz.getDeclaredField("entity");
f.setAccessible(true);
Object result = f.get(player);
}catch(Throwable t){
t.printStackTrace();
}
```
现在 `result` 里面存的就是我们需要的 EntityPlayer然后我们可以转换它...做点事情。
为什么要这么麻烦?其实下面的代码一样可以做到这个效果:
```java
try{
Player player = ....; // Who cares ?
Field f = CraftEntity.class.getDeclaredField("entity");
f.setAccessible(true);
Object result = f.get(player);
}catch(Throwable t){
t.printStackTrace();
}
```
假设BukkitAPI没有封装修改经验值的方法我们要修改玩家实体的经验值
就可以通过刚刚获取到的result的变量判断是否为 NMS 的 EntityPlayer 对象
```java
try{
Player player = event.getPlayer();
getLogger().info("test");
Class<?> clazz = Class.forName("org.bukkit.craftbukkit."+serverVersion+".entity.CraftEntity");
Field f = clazz.getDeclaredField("entity");
f.setAccessible(true);
Object result = f.get(player);
if (result instanceof EntityPlayer){
((EntityPlayer) result).d(1000);
}
}catch(Throwable t){
t.printStackTrace();
}
```
> 那为什么result的方法是 `d`
> 这是因为 Minecraft源码本身就是混淆的OBC则是Bukkit反编译探索出来这个大概是什么方法然后进行封装的
> 所以我们在和OBC做同样的事情时也要大概去猜测这些方法名是做什么的
> 大家可以尝试翻阅一下 EntityPlayer 源码,其中成员变量 newExp 被 方法 d() 所修改
> 所以大概猜测这个就是修改经验值的方法,调用后证实确实是这个方法
## 版本兼容性
实际上,如果你直接采用了 `CraftEntity.class` 的方法都会对这个 class 建立符号引用。
每个版本的 Spigot无论是 OBC 或者 NMS他们的包名都会变化——也就是说你的插件会爆炸
所以,如果你不想为了一个版本再把逻辑写一次,最好还是用反射的写法。对于公开的方法,也可以使用更高效的 `MethodHandle`
如果你有注意到的话, md_5 说过 `NMS 并不是`一个API(其实也包括OBC)。
什么意思呢?也就是`使用NMS没有任何安全性保障`**你反射的字段/方法或许下一个版本就会被删改。**
实例惨案: Minecraft 1.17 Spigot大改使用 Mojang 官方混淆表,以往 NMS 插件**全都**报废。(除了一些自带兼容的服务端)
# 本章小结
- NMS是`net.minecraft.server`一个包名放MC逻辑
- Spigot的NMS没有安全保障md_5 都不推荐用
- 使用NMS之前要先找是否已经有了对应的 API
- 在 Bukkit-API 和 NMS 之间还有一个实现,它叫 OBC。

View File

@@ -0,0 +1,106 @@
---
front:
hard: 高级
time: 30分钟
---
# 自定义发包
## 数据包介绍
翻阅NMS我们可以知道的是服务端的数据变更一般都是通过 DataWatcher 来判断以后通过
数据包的方式发送到客户端,客户端接收到数据包后对玩家客户端内容从而进行更改
这就是为什么上一个教程中,我们更改玩家的经验时不能直接通过赋值的方式进行更改
而是通过内置的一个“神秘(混淆了)”封装的方法去更改,这是因为只有通过此方法,才能将参数传参给父类
然后在通过父类的方法赋值给 DataWatcher
而翻阅 DataWatcher 类里的方法我们在一大堆混淆的代码发现了
```java
public void refresh(EntityPlayer to) {
if (!this.d()) {
List<b<?>> list = this.packAll();
if (list != null && to.getBukkitEntity().canSee(this.d.getBukkitEntity())) {
to.c.a(new PacketPlayOutEntityMetadata(this.d.af(), list));
}
}
}
```
此方法大概率就是发送数据包的方法了.
此时你会发现,他将 `PacketPlayOutEntityMetadata` 包发了出去
那这个包是干嘛的呢?
我们查阅[我的世界维基百科数据包](https://minecraft.wiki/w/Java_Edition_protocol/Packets?oldid=2772385)
可以发现其中的EntityMetadata 正好是更新现有生物实体的元数据,而经验值属于玩家实体特有的元数据,所以符合我们上述的说法
![](../images/0_20.png)
## 如何自定义一个数据包并发出去呢
说了那么多如何自定义一个数据包发出去呢
在这里做一个示例便是
在1.8更新我的世界更新了Title等一系列文字显示的内容但是Bukkit并没有及时的更新相关的API
直到1.12.2才更新快速发送Title的方法所以在1.8 - 1.11 期间开发者想要发送Title则需要自己通过封装NMS去手动发包
我们查阅对应版本的数据包在这里我拿1.12.2的wiki去演示 [我的世界维基百科数据包](https://minecraft.wiki/w/Java_Edition_protocol/Packets?oldid=2772385)
找到有关于Title的可以发现此数据包
![](../images/0_21.png)
我们可以看到基本介绍他是发送给客户端的状态是Play那么在NMS里应该就是 `PacketPlayOutTitle`
再查看PacketPlayOutTitle类虽然里面全部混淆了
但是对比成员变量与Wiki中Action描述可以不难猜出PacketPlayOutTitle包构造函数里的参数
按照顺序每个参数类型都是按照顺序填写的
![](../images/0_22.png)
我们发现有四个构造函数,那我们选择能够自定义程度最高的,也就是参数填入最多的那一个构造函数
第一个参数传入 Title 类型 TITLE、SUBTITLE
> 这就代表着我们要同时发2个包。一个是TITLE的数据包另一个是SUBTITLE的数据包。
第二个参数传入 `IChatBaseComponent` ,这个是我的世界的文本基础组件。
第三个参数传入 淡入时间
第四个参数传入 停留时间
第五个参数传入 淡出时间
当我们都填写好参数后,可以写出下述代码
```java
public void sendTitle(Player player, String title, String subtitle, int fadeIn, int stayIn, int fadeOut) {
IChatBaseComponent titlesend = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + title + "\"}");
IChatBaseComponent subtitlesend = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + subtitle + "\"}");
PacketPlayOutTitle packet = new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.TITLE, titlesend, fadeIn, stayIn, fadeOut);
PacketPlayOutTitle packet2 = new PacketPlayOutTitle(PacketPlayOutTitle.EnumTitleAction.SUBTITLE, subtitlesend, fadeIn, stayIn, fadeOut);
(((CraftPlayer)player).getHandle()).playerConnection.sendPacket((Packet)packet);
(((CraftPlayer)player).getHandle()).playerConnection.sendPacket((Packet)packet2);
}
```
此时我们即可快速调用自己写的 sendTitle 快速发送一份Title文字
但是翻阅各版本NMS可以发现每一个版本的Packet的参数混淆名、类的位置等都不一样
所以这就导致我们通过NMS去发送title就可能导致版本不互通
所以这就需要我们对各版本都有处理主要就是利用到Java的接口对不同版本都有一定的兼容这里就不再过多介绍
> 由于NMS兼容性确实很差一般来说开发者在操作Packet的时候都是通过第三方库 ProtocolLib 处理

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB