分类
Xkms

Word nb 好吧!

Word nb 好吧!

居然可以直接用word上传WP 个人博客

Microsoft 还是强大

😀

分类
Xkms

Bukkit插件入门教程[转载]

准备

插件开发需要什么

也许你已经有了雄心壮志, 准备开发一个插件出来了! 但是等一下. 开发插件也需要一定的基础知识!

最起码你需要有的东西

Java基础

插件开发对Java语言能力要求并不高, 通常情况下插件开发只会用到最基础的Java语法知识, 且开发环境搭建极其简单.
但本教程不针对Java初学者. 在本教程中, 将会尽可能避免较为复杂的Java语法知识.

这些Java基础内容你可以在网上、书籍中和一些现有的文献中学习到.

本教程目标保证在Java7及以上环境下运行.

了解Minecraft

我们假定你已经对Minecraft有充分了解.

尝试

插件开发离不开调试. 请你在提出问题之前、编写插件的过程中, 不要忘记不断调试. 这样你才能知道你的插件是否真的可以用, 别人说的不如自己试的, 自己想的不如实际干的.

编程的思维

编程的思维在实际编写一个项目当中尤为关键.
有Java基础并不够, 只知道语法, 不知道怎么写, 与不会Java没有什么区别.

希望你在实际开发中能够“脑洞大开”, 想出别人想不到的内容, 想出能打本文作者脸的好办法、新思路!

了解插件开发

在实际开发当中, 我们可以认为Bukkit、Spigot以及其衍生服务端(PaperSpigot等)都是一回事.

开发BukkitAPI插件需要准备一个用来开服的Jar文件. 推荐准备Spigot的服务端Jar文件.
然后你需要将该Jar文件当做“Libraries”文件导入至工程中即可开始开发.

对于BukkitAPI相关的问题, 通常百度上没有什么有价值的内容, 不妨添加一些开发者QQ群询问, 一些热心的开发者会回答你!
但是你要区分清楚 Java基础 和 Bukkit开发! 但是如果你有百思不得其解的问题, 不妨还是问一下, 问问题是成长的最快途径.

最简单的插件

Bukkit插件的本质

Bukkit插件本质是一个基于BukkitAPI的Java应用.

Bukkit插件有下面三个特征:

  1. 插件成品文件格式为jar.
  2. 插件jar文件根目录须有一名为 plugin.yml 且符合规范的文件.
  3. 插件内须有且仅有一个类继承 org.bukkit.plugin.java.JavaPlugin 类, 这个类将作为插件的主类.

简单的插件

在编写自己想做的插件之前, 不妨做一个简单的插件来了解一下Bukkit插件如何编写.

一个最简单的插件大致应分为两步编写:

  1. 编写主类
  2. 编写plugin.yml文件

无论是哪个插件, 这两步都是一开始应该去做的, 缺一不可.

新建一个Java工程, 创建tdiant.helloworld.HelloWorld类作为插件的主类, 并继承JavaPlugin类.
在主类里覆写onEnable方法和onDisable方法. 完成后, 代码应该类似这样:

package tdiant.helloworld;  

import org.bukkit.plugin.java.JavaPlugin;  

public class HelloWorld extends JavaPlugin {  
    // 关于onEnable与onDisable方法的作用会在下一节具体介绍
    @Override  
    public void onEnable(){  
        //your code here.  
    }  

    @Override      
    public void onDisable(){  
        //your code here.  
    }  
}  

创建plugin.yml文件. 打开plugin.yml文件并在其中输入如下信息:

name: HelloWorld
main: tdiant.helloworld.HelloWorld
version: 1
author: tdiant

特别注意: 如果你的插件是基于新版本API(1.13以及以上版本)编写的, 应当在plugin.yml中额外增加api-version: 1.13键值对.例如这样:

name: HelloWorld
main: tdiant.helloworld.HelloWorld
api-version: 1.13
version: 1
author: BakaRinya

这会告诉Bukkit, 这个插件是基于新版API编写的.
我个人不推荐您编写的插件同时兼容全版本Bukkit, 请把1.13版本以及以上版本与1.13版本以下版本两种API的插件分开编写, 以防出现BUG.

注意: 主类的名称并不是固定的, 但是plugin.yml文件的名称是固定的.

对于创建plugin.yml, 如果你 不知道 或者 没有使用Maven或Gradle, 请在src文件夹下直接创建该文件即可, 就像创建一个类一样! 例如在Eclipse中应这样:

上面的plugin.yml文件逐行分析如下:

意义 备注
name 插件名 不允许带有中文和空格, 推荐只含有下划线、英文.
main 插件的完整主类名 例如我这里插件主类为tdiant.helloworld.HelloWorld, 此处则需填写tdiant.helloworld.HelloWorld.
version 插件版本 您可以填写一个合理的String内容, 而不一定必须为数字, 例如可填写no_1.
author 作者

onEnable方法中添加相应的System.out.println("Hello World") 语句.
可以发现, 当插件Jar被正常生成后, 会在控制台输出Hello World字符串, 这标志着我们的HelloWorld插件正常工作.

Bukkit服务端会在插件被加载时调用onEnable方法, 被卸载时调用onDisable方法.

事件与事件的监听

事件

事件的概念

事件用来描述服务器内的一切变化与行为. 所有的事件都可以被监听.
简而言之, 事件就是服务器里发生的事.
例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件.

这也就意味着, 利用BukkitAPI, 我可以监听天气的变化, 在天气改变时执行某些代码.

事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能setCancelled).
不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件.

事件的用途

想象自己正在做一款登录插件, 登录插件是怎么制作出来的呢?

如果你正在写一个登录插件, 那么你需要监听玩家登录服务器的事件, 玩家进入服务器以后, 记录存储起来他的用户名. 等待玩家输入指令进行登录, 登录完毕以后去掉他的用户名.
然后再监听其他的各种事件, 判断玩家用户名有没有存储起来, 如果有, 那么他没有登录, 从而取消其他事件.

通过这样的例子可以发现, 事件是一个插件最重要的组成部分!

监听器

我们往往会想在一个事件被触发时, 执行一些代码, 此时我们便需要监听器.

监听器本质上是一个实现Listener类的类. 其中有一些带有@EventHandler注解的方法.
当某个事件触发时, Bukkit将会对应地调用这些带@EventHandler方法.
在BukkitAPI中, 每一个事件都对应一个类. 事件类一般以Event结尾, 所以它们都叫做XXXXXEvent. 所有的事件都在org.bukkit.event包内. Bukkit中所有的事件均可在JavaDoc中查阅.

下面通过一个示例,了解如何监听一个事件:

//这里为了偷懒, 直接把主类实现Listener作为监听器
//小的插件可以这样做, 节省开发时间成本(偷懒)
//如果是大插件, 建议采用之后我们将介绍的方法, 让代码更有条理, 方便开发
public class HelloWorld extends JavaPlugin implements Listener{
    public void onEnable(){  
        this.getLogger().info("Hello World!");  
        Bukkit.getPluginManager().registerEvents(this,this); //这里HelloWorld类是监听器, 将当前HelloWorld对象注册监听器  
    }  

    public void onDisable(){}  

    @EventHandler //这个注解告诉Bukkit这个方法正在监听某个事件, 玩家移动时Bukkit就会调用这个方法
    public void onPlayerMove(PlayerQuitEvent e){   
        System.out.println("玩家退出了!");  
    }  
}

PlayerQuitEvent 事件可以监听玩家退出服务器, 这是一个不可控事件. 这可以实现监听到玩家退出服务器, 并且在后台输出玩家退出了!的字符串.

onEnable方法中, registerEvents方法注册了该监听器.
registerEvents方法的第一个参数是监听器,第二个参数是插件主类的实例. 在这里主类就是监听器. 具体你可以在后面了解到.

监听器中带有@EventHandler的方法一个只能监听某一个事件, 而不能监听多个事件!

可取消事件与不可取消事件

事件分为可取消事件和不可取消事件. 其最大区别在于能不能取消(也就是能不能setCancelled).

有一个事件是PlayerMoveEvent, 顾名思义, 它将会在玩家移动时触发.
在JavaDoc中, 我们可以与该事件有关的信息. 关于玩家移动的JavaDoc文档可以在下面的链接中看到.
https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/player/PlayerMoveEvent.html

