2.6
This commit is contained in:
143
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/1-最简单的插件.md
Normal file
143
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/1-最简单的插件.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
front:
|
||||
hard: 入门
|
||||
time: 15分钟
|
||||
---
|
||||
|
||||
# 最简单的插件
|
||||
|
||||
# Bukkit插件的本质
|
||||
插件本质是一个基于BukkitAPI的Java应用. 一个插件必须要有 主类 和 `plugin.yml`文件.
|
||||
|
||||
例如下面是一个常见插件, 让我们找一下它的主类和`plugin.yml`文件.
|
||||
|
||||

|
||||
|
||||
# 简单的插件
|
||||
在编写自己想做的插件之前, 不妨做一个简单的插件来了解一下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插件正常工作.
|
||||
|
||||

|
||||
|
||||
# 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 + "显示帮助菜单。";
|
||||
> ```
|
||||
260
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/2-事件监听.md
Normal file
260
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/2-事件监听.md
Normal 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, 也许看后你会明白如何操作.*
|
||||
205
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/3-配置API.md
Normal file
205
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/3-配置API.md
Normal 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
|
||||
```
|
||||
|
||||
相信你可以**根据空格看出每个项目之间的所属关系**, 如下:
|
||||
|
||||

|
||||
|
||||
**我们把上面所属关系图中, 矩形框内的东西叫做键(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`.
|
||||
|
||||

|
||||
|
||||
*后续会详细介绍, 这里需要知道判断方法.*
|
||||
|
||||
在上面的配置文件中, 配置文件里储存了:
|
||||
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);
|
||||
128
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/4-命令执行器.md
Normal file
128
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/2-基础内容/4-命令执行器.md
Normal 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`!
|
||||
|
||||
Reference in New Issue
Block a user