JavaDoc是每个Bukkit插件开发者都需要查阅的资料, 官方版本地址为:
https://hub.spigotmc.org/javadocs/spigot

打开后, 你可以翻阅各个包、各个类, 查看各个方法的具体使用方式.

在文档中, 我们可以注意到这些内容:

public class PlayerMoveEvent
extends PlayerEvent
implements Cancellable

PlayerMoveEvent事件实现了Cancellable接口.
Cancellable中定义了setCancelled方法和isCancelled方法.
通过setCancelled方法, 你可以在事件触发时设置是否取消该事件. 例如, 如果监听玩家移动, 事件触发时使用setCancelled方法, 可以取消玩家移动.
isCancelled方法可以判断该事件是否被取消.

对于不可取消事件, 它们没有实现Cancellable接口, 因此它们无法被取消.
就像玩家退出服务器, 你不能像刀剑神域一样, 不让玩家退出服务器.

下面是一个取消所有玩家在服务器内移动的例子.

package tdiant.helloworld;  

import org.bukkit.plugin.java.JavaPlugin;  
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;

public class HelloWorld extends JavaPlugin implements Listener {  
    // 关于onEnable与onDisable方法的作用会在下一节具体介绍
    @Override  
    public void onEnable(){  
        //your code here.  
    }  

    @Override      
    public void onDisable(){  
        //your code here.  
    }

    @EventHandler
    public void onPlayerMove(PlayerMoveEvent e){
        e.setCancelled(true); //这样就可以设置玩家都移动不了了.
        e.setCancelled(false); //设置了以后还可以修改, 例如这样玩家又可以继续移动了.
        e.setCancelled(true); //现在又改成不取消了
    }
}  

值得注意的是, 如果玩家并没有改变他的X/Y/Z, 而只是利用鼠标转了一下身, 这也属于玩家移动, 仍会触发PlayerMoveEvent事件.

理解客户端与服务端的关系

如果你实际去使用上面的那个代码, 你可能会发现一个问题: 玩家移动在游戏里还可以移动, 但是一会儿会被服务器"弹回来".
这样确实是达到了取消玩家移动的目的, 但是, 为什么最终的效果不是"玩家一点都动不了"呢?

事实上, 我们无法在服务端取消玩家一点也不能移动.
客户端移动玩家时, 会在客户端显示出移动后的样子, 然后才会传递给服务器玩家移动的信号, 服务端收到客户端的信号后, 服务器才会触发PlayerMoveEvent事件, 做出响应.

也就是说, 客户端与服务端之间, 客户端往往都是"先斩后奏"的. 客户端不管你服务端取不取消, 先那么显示出来再说.

如果要是真的想实现让玩家在服务器的某个坐标一点也动不了, 也许需要发挥你的聪明才智了. 让玩家卡在一个透明方块里? 也许有更好的方案? 现在有人已经实现了!
目前我们通常利用设置玩家移动速度的方法来让玩家无法移动!

EventHandler注解的参数

监听优先级

想象一下, 如果有两个插件, 他们同时监听玩家移动. 其中一个插件判断后发现玩家没有充够450块钱, 于是它取消了这名玩家的移动. 但是另外一个插件判断后发现玩家非常帅, 于是它允许了这名玩家的移动.
那么就会存在问题: 有一个插件setCancelled(true), 而又有插件setCancelled(false). 应该以谁为准?
那就要看监听优先级了!

下面是两个插件处理PlayerMoveEvent的部分:
A插件:

    // A插件
    @EventHandler(priority=EventPriority.LOWEST)
    public void onPlayerMove(PlayerMoveEvent e){
        System.out.println("testA");
        e.setCancelled(true);
    }

B插件:

    // 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改成这样:

    // B插件
    @EventHandler(priority=EventPriority.HIGHEST, ignoreCancelled = true)
    public void onPlayerMove(PlayerMoveEvent e){
        System.out.println("testB");
        e.setCancelled(false);
    }

可以发现, 后台只输出了testA, 玩家无法在服务器中移动. 这说明B插件的onPlayerMove没有被触发.
如果有其他监听已经取消了该事件, 设置ignoreCancelledtrue将可以忽略掉这个事件, 所以B插件的onPlayerMove方法没有被触发.

监听器的注册

可能你已经发现了, 在之前的代码中, 我们都会在onEnable方法中插入这样的语句:

Bukkit.getPluginManager().registerEvents(this,this);  

当时解释的是, registerEvents方法注册了该监听器.
如果没有这样的注册语句, 那么Bukkit就不会在事件触发时调用监听器类的对应方法.

该方法的第一个参数是监听器, 第二个参数是插件主类的实例. 当时由于我们为了偷懒, 直接把主类实现了Listener作为监听器, 因此我们可以这样写.
可我们不能写插件的时候把代码都堆在主类中. 这也就意味着, 我们可以把其他类实现Listener, 用同样的方式注册它, 这样我们就可以把监听事件部分的代码放在别的地方, 使插件代码更有条理性.

我们新创建一个类, 让它实现Listener, 再写对应的方法监听玩家移动, 就像这样:

public class DemoListener {
    @EventHandler
    public void onPlayerMove(PlayerMoveEvent e){
        System.out.println("PLAYER MOVE!");
    }
}

现在我们在主类的onEnable方法里, 就可以注册它了!

Bukkit.getPluginManager().registerEvents(new DemoListener(), this);  

常用事件简介

登录、进入服务器

BukkitAPI中与登录有关的常见的有: PlayerLoginEvent PlayerJoinEvent.
值得注意的是, 所有玩家进入服务器的事件都是不可取消事件.

在玩家尝试连接服务器时, 会触发PlayerLoginEvent, 玩家完全地进入服务器后, 会触发PlayerJoinEvent.
PlayerLoginEvent触发的时候, 你不可以操控玩家Player对象获取其背包等信息, 而仅可以获取UUID、玩家名和网络信息(IP等)等.
顺便一提, 玩家如果不在线, 你不可以通过BukkitAPI操控其背包.
PlayerJoinEvent触发时, 服务器内将会出现玩家实体. 此时你可以当做玩家完全进入服务器, 对其自由操作.

打个比方, 你家有一扇防盗门, 有人想进入你家.
首先他需要敲门, 在门外喊出自己的基本信息(名字等), 这是PlayerLoginEvent触发的时候. 如果你想从他背包里拿出东西, 不可以, 因为他在门外面.
当你给他打开门, 他进了你家中站稳了以后, 这是PlayerJoinEvent触发的时候, 这时候不管你是想打他还是想拿走他的东西, 都可以.

认识配置文件

配置文件

配置文件用来储存配置信息, 以便使用文件开关功能、储存数据、修改信息.
我们往往需要读写配置文件. Bukkit为我们提供了配置API.

配置API

配置API是BukkitAPI提供的读写配置文件的工具. 其相对而言较为简单, 是插件开发中常用的API.

目前为止, 配置API只有YAML配置功能可用. 这也是大多数插件为什么配置文件是YAML文件的原因.
在本文中, 我们也将使用YAML配置API.

现在的配置API的类均在 org.bukkit.configurationorg.bukkit.configuration.file 包中. 在早期版本(MC 1.1以及之前), 这些东西都在org.bukkit.util.configuration包, 而并非上面的这两个包.

了解YAML文件

相信开服的经验已经使你对YAML文件有了初步认识.
YAML文件的文件后缀是.yml. 其配置文件结构需要严格遵守YAML标准.

下面即是一个符合标准的YAML配置文件的内容:

a: 1
b: "abc"
c: abc
d:
  e: true
  f: 666.233
  g:
  - '2333'
  - '998'

在本节中, 我们暂不学习如何使用配置API读写配置文件. 首先对YAML配置文件做简单了解.

第一行 a: 1 代表a键(Key)对应的值(Value)为1.
在读写YAML配置文件时, 需要知道键, 由键获取这个键对应的数据.

在d键中, 存在e、f、g三个子键, 相信你可以根据空格看出来谁是谁的子键, 其对应的名称分别为d.ed.fd.g.
也就是说, 我们想获取那个现在值为true的键对应的数据, 应该获取键d.e的数据.

子键也可以有其子键, 例如:

a:
  b:
    c:
      d:
        e:
          f:
            g: 233

那么a.b.c.d.e.f.g键的值为233.

回到第一份YAML配置内容中.
a键对应的值是1, 1可以代表是一个数字, 也可以代表是一个字符串.
这意味着, 字符串在一些情况下可以不必带有引号.
那我在实际使用配置API的时候获取到的a键值究竟是什么类型? 具体会在后面提及.

d.e键对应的值是一个boolean类型数据.
d.g键对应的是一个StringList类型数据.

YAML中字符串可以用双引号表示, 也可以使用单引号表示, 但是不能一半是双引号, 一半是单引号.
YAML中注释使用#, 类似Java中的//, 请看下面的实例:

#我是注释
a: 'abc' #这是一个字符串abc
b: "def" #用双引号也可以
c: 'hji" #这样不可以
d: '""' #这是字符串, 内容是一对英文双引号
e: "''" #这是字符串, 内容是一对英文单引号

感受配置API的使用

在后面的内容中, 将详细叙述配置API的使用方式. 在这里, 通过一个实例来感受配置API的使用方式.

首先我们需要创建config.yml文件. 默认的config.yml文件要与plugin.yml文件处于同一目录下. 在这里我们在默认config.yml文件中存入这些信息:

a: 1

完成后, 我们需在onEnable方法中插入这样的语句:

saveDefaultConfig(); //我建议这个东西写在主类onEnable方法开头那里或者onLoad方法里
System.out.println("Hello! Test Number is "+getConfig().getInt("a")); //输出文件

插件加载后, 可以发现控制台输出:

Hello! Test Number is 1

在plugins文件夹中生成了以插件名为名的文件夹, 打开该文件夹里的config.yml文件, 并将其中的a键改为其他整数后使用reload指令, 可以显示该数值.

基本用法(操作config.yml)

默认配置文件

每个插件都可以有一个config.yml文件作为其配置文件.
我们首先需要准备一份默认配置文件, 也就是插件放进plugins文件夹后第一次被加载自动生成的配置文件.

首先我们需要创建config.yml文件. 默认的config.yml文件要与plugin.yml文件处于同一目录下. 在这里我们在默认config.yml文件中存入这些信息:

a: 1
b: "abc"
c: abc
d:
  e: true
  f: 666.233
  g:
  - '2333'
  - '998'

完成后, 我们需在onEnable方法中插入这样的语句:

saveDefaultConfig(); //我建议这个东西写在主类onEnable方法开头那里或者onLoad方法里

该语句需要保证在读写配置文件之前被执行.
它可以自动判断插件配置文件夹中是否存在config.yml文件, 在没有的时候将该插件jar文件的根目录中config.yml保存至插件配置文件夹.

写入配置文件

让我们尝试写入配置文件, 在onEnable方法被调用时把配置文件中键h改为字符串baka.
我们可以这样做:

public void onEnable(){
    this.saveDefaultConfig();
    this.getConfig().set("h","baka");
    this.saveConfig();
}

执行后可发现, 插件配置文件夹中config.yml文件的h键内容已经成为baka.
set以后要记得saveConfig!

如果配置文件中本来没有某个键,你可以直接使用set方法创建它.
如果配置文件里存在某个键,现在你想删去它,可直接使用set方法将其修改为null.

getConfig().set("d.f",null);
saveConfig();  //这样在最终保存的配置文件里将不存在d.f键

getConfig().set("d.aaabbb",6666);
saveConfig();  //这样在最终保存的配置文件里将存在一个值为6666的d.aaabbb键,无论d.aaabbb键在之前的配置文件里有没有

读取配置文件中数据

我们可以使用get来读取数据.

public void onEnable(){
    this.saveDefaultConfig();
    System.out.println(getConfig().get("a"));  //这里的get方法返回Object类型数据
}

后台将a键的值, 也就是将会输出1.

对于不同的数据, 配置API给出了不同的get方法. 例如 getStringgetBooleangetDoublegetInt等.

还记得前面的那个"类型问题"吗? a键的值究竟是什么数据类型呢?
答案已经揭晓, 取决于你使用的是哪个get方法. 如果使用getString获取a键的值, 那么获取到的将是String类型, 如果是getInt, 那么就是int类型, 如果是getDouble, 那么就是double类型......

对于d.g键, 其内容这样获取:

List<String> list = getConfig().getStringList("d.g");

在其他类中操作配置文件

我们已经知道如何注册监听器. 那么我们免不了遇到在其他类中操作配置文件的情况.
然而你会发现, getConfig方法并不是static(静态)的. 我们不能直接在其他类操作配置文件.

如果你有一些Java开发常识, 此时你可能意识到了, 我们需要做一个静态的主类实例才行.
在这里赘述一种我自己喜欢用的方式. 这是一个经过处理的主类, 有两处需要注意:

public class HelloWorld extends JavaPlugin{
    private static HelloWorld INSTANCE;

    public void onEnable(){
        INSTANCE = this; //这个推荐放在最最最开头
        ......

    ......

    public static HelloWorld getInstance(){
        return INSTANCE;
    }
}

然后你就可以在其他类中这样操作配置文件:

HelloWorld.getInstance().getConfig().你要做的各种操作
HelloWorld.getInstance().saveConfig(); //写入配置文件内容了以后记得保存!

操作自定义的配置文件

关于非config.yml的YAML文件的操作, 有很多种方式可以做到.
下文叙述的是其中的一种.

默认配置文件

我们还是需要像config.yml那样准备一份默认配置文件, 放在与plugin.yml相同目录下. 不同的是, 除了saveDefaultConfig以外, 我们还需要其他的代码来保存默认配置文件.

例如我们有config.ymlbiu.yml两个配置文件, 插件加载时应该这样生成默认配置文件:

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文件夹

基本读写与保存

下面是一个读写与保存的示例:

//读取
//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);
biuConfigFile.get.......
biuConfigFile.set....... //set完了记得保存!
//保存
biuConfig.save(biuConfigFile);

命令执行器

认识命令机制

MC中的命令是一个字符串, 用来实现游戏内高级功能.

在MC客户端中, 玩家将在聊天框内输入命令.
当且仅当在“聊天”内, 命令与普通的聊天内容的区别在于其内容的第一个字符是一个斜杠/.

该字符串中的空格表示一个分隔, 开头的一节为命令的名称.
除去命令的名称, 剩下的部分从空格处断开可以分成一个数组.

例如, a b c是一个命令, 其命令名称为a, 其参数可用一个数组args表示为:

args[0]: "b"
args[1]: "c"

定义新命令

如果我们需要定义一个新的命令, 首先我们需要在plugin.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: / YOUR_NAME

注意:

  1. 在usage里可以代表你的命令名.
  2. 你的命令设置了aliases后命令名不能按照aliases称呼. 比如你给login命令设置了aliases: [l]你不能也叫他l命令, 它还是login命令.
  3. 不推荐使用permissionpermission-message, 因为plugin.yml里出现中文爱出问题. 事实上, 我们可以用Player.hasPermission方法在监听命令的时候自己亲自判断有没有权限.
  4. 如果一个名称被别的插件注册了或设置为了某个命令的别称, 会出现冲突问题, 尽量避免.
  5. 别弄中文的命令, 如果想搞, 去试试监听PlayerCommandPreprocessEvent.

onCommand

我们可以类似Listener, 做一个CommandExecutor监听命令.

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里加入注册:

Bukkit.getPluginCommand("rua").setExecutor(new DemoCommand());

但是如果onCommand方法放在了主类里, 那就不需要注册了.

onCommand方法有四个参数, 分别为:

  1. CommandSender sender —— 命令输入方, 实际传入的有可能是Console, 有可能是Player或者其他情况.
  2. Command cmd —— 所执行的命令对象.
  3. String[] args —— 参数. 例如/rua a b的话, args[0]为"a", args[1]为"b".

    警告: 字符串的比较, 请不要使用==, 因为其比对的是内存地址, 可能造成一些没有预料到的结果! 建议使用equals方法, 例如args[0].equals(string)

如果你的命令希望只被玩家使用, 通常这样判断:

if(!(sender instanceof Player)){
    sender.sendMessage("你不是玩家!不能用!");
    return true; //不返回true, Bukkit还会显示出来一串错误提示, 你可以试试看.
}

判断完为玩家后, 若希望判断其有没有权限执行命令, 可以:

Player p=(Player)sender; //sender可以直接强转为Player
if(p.hasPermission("rua.use")){
    p.sendMessage("你有权限!");
}

玩家将会在聊天区域内看到输出:

你有权限!

Bukkit内可以用ChatColor表示颜色前缀, 例如:

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方法里试试看:

System.out.println(ChatColor.RED+"猜猜我是什么效果"); 
this.getLogger().info(ChatColor.RED+"你再猜猜我是什么效果");

以后推荐您用getLogger().info方法代替System.out.println(也就是sout、sysout方法)!

在实际应用的时候, 还要小心args.length! 玩家只输入/rua没有参数的时候, 小心因为自己的疏忽造成ArrayIndexOutOfBoundsException!

插件制作实例

这是第一单元的最后一章.
在第一单元中, 你已经对Bukkit中常见且重要的系统有了初步的认识.

那么如何利用这些认识写一个Bukkit插件呢?
在本节中, 将着重讨论“制作插件的思路”, 没有新内容.

插件制作的思路

做插件需要准备

如果你想开始做一个插件, 那你需要①准备服务端Jar文件, 在IDE中创建Project后②创建主类和plugin.yml. 这是编写一切插件的基本和开始.

大多数开发者更倾向于使用Maven或Gradle.
作者在此认为Maven和Gradle与插件开发相关的内容超出了本文的内容范围, 推荐您参考这些优秀文章:
如何利用Maven来管理你的插件 - author: 莫老

如何做我想做的功能

如果你想做登录插件, 你需要监听登录事件和玩家的输入事件; 如果你想做小游戏, 你可能会监听玩家的移动、右键等事件.

由此看来, 插件往往基于事件. 事件的监听是一个插件的灵魂.
插件往往就是监听事件触发后执行某个代码, 由此运行的.
在制作插件前的设计过程当中, 不妨思考一下 "我的插件应该监听哪些事件", 然后去寻找BukkitAPI中这些事件的对应的类, 监听它们.

对于怎么找到自己想要的事件, 相比你已经发现, 相应事件类的名字有特殊的含义.
例如PlayerMoveEvent. 其实根据名称就能判断出来, 这是玩家移动. EntityDamageByEntityEvent, 很简单, 会在某个生物被其他生物攻击时被触发.

CreeperPowerEvent, 这个事件也许就不是那么好猜了. 在拿捏不准的时候可以在JavaDoc上查询.

  1. SpigotAPI 最新版本JavaDoc: https://hub.spigotmc.org/javadocs/spigot/index.html?overview-summary.html
  2. BukkitAPI 1.7.10版本JavaDoc: https://jd.bukkit.org/
  3. BukkitAPI 中文版JavaDoc(仅供参考): https://docs.windit.net/Chinese_BukkitAPI/

实际应用时应主要以1号JavaDoc为主. 在这份JavaDoc中, 右上角处有搜索栏, 可以在这里输入你想搜索的内容.
比如我想搜索如何发Title, 我可以在这里搜索title这个关键字, 你就会轻易发现Player类下有sendTitle方法, 然后可以找到这个方法的详细介绍.
每个开发者都会在插件制作过程中经常查看JavaDoc. 这是插件制作所必须的.

数据的储存

在学习Java基础的过程中, 你一定学习到了MapList等类型. 这些类型在插件编写当中非常常见.

如果我们想做一个登录插件, 也许我们会把没有登录的玩家名称加在一个List里, 然后监听很多事件, 诸如PlayerMoveEventPlayerChatEvent等, 利用List.contains就可以判断出玩家是不是登录了, 然后通过控制event.setCancelled方法控制是否禁止事件, 这就完成了登录插件的基本结构了.

如果我想实现 关闭服务器后再打开还能保留 的数据, 这种功能的实现思路往往是: 保存数据到配置文件->把数据文件读取出来.
也就是要学会利用配置文件, 配置文件不只是能帮助你储存配置, 一定要意识到!

几个插件样例

也许你已经大致对插件制作有一定的了解了. 那就把知识应用到实际当中, 做个插件试试看.
请你在下面的内容中着重注意领悟插件制作的思路.
我们假设服务端测试环境只有我们自己做的那个插件.

限于文章篇幅, 这里仅做一个小示范.
希望读者能够看完这个示范后, 能有所感悟. 试试看, 能不能做一个小游戏出来? 你现在完全可以做出来了!

下面的内容你可以在本教程的Git上获取到源代码:
https://github.com/tdiant/BukkitDevelopmentNote/tree/master/source/

StupidLogin - 简单的登录插件

我一直反复在说登录插件. 不做一个登录插件怎么能行呢? 233333

基本准备

首先我们要写好基本的主类和plugin.yml, 引入服务端Jar作为Lib. 我这里创建了StupidLogin工程作为演示, plugin.yml目前设置了name version main author, 主类目前如下:

package tdiant.bukkit.stupidlogin;

public class StupidLogin extends JavaPlugin {
    public static StupidLogin instance;

    public void onEnable() {
        instance = this;        
        getLogger().info("StupidLogin插件启动!");
    }

    public void onDisable() {
        getLogger().info("StupidLogin插件已经卸载.");
    }
}

然后我们大致设计一下这个插件, 其实这个插件工作原理我已经不必多说, 你应该清楚了:

登录注册部分

  1. 注册登录注册指令.
  2. 监听玩家进入服务器, 把玩家的名字写在 未登录玩家 的List当中.
  3. 玩家登录或注册.(这里注册后还需要输入登录指令)
  4. 玩家输入登录指令后, 把玩家名从 未登录玩家 List 里去掉.

然后再写一个Listener, 监听玩家各种事件例如PlayerMoveEvent等, 在玩家未登录时禁止这些事件.

在开头提醒: 不能监听PlayerEvent、EntityEvent或诸如此类的事件! 限制玩家未登录时的行动必须需要监听一遍所有事件!

LoginManager

LoginManager有 查询玩家是否登录、查询是否注册、注册玩家账号、修改是否登录的状态、查询字符串与玩家密码是否一致 的五个功能.

首先先写出基本的结构, 把构思中的 未登录玩家 的List写出:

public class LoginManager {
    private static List<String> unloginList = new ArrayList<>();

    //获取玩家是否登录
    public static boolean isLogin(String playerName) {
        return !unloginList.contains(playerName); //如果玩家名字没在列表里就返回true, 代表已经登录
    }

    //设置玩家登录状态,flag=true为登录, false为未登录
    public static void setPlayerLogin(String playerName, boolean flag) {
        if(flag) {
            unloginList.remove(playerName); //设置为true就会把玩家从未登录玩家里删除
        } else {
            unloginList.add(playerName); //反之添加
        }
    }
}

然后再写出与配置文件有关的三个方法:

    //判断玩家是否注册账号
    public static boolean isRegister(String playerName) {
        //如果配置文件里有  player_data.玩家名  这个键就代表已经注册
        return StupidLogin.instance.getConfig().contains("player_data."+playerName); 
    }

    //让玩家注册账号
    public static boolean register(String playerName, String password) {
        if(isRegister(playerName))
            return false; //如果已经注册就return,阻止下面的操作

        //设置  player_data.玩家名.password 键为玩家的密码字符串
        StupidLogin.instance.getConfig().set("player_data."+playerName+".password", password); 
        StupidLogin.instance.saveConfig(); //设置完了别忘了保存!
        return true;
    }

    //判断密码是否正确
    public static boolean isCorrectPassword(String playerName, String password) {
        if(!isRegister(playerName))
            return false; //没注册能正确就怪了!

        //pass是玩家注册的密码
        String pass = StupidLogin.instance.getConfig().getString("player_data."+playerName+".password");
        return pass.equals(password); //返回结果
    }

事件监听

然后编写监听器.

首先先编写PlayerLimitListener, 用以禁止 玩家未登录时做的操作 ,并且在玩家登录和退出服务器时把玩家加进 未登录玩家 List 里.

package tdiant.bukkit.stupidlogin.listener;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.*;

import tdiant.bukkit.stupidlogin.LoginManager;

public class PlayerLimitListener implements Listener {

    //这里只是把几个常见的情况进行了拦截,玩家说话、玩家鼠标操作和玩家移动.
    //你可以在这里按照这样的方式添加更多的拦截,使其更加安全

    @EventHandler
    public void onPlayerChat(PlayerChatEvent e) { //不让聊天
        if(e.getMessage().substring(0, 0).equals("/")) //这里不拦截玩家用命令, 后面我们会处理一下限制玩家用命令
            return;
        e.setCancelled(needCancelled(e.getPlayer().getName()));
    }

    @EventHandler
    public void onPlayerMove(PlayerMoveEvent e) { //不让玩家移动
        e.setCancelled(needCancelled(e.getPlayer().getName()));
    }

    @EventHandler
    public void onPlayerInteract(PlayerInteractEvent e) { //不让玩家跟别的东西交互,约等于屏蔽左右键
        e.setCancelled(needCancelled(e.getPlayer().getName()));
    }

    @EventHandler
    public void onPlayerInventory(InventoryOpenEvent e) { //不让玩家打开背包
        e.setCancelled(needCancelled(e.getPlayer().getName()));
    }

    private boolean needCancelled(String playerName) {
        return !LoginManager.isLogin(playerName);
    }

    // 下面的两个监听用来修改玩家的登录状态
    @EventHandler
    private void onPlayerJoin(PlayerJoinEvent e) {
        LoginManager.setPlayerLogin(e.getPlayer().getName(), false);
    }

    @EventHandler
    private void onPlayerQuit(PlayerQuitEvent e) {
        LoginManager.setPlayerLogin(e.getPlayer().getName(), false);
    }
}

为了体现我们的友♂好, 在玩家上线时给玩家发一个提示:

package tdiant.bukkit.stupidlogin.listener;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;

import tdiant.bukkit.stupidlogin.LoginManager;

public class PlayerTipListener implements Listener {

    //玩家进入服务器后的“请您登录”提示语
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent e) {
        e.getPlayer().sendMessage(
                    LoginManager.isRegister(e.getPlayer().getName())?
                            "欢迎回来!请输入/login 密码 登录服务器!":
                            "欢迎第一次来到本服务器!请输入/register 密码 注册账号!"
                );
    }

}

指令

我认为指令部分可以通过代码看懂我的意思.
在这里我会将login指令和register指令都放在这一个CommandExecutor里.

对了, 要养成整理整齐onCommand方法的好习惯哦!

package tdiant.bukkit.stupidlogin.command;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;

import tdiant.bukkit.stupidlogin.LoginManager;

public class PlayerLoginCommand implements Listener, CommandExecutor {
    @EventHandler //用来拦截除了登录插件以外的指令
    public void onPlayerCommand(PlayerCommandPreprocessEvent e) {
        if( LoginManager.isLogin(e.getPlayer().getName()) )
            return;

        e.setCancelled(true);
        if( e.getMessage().split(" ")[0].contains("login") 
                || e.getMessage().split(" ")[0].contains("register") )
            e.setCancelled(false);
    }

    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] arg) {
        if(!(sender instanceof Player)) 
            return false;

        Player p = (Player)sender;

        if(cmd.getName().equalsIgnoreCase("login"))
            loginCommand(p,arg);
        else if(cmd.getName().equalsIgnoreCase("register"))
            registerCommand(p,arg);
        return true;
    }

    private void loginCommand(Player p, String[] args) {
        if(LoginManager.isLogin(p.getName())) {
            p.sendMessage("你已经登录了!");
            return;
        }
        if(LoginManager.isRegister(p.getName())) {
            p.sendMessage(ChatColor.RED+"你还没有注册!");
            return;
        }
        if(args.length!=1) {
            p.sendMessage(ChatColor.RED+"登录指令使用错误!");
            return;
        }
        if(LoginManager.isCorrectPassword(p.getName(), args[0])) {
            p.sendMessage(ChatColor.GREEN+"登录成功!");
            LoginManager.setPlayerLogin(p.getName(), true);
        }
    }

    private void registerCommand(Player p, String[] args) {
        if(LoginManager.isLogin(p.getName())) {
            p.sendMessage("你已经登录了!");
            return;
        }
        if(LoginManager.isRegister(p.getName())) {
            p.sendMessage("你已经登录了!");
            return;
        }
        if(args.length!=1) {
            p.sendMessage(ChatColor.RED+"注册指令使用错误!");
            return;
        }
        LoginManager.register(p.getName(), args[0]);
        p.sendMessage(ChatColor.GREEN+"注册成功!请登录!");
    }
}

在主类和plugin.yml注册

我们需要把监听器和命令在主类和plugin.yml中进行注册.

name: StupidLogin
main: tdiant.bukkit.stupidlogin.StupidLogin
version: 1
author: tdiant

commands:
  login:
    usage: "/login YOUR_PASSWORD"
    description: "Login your account."
  register:
    usage: "/register YOUR_PASSWORD"
    description: "Register your account."

plugin.yml中的注册需要注意不能出现中文, 请全英文! 编码推荐UTF-8.

然后就是在主类注册一下了! onEnable方法内添加:

        //Configuration
        this.saveDefaultConfig();  //输出默认配置
        //Listener
        Bukkit.getPluginManager().registerEvents(new PlayerLimitListener(), this);  //注册监听器
        Bukkit.getPluginManager().registerEvents(new PlayerLoginCommand(), this);
        Bukkit.getPluginManager().registerEvents(new PlayerTipListener(), this);
        //Commands
        CommandExecutor ce = new PlayerLoginCommand();  //注册指令,这里两个指令公用同一个Executor
        Bukkit.getPluginCommand("login").setExecutor(ce);
        Bukkit.getPluginCommand("register").setExecutor(ce);

后记

至此, StupidLogin 已经完成! 希望你能阅读一下源码, 体会一下写这个插件的思路!

但是这个登录插件的功能未免显得简单. 有这些缺陷:

  1. 如果玩家密码带空格, 这个插件会出现问题. (提示: 需要对onCommand的args做处理, 做出玩家密码带空格时真正想输入的密码)
  2. 玩家名大小写可能会存在缺陷.

在早期时有一款登录插件叫做PlayerLogin, 这款插件存在这样的问题:
如果一个OP名字用ID abc 登录服务器中,拥有OP权限,那么一个不知道其密码的普通玩家可以通过ID Abc、ABC等更改大小写的ID登录服务器中,而登录插件却判定这个ID从未被注册过,可实际上Abc或ABc对于Minecraft服务器而言都有OP权限.
本节中做的StupidLogin插件实际上也有这个问题,只要更改一个大小写,玩家也许就可以获得OP.

你可以尝试思考如何解决这一问题.
Tips: 常规的思路是将玩家名中英文字母全部转换为小写,尤其是在判断注册时应以小写判断.

  1. PlayerLimitListener监听的事件太少, 可能有想不到的地方. (建议: 反编译或看开源的源代码, 看看AuthMe是如何做禁止的)
  2. loginregister输入起来太麻烦, 没有简便指令.
  3. 没有更换密码功能.
    还可能有其他各种问题......

这些问题如何解决呢? 你来动手试一下吧!

插件的加载机制

插件的加载顺序会因为多种原因而改变.

依赖

如果我们的插件需要调用其他插件的API, 例如获取经济插件中玩家金币数据, 那么我们的插件就会依赖(depend)那个插件.
我们可以在plugin.yml中设置依赖, Bukkit会把 我们依赖的插件 放在 我们的插件 之前被加载.

有两种依赖方式:

  1. depend: 我需要依赖其他插件, 并且我必须依赖它.
  2. softdepend: 我需要依赖其他插件, 但这是可选的, 不依赖该插件仍能正常工作.

在后续将更进一步地研究依赖.

onLoad、onEnable和onDisable

在HelloWorld插件的实验中可得, onEnable会在服务器启动时被加载, onDisable 会在服务器关闭时被加载.
事实上, 还有第三个方法, onLoad.

这三个方法的关系如下:
对于服务端启动:

  1. 服务端启动, 服务端体系加载完毕后, 加载所有插件jar文件, 按照顺序逐个调用每个插件的onLoad方法.
  2. 所有插件onLoad方法调用完毕. 服务端在一些处理过后, 按照顺序逐个调用每个插件的onEnable方法.

对于服务端关闭(stop指令):
服务端关闭时, 逐一调用每个插件的onDisable方法.

对于reload指令:

  1. reload指令触发后, 服务端在一些处理后, 按照顺序逐个调用每个插件的onDisable方法.
  2. 所有插件onDisable方法调用完毕, 服务端按照顺序逐个调用每个插件的onLoad方法.
  3. 所有插件onLoad方法调用完毕, 服务端按照顺序逐个调用每个插件的onEnable方法.

    自定义事件

自定义事件

我们现在所了解的事件都是Bukkit提供的. 例如, 玩家移动等.
那如果我们想自己去做一个事件呢?

比如, 我想自己做出来一个RuaEvent, 实现在玩家聊天说rua的时候触发.
很明显, Bukkit只会提供玩家发送聊天信息的事件, 肯定不会单独为了实现在玩家聊天发送rua的时候单独做个事件. 那应该怎么做?

首先想到的应该是监听玩家聊天事件, 然后判断玩家聊天发送的内容是什么, 如果是rua做我想做的事情. 这是常规的解决方法.
但是如果我想做一个强化插件, 我想在玩家强化物品的时候触发一个事件给自己和其他插件, 那我应该怎么做? 不如自定义一个属于自己的事件!

这里我们以创建上文的RuaEvent事件举例, 我们的大致思路是这样的:

  1. 创建一个RuaEvent类.
  2. 监听玩家聊天, 判断玩家聊天内容, 如果是rua, 让Bukkit触发我们新建的RuaEvent对象.

我们就先新建一个类RuaEvent, 让其继承org.bukkit.event.Event类. 在该类中写下这些固定代码:

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方法永远会返回null, 结合上面的内容, 你应该可以意识到PlayerEvent是无法正常工作的吧.
所以你只能把所有Player开头的Event监听一个遍才可以达到目的!

现在我们的自定义事件雏形已经完成. 你可以根据自己的需要添加相关代码!
这里我们示例的RuaEvent代码最终如下:

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接口:

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是个不可取消事件
@EventHandler
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个不可取消事件
    if(e.getMessage().equals("rua"))
        Bukkit.getServer().getPluginManager().callEvent(new RuaEvent(e.getPlayer())); //触发事件
}
  1. 如果RuaEvent是个可取消事件
@EventHandler
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个可取消事件
    if(e.getMessage().equals("rua")){
        RuaEvent event = new RuaEvent(e.getPlayer());
        Bukkit.getServer().getPluginManager().callEvent();
        if(event.isCancelled()) //这里加判断即可
            e.setCancelled(true);
    }
}

在这里监听了PlayerChatEvent,但是此事件已被标记@Deprecated,实际的开发过程中不推荐监听此事件.
实际开发中建议监听的是AsyncPlayerChatEvent事件. 注意这是异步监听,用法基本类同于上述事件的监听,具体请参见JavaDoc.

深入plugin.yml

了解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错误.

plugin.yml文件中, 目前我们已知的有nameversionmainauthor四个项目可以设置.
事实上, plugin.yml文件中还有许多可以设置的项目, 部分项目是本节的内容, 其余可以在spigotmc的官方文档中查阅到.

目前(2018.7.28)BukkitAPI主要由SpigotMC维护, 因此大量的BukkitAPI文档都在 SpigotMC 网站上.
有关plugin.yml文件的官方文档在这里:
https://www.spigotmc.org/wiki/plugin-yml/

必要设置项

plugin.yml文件中, namemainversion三项必须存在.
这也意味着, 前面的实例中, 我们使用的plugin.yml文件, 删去author键仍可被服务端正常加载.

不妨来认识一下这三个设置项.

name

顾名思义, 它定义了插件的名称.

对于名称, 官方WIKI中给出了严格的要求, 即只能由 英文小写或大写字符、阿拉伯数字或下划线 构成. 决不能出现中文字符、空格等.
在后续生成插件配置文件夹时, 该项设置的插件名将会是插件配置文件夹的名称.

起名的时候应该注意, 尽可能起一个“个性”的名称, 防止与其他插件重名.

version

指插件的版本号.
该键理论上可以在后面填写任意String内容. 但是官方WIKI要求尽可能使用X.X.X格式的版本号表示(例如: 2.3.3).
关于版本号规则,可以参考语义化版本

main

指插件的主类名.

在插件中, 主类有且只有一个, 且需要继承JavaPlugin类. 主类是插件的“入口”, 这里的main即意在说明主类的名称.
这里需填写主类的全名, 也就是精确到主类所在的具体包. 说白了就是不只是需要把主类名带上, 还要把包名带上.

可选设置项

plugin.yml文件只需要存在必要设置项的三个键即可.
下面的键可选, 可有可无. 但有一些在一些特定的情况下必须要有.

依赖

有时候你的插件可能需要调用Vault(用来获取玩家货币余额)或其他的插件, 即依赖其他插件.
这时候需要在plugin.yml文件中进行设置告知服务端, 从而保证所依赖的插件在本插件之前被加载.

你可以在plugin.yml文件中加入depend键或softdepend键来控制依赖.

depend键或softdepend键接的值必须是数组. 例如这样:

depend: [Vault, WorldEdit]
softdepend: [Essentials]

两个键设置的内容区别如下:

  1. depend: 插件强制要求的依赖. 如果没有这个插件, 该插件将无法正常工作, Bukkit此时会抛出相应错误.
  2. softdepend: 插件不强制要求的插件. 如果服务端内没有这个插件, 插件仍可正常工作.

后面设置的数组内的内容都是所依赖插件的名称, 此处名称应与所依赖的插件的plugin.yml文件的name键的值相同.

loadbefore

dependsoftdepend可以实现插件在某个插件之后加载. 但也许有时你的插件可能需要实现在某个插件之前被加载.
此时你可以使用loadbefore设置, 用法类似. 例如:

loadbefore: [Essentials, WorldEdit]

在上面的例子中, 可保证插件在WorldEdit与Essentials插件之前被加载.

commands

如果你的插件定义了新指令, 你第一步就需要设置该项告知服务端.
此处仅做示范:

commands:
  test:
    description: "Hello World!"

这可以告知服务端注册了指令test, 并且描述为Hello World!字符串, 该描述字符串将会在/help指令中被显示.

author与authors

此处不再赘述其作用. 如果你想表示多名作者, 你可以设置authors项, 值需为一个数组.

authors: [tdiant, Seraph_JACK]

如果同时存在authorauthors, 将忽略author.

配置API的序列化和遍历

序列化

了解序列化

如果我自己做了一个类型, 例如下面的BakaRua类:

public class BakaRua {
    public String name;
    public String str;

    public BakaRua(String name, String str) {
        this.name = name;
        this.str = str;
    }
}

现在我们新建一个BakaRua对象:

BakaRua test = new BakaRua("tdiant", "hello!!");

我们想把test保存在配置文件里怎么办?
很遗憾,getConfig().set("demo",test);是行不通的.

哪些东西可以直接set保存呢?
类似getInt, 所有拥有get方法的类型都可以直接保存. (包括List)

还有一些BukkitAPI给的类型, 例如ItemStack. 但不是全部都是这样.
如果你想判断一个类型是不是可以直接set, 你可以在JavaDoc中找到它, 看它是否实现了ConfigurationSerializable类.

你可能想到了最简单粗暴的办法:

//这样set
getConfig().set("demo.name",test.name);
getConfig().set("demo.str",test.str);
//然后保存, 用的时候这样
getConfig().getString("demo.name");
getConfig().getString("demo.str");

这的确是一种切实可行的办法. 但是这真的是太麻烦了. 有没有一种方法直接set test这个对象, 直接get就得到这个对象的办法呢? 有! 你可以使用序列化和反序列化实现它!

让自定义类型实现序列化与反序列化

以上文BakaRua为例. 首先让他实现ConfigurationSerializable, 并添加deserialize方法. 如下:

public class BakaRua implements ConfigurationSerializable {
    public String name;
    public String str;

    public BakaRua(String name, String str) {
        this.name = name;
        this.str = str;
    }

    @Override
    public Map<String, Object> serialize() {
        Map<String, Object> map = new HashMap<>();
        return map;
    }

    public static BakaRua deserialize(Map<String, Object> map) {

    }
}

然后继续完善serialize, 实现序列化. 我们只需要把需要保存的数据写入map当中即可.
注意, 需要保存的数据要保证可以直接set, 不能则也需要为他实现序列化与反序列化.

@Override
public Map<String,Object> serialize() {
    Map<String,Object> map = new HashMap<>();
    map.put("name",name);
    map.put("str",str);
    return map;
}

序列化后, 数据即可直接set进配置文件里. 为了实现直接get的目的, 还需要进行反序列化.

public static BakaRua deserialize(Map<String, Object> map) {
    return new BakaRua(
        (map.get("name") != null ? (String) map.get("name") : null),
        (map.get("str") != null ? (String) map.get("str") : null)
    );
}

编写完毕后, 我们需要像注册监听器一样, 注册序列化. 在插件主类的onEnable中加入如下语句:

ConfigurationSerialization.registerClass(BakaRua.class);

至此, 你就可以自由地对一个自定义的对象直接地get和set了!

配置文件的遍历

试想, 如果存在下面的配置文件:

demo_list:
   a: 1
   b: 233
   c: 666
   d: lalalalalal

我应该如何对demo_list的子键进行遍历, 得到所有子键的对应值?
最简单错报的方式就是将demo_list.a键、demo_list.b键...依次读取. 但这是建立在你知道demo_listabc...这些子键的基础之上的.
如果我事先不知道demo_list的子键都各自叫什么, 又应应该如何对demo_list的子键进行遍历, 得到所有子键的对应值?

配置片段 ConfigurationSection

我们可以把demo_list键对应的部分拆出来.
下文假设config对象是我们现在正在操作的FileConfiguration对象.

ConfigurationSection cs = config.getConfigurationSection​("demo_list");

这里我们得到了ConfigurationSection对象, 这个对象可以当做config对象demo_list键部分的片段, 等效于这个yaml数据:

a: 1
b: 233
c: 666
d: lalalalalal

对于一个ConfigurationSection对象, 其代表着一个完整配置数据的某个片段, 你不能利用诸如saveConfig的方式保存这个片段到另外一个yml文件里.

利用getKeys实现遍历

在上面我们得到了ConfigurationSection对象, 这代表着config对象demo_list键部分的片段.
现在问题转化成了, 如何获取到ConfigurationSection对象里的所有键.
可以利用getKeys(false)的方式达到目的.

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数据:

test:
  a:
    b: 1
  c: 2
d: 1

我们得到了这个配置文件的FileConfiguration对象config, 现在对其用getKeys(false)进行遍历, 得到所有键.

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)会递归获取配置文件里所有出现的键.

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实例.
例子:

Bukkit.getScheduler().runTask(this, new Runnable() {
    @Override
    public void run() {
        // 逻辑代码
    }
});

另一个是构造一个继承org.bukkit.scheduler.BukkitRunnable的匿名内部类, 就像这样:

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 不是线程安全的.
什么是线程安全呢?

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
“引自百度百科”

大多数集合不是线程安全的, 比如经常使用的HashMapArrayList. 同样适用于非线程安全的对象.
限于篇幅, 这里不作深入探讨. 想要了解更多, 请询问您的书籍与搜索引擎.
Bukkit 中的线程安全?
Minecraft 中几乎所有的游戏逻辑都运行于主线程中, 而插件的大多数逻辑也是运行于主线程中的, 这包括插件命令的执行、(同步)事件的处理等等.
如果我们调度了一个异步任务, 或者处于异步事件中, 那么就不应当访问与Minecraft游戏内容有关的API(比如操作方块、加载区块、踢出玩家等). 尝试这么做极有可能得到异常, 使得插件崩溃.

如何在异步任务中调度同步任务, 以访问 Bukkit 的非线程安全的方法?

一种就是BukkitScheduler.runTask (方法不带asynchronously字眼). 这返回的永远是同步任务, 可以大胆访问 Bukkit API, 就像这样:

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方法写上这一行代码:

Bukkit.getScheduler().cancelTasks(plugin);

其中plugin是你的插件实例, 通常是this.
如果不这么做,那么你的插件被关闭之后, 残存的任务(一般是重复任务)仍在运行, 任务会调用相关变量, 而你在关闭插件时如果清理了那些变量, 将会导致一些无法预料的问题.

小技巧

使用 lambda 表达式替换匿名内部类

自Java 8开始提供对 lambda 表达式的支持. 匿名内部类转 lambda 表达式可使代码看上去更加简洁漂亮. 比如

scheduler.runTask(this, new Runnable() {
    @Override
    public void run() {
        System.out.println("这是从在任务中输出的一句话.");
    }
});

可以替换成:

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 以及询问搜索引擎.

直接上食用方法吧! 这是一个使用主线程获取当前在线玩家数量并返回的例子:

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得人心.

自定义合成表

在背包、工作台中, 玩家可以通过指定的物品摆放, 消耗所摆放的物品得到新物品, 这被称作物品的合成. 物品的摆放方式与得到的新物品即为合成表.

合成表物品摆放的文字表述法

如何用文字表述物品在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方法的适当位置添加如下内容:

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方法中添加如下内容:

getServer().clearRecipes();

经验证可发现, 现在我们可以通过控制台通过在铁锭周围围一圈金锭的方式合成绿宝石了.

那么设x为金锭, 我想实现像熔炉那样, 八个金锭围一圈合成一个绿宝石, 不需要铁锭了. 就像这样:

xxx
x空x
xxx

中间有个位置是空的, 该怎么办? 应该设个y表示AIR吗? 不需要, 空位置可以使用空格表示. 就像下面这个例子:

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摆放方式:

.shape("x") //1X1(就像按钮那样的摆放方式)
.shape("xx","xx","xx") //2X3(就像木门那样的摆放方式)
.shape("xx","xx") //2X2(就像合成台那样的摆放方式)

如果你这样设置

.shape("xx ","xx ","   ")

那玩家在游戏中只能这样在合成台合成:

xx空
xx空
空空空

而不能用其他等效的位置摆放合成, 比如这样:

空空空
xx空
xx空

粒子效果及音效播放

几何初步

几何基础知识是做特效的基础内容,应当了解。
作者高中数学没及过格,这里内容仅供参考。
若您了解相关内容或接受了高中数学的有关学习,您可以跳过这部分内容。
不建议跳过这部分内容。

限于篇幅,若需要查看请点击下方链接:
几何初步

粒子效果

客户端正常配置时,若对草方块上使用骨粉,草方块上会长出草丛,同时还会生成绿色的颗粒动画. 这样的动画效果就是Minecraft中的粒子效果.

播放粒子效果

如果想在某一个Location对象所对应的位置播放粒子效果,对于不同的Minecraft版本有不同的方案:

PlayEffect

可以利用World类的PlayEffect方法:
对于Effect,BukkitAPI在后续的更改中,其中的枚举几乎都或多或少有些许改动。开发时应小心。

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有意将SoundVisual这两个概念分隔开,对于粒子效果,在使用spawnParticle方法时,取Effect而代之的是Particle枚举.

spawnParticle的用法较多,在此略去大篇幅对各个方法与参数的介绍,可以查阅JavaDoc,其中有十分简单易懂的注释.
BukkitAPI后续更新中,枚举或多或少都有变动,应当注意!

播放所需的形状

开发实例: 在玩家脚底播放一圈半径为1的粒子效果

分析

  1. 几何角度考虑

以玩家脚底处为原点,建立平面直角坐标系. 如下图所示:

绿色部分为粒子效果

由圆的定义知,所绘制的粒子为到原点的点集.

  1. 实现
    播放想要的形状就是逐次的在所需播放的坐标处播放粒子效果.

这里将不解释什么是弧度制,而是做强制要求,只要算角度都必须用这样的方法变为弧度制,有兴趣可以在网上查阅

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后续更新中,枚举或多或少都有变动,应当注意!

几何初步

几何基础知识是做特效的基础内容,应当了解。
作者高中数学没及过格,这里内容仅供参考。
若您了解相关内容或接受了高中数学的有关学习,您可以跳过这部分内容。
不建议跳过这部分内容。

坐标系

Minecraft是一款3D游戏。对于任意一个方块,都可以用X、Y、Z来表示他的位置。

Minecraft采用的是右手坐标系。
试试看,如果可以的话,伸出右手,中指指向自己,食指指向天,大拇指指向你的右方. 把大拇指指向作为X轴的正方向,中指指向作为Z轴的正方向,食指指向作为Y轴的正方向,那么你可以建立这样的坐标系:

图源自网络. 我懒得画图了...

平面直角坐标系中的三角函数

概念

我们暂时只画X轴和Y轴,即平面直角坐标系. 这是我们在初中阶段所熟知的坐标系.

对于上图中的∠α, 根据初中我们所知的三角函数定义,我们可以知道:

α的正弦值 sinα=MP/OP=b/r
α的余弦值 cosα=OM/OP=a/r
α的正切值 tanα=MP/OM=b/a

三角函数的诱导公式

通过上面的方法,此我们可以轻易推算出0至90°内任一角的sin、cos、tan值.
后续经过大量的事实、数学推导以及无数数学家的研究,三角函数有这些规律性:

对于一个整数k,任一角α,我们有:

公式一:设α为任意角,终边相同的角的同一三角函数的值相等
sin(2kπ+α)=sinα(k∈Z)
cos(2kπ+α)=cosα(k∈Z)
tan(2kπ+α)=tanα(k∈Z)
公式二:设α为任意角,π+α的三角函数值与α的三角函数值之间的关系
sin(π+α)=-sinα
cos(π+α)=-cosα
tan(π+α)=tanα
公式三:任意角α与-α的三角函数值之间的关系
sin(-α)=-sinα
cos(-α)=cosα
tan(-α)=-tanα
公式四:利用公式二和公式三可以得到π-α与α的三角函数值之间的关系
sin(π-α)=sinα
cos(π-α)=-cosα
tan(π-α)=-tanα

以及其他一些公式

任意角的三角函数

利用这些公式我们可以推算出,对于任一角的sin、cos、tan值为多少.

这些是常用的三角函数值,在高中阶段应当熟练背会.

三角函数的规律性周期变化

通过上面的分析,我们可以使用画图的方法,得到三角函数的图像,并且得知三角函数是周期性函数:

世界生成器

本章编写参考了如下内容, 这篇文章对于插件开发而言十分重要:
https://www.mcbbs.net/thread-811614-1-1.html

如果你对Minecraft 1.13中世界生成机制大改动感兴趣, 可以点击这里.
并且, 对于Minecraft 1.13之前版本的世界生成阐述, 可以见此文

在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).

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;
                }
            }
        }
    }
}

最终效果如下:

可以发现, 生成的world中, 按照我们的设定, 在地表草方块上零散的分布了钻石块.
这说明在Bukkit中, 你可以创建一个BlockPopulator对象, 在世界初始化时添加为某一World的Populator, 依此来干涉Population阶段.
Bukkit中的Populator只有BlockPopulator一种.
但是你可以以此类推, 通过这种方式实现在地面随机生成某种建筑等其他效果.

值得注意的是, 在自定义的Populator中, populate方法的参数中有一个传入的Random对象.
这是为了让随机数的生成符合World对应的种子. 在需要生成随机数的时候, 应尽可能使用方法参数中的Random对象.

控制Generation

通过控制一个世界的Generation, 我们可以控制世界的大体地形.
下面我们将在插件加载时, 生成一个新的世界RuaWorld, 这个世界是一个超平坦世界, 第一第二层为基岩, 第三层为草方块.

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;
    }
}

我们进入RuaWorld世界, 可以发现世界按照我们所需要的地形生成了.

VaultAPI

Vault插件

服务器里经常有服主会安装经济插件,实现/money功能. 此功能Essentials插件可以提供,还可以通过下载其他插件CraftConomyiConomy等等实现这一功能.

如果你想写个插件,给玩家加钱或者扣钱应该怎么办?
最简单的想法就是看看你所用的经济插件有没有API,通过你所用的经济插件作者给的API,调用API实现.
但如果你想做一个插件,兼容大多数的经济插件应该怎么办?难不成需要对所有经济插件的API一个个手动做兼容?那是不可能的。

但是大多数的经济插件都对Vault插件做了支持!
Vault插件本身无作用,但是它可以为开发者提供经济API,你可以通过Vault实现对几乎所有经济插件的兼容。

Vault插件除了有经济API以外,还有有关权限管理的API和玩家聊天相关的API(主要是设置聊天前缀后缀),但实际开发中不常使用,感兴趣可以进行了解,本章将不赘述。

Vault的JavaDoc(非官方):
https://pluginwiki.github.io/VaultAPI/

经济API的使用

准备工作

首先在plugin.yml中将vault加入depend. 在主类中定义这样的属性:

public Economy economy = null;

onEnable方法内前部加入这样的代码检查:

        //初始化Vault Economy API
        //可以根据实际需要进行修改
        public RegisteredServiceProvider<Economy> vaultEcoAPI = 
            getServer().getServicesManager().getRegistration(net.milkbowl.vault.economy.Economy.class);
        if (vaultEcoAPI != null || (economy = vaultEcoAPI.getProvider()) == null) {
            getLogger().info("Vault尚未配置正确!请检查Vault插件!");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

简单的操作方法

如果你有一个OfflinePlayer对象或玩家的名称(官方不推荐使用名称),你可以进行如下操作:

economy.hasAccount(player);                    //检查玩家在经济插件中是否有账户(一般来说不判断,直接给钱应该没问题,扣钱的话如果没账户,部分老插件会报错)
economy.getBalance(player);                    //获取玩家player余额
economy.has(player, 233.0);                    //检查玩家player账户里有没有233的余额
economy.withdrawPlayer(player, 233.0);         //给玩家player扣233
economy.depositPlayer(player, 666.0);          //给玩家player加666
分类
Xkms

XINGapi——基于樱花镜像站和bmclapi的新一代MC服务端api

XINGapi——基于樱花镜像站和bmclapi的新一代MC服务端api

本人正在开发XST[Server Tool],找到一个服务器镜像站,可API过于多,所以此API是基于樱花镜像站上

为什么不用自己的服务器下载?

因为太麻烦 服务器只有1Mbps带宽,要啥自行车

这是优化API,由于基于bmcapi所以我们并不封装协议

api调用格式

106.14.64.250/api.html

获取端的类型

格式

["Minecraft_Server","PaperSpigot","Catserver","Forge","SpongeForge","Spigot","NukkitX","Nukkit","Bedrock_Server","CraftBukkit","SpongeVanilla","Cauldron","CubeRite","Akarin",]

获取一种端的下载地址

106.14.64.250/Minecraft_Server示例.html

格式

[ { "name": "minecraft_server.1.10.1.jar", "size": 9459770, "date": "2018-10-11 00:33:20", "file": "https:\/\/cdn.zerodream.net\/download\/server\/Minecraft_Server\/minecraft_server.1.10.1.jar" }, { "name": "minecraft_server.1.10.2.jar", "size": 9459897, "date": "2018-10-11 00:33:21", "file": "https:\/\/cdn.zerodream.net\/download\/server\/Minecraft_Server\/minecraft_server.1.10.2.jar" }]

好了,开发者可以拿去用了

如果可以的话,要不赞助我们,蟹蟹٩('ω')و

afdian.net/@xingxing520

分类
Xkms

隆重发布——XKms V1正式版

Xkms激活工具

这是一个由xingxing制作的kms小程序,它可以帮您激活win10

废话不多说————

记得管理员启动!!!

上链接:XKms下载

临时下载点: 下载点 下载点2 下载点3

请注意:此程序无毒 安全请放心下载 ——可能不支持(vol版)office激活——

三丰云服务器维护中,请不要选择

office激活推荐OTP(Office Tool Plus) 前往官网 直接下载