Skip to content

Latest commit

 

History

History
661 lines (449 loc) · 29.5 KB

EX-1-1.md

File metadata and controls

661 lines (449 loc) · 29.5 KB

EX-1-1 登录插件

行动背景

所有未开启正版验证的服务器(相信你应该进入过许多),都必须安装登录插件。否则,任何玩家都可以使用别人的 ID 登录到服务器,获取她的财产,这可是不行的。

因此我们需要进行验证。

这将是你要动手设计编写的第一个插件,感觉如何?你可能只经过了几天甚至几个小时就读到了这里,没错,下面就是我们的插件。插件叫什么呢……就叫「HarmonyAuth」吧!希望它能守护服务器的和谐~

行动规划

行动名称:HarmonyAuth

行动代号:EX-1

行动类别:演习

涉及章节:

  • EX-1-1
  • EX-1-2

难度:僵尸

开发插件,从设计功能开始。这里先不考虑代码,只考虑逻辑。

玩家登录服务器时:

  • 玩家是否已经注册?如果已经注册,将玩家的名字添加到一个「限制列表」中并提示玩家进行登录或注册。

玩家尝试移动、破坏方块、与实体交互时:

  • 玩家是否在「限制列表」中?如果是,拒绝该操作。

玩家使用登录命令时:

  • 玩家是否已经登录?如果是,拒绝该操作;否则继续:

  • 玩家是否已经注册?如果不是,记录密码并将玩家的名字移出「限制列表」;如果是,验证密码。

  • 密码是否匹配?如果是,将玩家的名字移出「限制列表」;如果不是,提示玩家再试一次。

这里我们认为登录命令和注册命令的效力一样。

玩家离开服务器时:

  • 将玩家的名字移出「限制列表」。

对,就这么多,这就是一个基本的登录插件的功能,下面我们来实现它们……

开始行动

按照上一章的方法,在「Project Structure」下的「Modules」中创建新模块「HarmonyAuth」(左上角的「+」,还记得吗),并为它添加依赖:在右侧的「Dependencies」窗口的最下方单击「+」,选择「Library」,并单击「spigot-1.16.5」,按「OK」。

MODULEDEP.png

单击「Apply」、「OK」。

右键 src,「New」、「Package」,又到了给包命名的时刻。

!> 请自己命名
这一次,不要在这里再写上 rarityeg.harmonyauth 之类的东西了,那是我的名字。现在这是你自己的作品了,为它起个响当当的名字吧!包的命名我们在之前讲过了,也不必局限于一个点(两级)。
可以学习 Bukkit 的命名方式,例如 net.mcbbs.<你的名字>.ha 或者 org.blahblah.myplugins.login 之类的……不要照搬,我说清楚了吗

为了便于演示,示例代码仍旧是在 rarityeg.harmonyauth 包下创建的(因为那是写的),但是出自手的代码,就该自己命名啦~


接下来该做什么?想想?一个插件——创建插件主类,继承 JavaPlugin 类!对了,就是这样!

右键刚刚创建的包,「New」、「Java Class」,填入 HarmonyAuth

我们顺便也把 onEnable 方法重载(别忘了 @Override)。

?> 善用自动补全
IDEA 提供了极为强悍(乃至可怕)的自动补全能力,比如,当你输入 JavaP 的时候,它就会在你的光标下弹出一个小窗口,里面显示了所有可能的补全选项,你可以用上下键选择,按 Tab 接受建议。
此外,IDEA 对于有问题的代码,会在其下方画一条小红线。你可以把鼠标移动到红线上来查看错误报告以及修复办法,比如:
AUTOTAB.png

package rarityeg.harmonyauth;
// 你的代码中可能和我不一样,保持原状,不要修改……这是 IDEA 为你写好的

import org.bukkit.plugin.java.JavaPlugin;
// 如果你使用了上面的自动补全,这一行 IDEA 会帮你写好

public class HarmonyAuth extends JavaPlugin {
    @Override
    public void onEnable() {
        
    }
}

基本上没什么内容。

要注意的是,package 语句指明了当前类所属的包,IDEA 会自动为你生成它。我已经说过你应该自己命名,因此你的这一条语句应该和我的不一样——再说一遍,保持原状自己的事情自己做

关于 import,这里有个小技巧。

?> 小技巧
先不编写 import 语句,先写 extends JavaPlugin,并使用 IDEA 的自动补全(上下键选择后按 Tab 接受),IDEA 就会自动帮你写好 import,多方便啊!
那我要是忘了自动补全,自己把它写完了呢?那也不需要删掉再来一次。首先,JavaPlugin 会被 IDEA 标为红色,表示「找不到这个类」,接着,把鼠标放到红色的 JavaPlugin 上,按下「Import class」,IDEA 就会帮你导入它。
AUTOIMP.png
总之,红色在 IDEA 里就表示「错误」,所以,看到任何红色的地方,不要犹豫,把鼠标放上去,看看到底哪里出错了吧!

事件监听器

我们先来完成事件监听器。事件处理器要实现 org.bukkit.event.Listener,如果你不记得了,请参考 2-2 的内容、

!> 当心同名!
许多接口都被称为 Listener,请确保你实现的是 org.bukkit.event.Listener!你可以通过 IDEA 快速补全右侧显示的包名来判断,按上下键选择。

package rarityeg.harmonyauth;

import org.bukkit.event.Listener;

public class EventListener implements Listener {
}

我们添加一个事件处理函数,监听玩家登录事件 PlayerLoginEvent

package rarityeg.harmonyauth;

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

public class EventListener implements Listener {
    @EventHandler
    public void onPlayerLogin(PlayerLoginEvent e){
        
    }
}

?> 快速搜索
除了之前说到的查询 JavaDocs 的办法,在 IDEA 中,你还可以通过按两下 Shift 进行全局搜索,输入你要查找的内容并在顶部选择「Classes」即可查找类,如果想查找方法,可以使用「Symbols」。
(实际上在「All」中搜索也可以,但是「All」有时候会莫名其妙的找不到)
再强调一遍,Bukkit 对事件的命名都是 <主体><行为>Event,例如 PlayerLoginEvent(玩家登录事件)和 InventoryClickEvent(物品栏被点击事件),多试几次就能够找到的。

限制列表的实现

回到正题上来。刚刚我们说,玩家登录时应该做什么?

添加名字到限制列表中

那么我们需要一个单独的类,用于存储这个列表。创建类 LoginData并使用一个 ArrayList 存储数据。这里的 Listjava.util.List,别写错了!

List 和数组一样能够存储一系列对象,但它更强大、更灵活。

package rarityeg.harmonyauth;

import java.util.ArrayList;
import java.util.List;

public final class LoginData {
    public static final List<String> RESTRICTS = new ArrayList<>();
}

如果一个类没有子类,那么将该类设为 final 可以些许加快 JVM 的处理速度。

?> 到底怎么回事
虽然类应该被用来描述对象,但这里我们只需要用它来存储一点数据,因此设置了一个静态的 List,它属于 LoginData 这个类,而非它的实例,这就使得任何地方都可以访问 RESTRICTS,并且访问到的都是同一个东西。这就实现了「限制列表」。

这里出现了一点新知识:<String>

这是模板化的意思。什么是模板化呢?

就拿 List 举例,List 用于存储数据。那为了存储 String,我们需要创建 StringList,为了存储 int,我们需要创建 IntList……

那如果要存储任意类型的数据呢?这就得编写很多很多的 List

当然你会说,用 Object 兜底不就完了?但是,这就会有强制类型转换出错的风险。

万一我今天身体不舒服,写代码脑子一晕,本来该写 (String) 的地方写成了 (int),那谁知道会有什么后果!这个程序也许用来控制航班起落,结果导致机毁人亡;可能用来统计费用,结果你多了几千块的账单……

为了避免这样的风险,Java 采用了泛型与模板类来解决这个问题。

泛型的原理是:在实现时采取「兜底」,使用 Object,在使用时根据传入的类型,「升级」为对应的类。比如,传入 String,Java 就自动生成一个可以存储 String 的列表。多方便啊!这项工作是由 Java 来完成的,我们尽管用就是了。

有关 Java 泛型的高级知识,请参考 RUNOOB Java 泛型教程

因此,为了存储 String,我们要使用 List<String>

这里你可能还有一点疑问:

new ArrayList<>();
  1. 这里的 <> 又是啥?
  2. 为什么是 ArrayList

我一个一个回答。

第一个问题,因为 ArrayList 也是一个模板类,也需要使用 String 进行模板化,但由于我们的代码是:

public static List<String> RESTRICTS = new ArrayList<>();

Java 已经知道我们用 String 模板化了 List,所以它会自动把 ArrayList 也用 String 模板化。因此 <> 里面就什么也不用写。

第二个问题,为什么是 ArrayList

首先,List 是一个接口,你无法创建一个 List,你只能创建它的实现。就像协议书上写着这个那个服务,但协议本身什么也不做,我们需要找到能够实现这个协议的类

List 的实现有很多,ArrayList 是其中比较快的一种。

那为什么不一直使用 ArrayList 呢?

嗯,其实也可以,但所有的地方都要使用 ArrayList

首先你要明白,各个实现都是有好有坏的,ArrayList 牺牲了安全性获得了速度,而 Vector(另一个 List 的实现)则降低速度换取了较高的安全性。

万一哪一天,我的代码要用于一个安全性要求很高的地方呢?这就得手动把 ArrayList 全部换成 Vector!这个工作量是无法承受的。

回过头来,当初为什么要用 ArrayList 呢?

没必要啊!说到底我们只不过是要有 List 的那些功能罢了!换句话说,任何一个 List 的实现都能满足我们的需求,那当然应该使用接口啦!

这就像你找餐馆吃饭一样,不是「因为那家店(ArrayList)好吃(能够实现 List),就一定要去那家店」,而是「只要好吃(实现了 List),哪一家餐馆(ArrayListVector 或者别的)都可以」。


回到开发上来。

直接操作 RESTRICTS 太过暴力,还使得代码难读,因此我们需要创建三个方法:一个用于添加,一个用于移除,还有一个用于查询玩家名字是否存在。

package rarityeg.harmonyauth;

import java.util.ArrayList;
import java.util.List;

public final class LoginData {
    private static final List<String> RESTRICTS = new ArrayList<>();

    public static void addPlayerName(String playerNameIn) {
        String convertedName = playerNameIn.toLowerCase();
        // toLowerCase 返回一个小写的副本,是 String 类的一个成员方法
        if (!RESTRICTS.contains(convertedName)) {
            // contains 方法返回一个逻辑值,! 符号把它变为相反的值,因此这个 if 语句只有在 RESTRICTS 中不含 convertedName 时才会执行里面的部分
            RESTRICTS.add(convertedName);
        }
    }

    public static void removePlayerName(String playerNameIn) {
        String convertedName = playerNameIn.toLowerCase();
        RESTRICTS.remove(convertedName);
    }

    public static boolean hasPlayerName(String playerNameIn) {
        String convertedName = playerNameIn.toLowerCase();
        return RESTRICTS.contains(convertedName);
    }
}

这里的 private 使得 RESTRICTS 只能通过下面的三个 public 方法进行操作,RESTRICTS 不能被外部直接访问,受到保护。

?> Mojang 的 Bug?
按照 Minecraft 规定,玩家名字是区分大小写的,那为什么这里的代码要把玩家的名字转换为小写(convertedName)呢?
这实际上是一个服务端的 Bug,如果有一个玩家叫 RarityEG,并且服主给了她 OP 权限,那这个玩家名的任意大小写,例如 rARityEg 或者 RARITyeG 都是有 OP 权限的。
而如果我们不进行转换,那其它玩家就可以利用这个漏洞注册合法的账号,并且拥有本不应有的 OP 权限。为避免这一点,我们只能全部转换为小写了。

这里无需担忧 List 的容量。要知道一个(单例)服务器在线人数能上百就已经不得了了,这一个 List 消耗的资源可以忽略不计,毕竟,100 人的服务器至少有 8 GB 内存,就算玩家全部采用命名长度上限(32 字节),List 的大小也只有大约 3 KB,只是 8 GB 的 0.0003% 而已。

好了,这样我们就可以在 onPlayerLogin 方法中添加玩家的名字了:

// EventListener 节选
@EventHandler
public void onPlayerLogin(PlayerLoginEvent e) {
    LoginData.addPlayerName(e.getPlayer().getName());
}

PlayerXXXEvent 都是 PlayerEvent 的子类,这些类都有 getPlayer() 方法用于获得涉及到的玩家。仔细想想这实际上很自然:有玩家参与的事件,自然可以获取到玩家嘛。

然后,在玩家离开服务器时,将玩家移出列表吧~

// EventListener 节选
// 创建函数 onPlayerQuit
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
    LoginData.removePlayerName(e.getPlayer().getName());
}

还要在玩家移动、交互、传送时阻止玩家,和这些相关的事件是 PlayerMoveEventPlayerInteractEventPlayerInteractAtEntityEventPlayerPortalEventPlayerTeleportEventInventoryOpenEvent,这可以通过 IDEA 搜索或者 JavaDocs 找到。

另外我们需要进行很多次「判断是否登录,以此决定是否取消」这一操作,因此我们将它写成一个静态方法,这样就可以少写很多重复代码。

// EventListener 节选
public static void cancelIfNotLoggedIn(Cancellable e) {
    // 这里写着 Cancellable,和上面的 List 是一个原理,说到底我们只需要「可以取消」这个功能就可以了,至于到底是哪个类,不重要
    
    
    if (e instanceof PlayerEvent) {
        // instanceof 关键字指示 Java 重新判断左边对象的类型是不是右边的类或者右边类的子类,也就是判断能否进行强制类型转换
        if (LoginData.hasPlayerName(((PlayerEvent) e).getPlayer().getName())) {
            // if 语句用于看看玩家是不是在限制列表中
            // (PlayerEvent) e 进行类型转换
            e.setCancelled(true);
        }
    } else if (e instanceof InventoryOpenEvent) {
        // else if 表示「上一条 if 的条件为假」并且「当前括号中的条件为真」时才执行大括号里面的内容,相当于「如果不是那样,而是这样,就做……」
        
        // 限制玩家打开物品栏,需要 InventoryOpenEvent
            if (LoginData.hasPlayerName(((InventoryOpenEvent) e).getPlayer().getName())) {
                
                e.setCancelled(true);
            }
        }
}

@EventHandler
public void restrictMove(PlayerMoveEvent e) {
    // 移动
    cancelIfNotLoggedIn(e);
    // 你看这多方便
}

@EventHandler
public void restrictInteract(PlayerInteractEvent e) {
    // 交互
    cancelIfNotLoggedIn(e);
}

@EventHandler
public void restrictInteractAtEntity(PlayerInteractAtEntityEvent e) {
    // 实体交互
    cancelIfNotLoggedIn(e);
}

@EventHandler
public void restrictPortal(PlayerPortalEvent e) {
    // 传送门
    cancelIfNotLoggedIn(e);
}

@EventHandler
public void restrictTeleport(PlayerTeleportEvent e) {
    // 传送
    cancelIfNotLoggedIn(e);
}

@EventHandler
public void restrictOpenInventory(InventoryOpenEvent e) {
    // 打开物品栏
    cancelIfNotLoggedIn(e);
}

这里的 InventoryOpenEvent 是一个另类,虽然只有玩家能够打开物品栏,但 Bukkit 认为这个事件和物品栏的关系更密切,所以把它列为了 InventoryEvent,因此我们不得不在 cancelIfNotLoggedIn 中做额外的处理。

后面我们会说到,只监听这些事件是不够的,这里存在很大的漏洞,但毕竟这不是成品,我希望你能把注意力放在更重要的事件监听和命令处理上。如果一开始就让大家知道现有的插件都考虑地如此周到,大家肯定会想:果然初学者的作品,没法和老牌插件比。这可是很不好的。

命令处理器的实现

如果你忘了命令处理器的内容,请重新阅读一下 2-4。

src 下创建 plugin.yml 文件,填写主类(包名 + 类名),并且追加有关命令的信息:

main: rarityeg.harmonyauth.HarmonyAuth
# 这里改成你的包名!
version: 1.0
api-version: 1.16
name: HarmonyAuth
commands: 
  login:
    aliases: 
      - "lg"
      - "l"
      - "L"
    description: "Login or register"
    usage: "/login <PASSWORD>"

!> 注意命名
还记得吗?main 指向的是你的插件主类,这里的示例代码指向的是我的插件主类。因此你要根据你取的名字进行相应的修改,如果你的包叫做 xxx.yyy.zzz,这里就写 xxx.yyy.zzz.插件主类的名字,以此类推,不要照搬

你不会傻到把「插件主类的名字」这几个字敲上去吧?不会吧不会吧!哎哟……本小马为了让你们听懂已经操碎了心喂……

接下来我们回到代码部分。

在你的包里(不是 src 下!)创建 CommandHandler 类(使用不同的名字只是为了防止冲突):

package rarityeg.harmonyauth;
// 仍旧,这个让 IDEA 自己决定

import org.bukkit.command.CommandExecutor;

public class CommandHandler implements CommandExecutor {
}

这个时候 IDEA 应该会在这一行底部画一条红线:

public class CommandHandler implements CommandExecutor {

因为我们还没实现 onCommand 方法,补上这个方法(除了手动输入,也可以把鼠标放到红线上,按「Implement methods」让 IDEA 帮你补上):

package rarityeg.harmonyauth;

import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;

public class CommandHandler implements CommandExecutor {
    @Override @ParametersAreNonnullByDefault
    public boolean onCommand(CommandSender commandSender, Command command, String s, String[] strings) {
        return false;
    }
}

IDEA 的自动命名并不是很好……我们修改一下,修改后的函数签名像这样:

public boolean onCommand(CommandSender sender, Command command, String label, String[] args);

那么接下来就很简单啦,我们检查玩家有没有注册……等一下!还没有实现这个呢!

数据的存储

先把命令处理器放一放,我们来实现数据的存储。由于使用数据库太过复杂,我们这次先用文件存储。

记得之前我们说到的 Bukkit 默认配置文件吗?

src 下创建 config.yml,什么也不用写。(本来就是用来存储数据的……)

然后我们还需要一个单独的类来读取文件,创建类 ConfigReader

package rarityeg.harmonyauth;

public final class ConfigReader {
    public static boolean isPlayerRegistered(String playerName) {
    }
    // 查询是否注册

    public static boolean verifyPassword(String playerName, String password) {
    }
    // 验证密码

    public static void addPlayer(String playerName, String password) {
    }
    // 注册
}

这里也是用来「存储」一些方法,这个类并不用来描述某个对象,只是用来「存放东西」。因此将这些方法设为了 static 以便在其它地方使用。

getConfig 方法可以获得插件实例的配置文件,只需要使用 <插件实例的变量名>.getConfig 就行了……可是我们怎么获取插件实例呢?!

这里我们要介绍一个小技巧。不要想「怎么在茫茫的内存中找到那个实例」,而要想「怎么在我们还能够使用插件实例的时候将它放到一个之后能找到的地方」。

不要等到失去了才追悔莫及!

什么地方能够访问插件实例呢?记得 TR-2 中的内容吗?

在插件主类中,this 就代表插件实例。

那怎么放到一个能找到的地方呢?我在 2-1 中说过:

static不能修饰外部类,修饰方法时表示该方法不属于哪个对象,而属于整个类共有;修饰变量时表示该变量不属于哪个对象,属于整个类共有,要调用静态方法和变量,不使用 <对象名>.<方法或变量的名字>而使用 <类名>.<方法或变量的名字>

看到了吧!机会就在这里!类名是 HarmonyAuth,是不会改变的,那么我们只需要将 this 赋给一个静态变量就可以了嘛!

修改 HarmonyAuth(插件主类):

package rarityeg.harmonyauth;

import org.bukkit.plugin.java.JavaPlugin;

public class HarmonyAuth extends JavaPlugin {
    public static JavaPlugin instance;

    @Override
    public void onEnable() {
        instance = this;
    }
}

我们一开始先声明一个变量 instance,可以不必初始化,相当于「占个位置」,又由于它是 static,因此稍后可以通过 HarmonyAuth.instance 获得这个变量。

然后在插件被启用时(也就是尽可能早的时候)将这个之前占了位置的变量改为真正的实例。现在再通过 HarmonyAuth.instance 得到的就是插件实例啦!

回到 ConfigReader

package rarityeg.harmonyauth;

import org.bukkit.configuration.file.FileConfiguration;

public final class ConfigReader {
    
    public static FileConfiguration config = HarmonyAuth.instance.getConfig();
    // 由于三个方法都要使用,我们将这个变量抽取出来到最外层
    public static boolean isPlayerRegistered(String playerName) {
        
        return config.contains(playerName.toLowerCase());
    }

    public static boolean verifyPassword(String playerName, String password) {
        return password.equals(config.getString(playerName.toLowerCase()));
        // 三步合成一行:转换小写,读取字符串,返回是否相等
    }

    public static void addPlayer(String playerName, String password) {
        HarmonyAuth.instance.getConfig().set(playerName.toLowerCase(), password);
        HarmonyAuth.instance.saveConfig();
    }
}

这里的实现也很简单,我们通过 getConfig 获得了插件的配置文件,然后通过 getString 读取,set 设置,分别实现了「是否存在」、「密码是否正确」和「添加玩家」的功能。同样,这里把名字转换为了小写。

!> 漏洞
上面这些代码存在一个巨大的漏洞。
还记得我们说过的安全性吗?这里就是一个例子:明文存储密码。服主拿到玩家的密码会做什么?谁也不敢保证。
正规的操作是使用哈希算法单向加密进行保存,验证时也进行哈希加密。但那样会添加很多与插件开发关系不那么紧密的代码,不利于大家的学习,因此这里没有这样做。
在我们后面编写使用数据库的 HarmonyAuth SMART(HarmonyAuth 的增强版本)时会再次提到这一点,并使用哈希算法。现在笔者希望大家把注意力放在事件监听和命令处理这些更重要的地方。

好了,现在我们可以继续实现命令处理器了~

再谈命令处理器

回到 CommandHandler 类中,现在我们知道应该做什么了:

package rarityeg.harmonyauth;

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 javax.annotation.ParametersAreNonnullByDefault;

public class CommandHandler implements CommandExecutor {
    @Override
    @ParametersAreNonnullByDefault
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player)) {
            // 如果 sender 是 Player 的实例,那么这条命令是玩家发送的,反之则不是
            return false;
            // 服主也有可能从服务器控制台使用命令,先确认命令来自于玩家
        }
        if (!LoginData.hasPlayerName(sender.getName())) {
            sender.sendMessage(ChatColor.GREEN + "你已经登录了!");
            return true;
            // 已经登录了,就没必要验证了
        }
        if (args.length == 0) {
            sender.sendMessage(ChatColor.RED + "必须输入密码!");
            // sendMessage 用于向该终端(玩家或控制台)发送消息
            return false;
            // 参数不完整,拒绝处理
        }
        String pwdConcat = String.join("<space>", args);
        // 玩家的密码可能含有空格,join 将它们粘在一起,<space> 只是定义的分隔符,换成别的也行
        if (ConfigReader.isPlayerRegistered(sender.getName())) {
            // 已经注册
            if (ConfigReader.verifyPassword(sender.getName(), pwdConcat)) {
                // 验证密码
                LoginData.removePlayerName(sender.getName());
                // 解锁玩家
                sender.sendMessage(ChatColor.GREEN + "登录成功,欢迎回来!");
            } else {
                sender.sendMessage(ChatColor.RED + "密码错误!");
            }
            return true;
            // true 代表语法正确,虽然密码错误,但语法正确即可返回 true

        } else {
            // 玩家没注册,准备注册
            ConfigReader.addPlayer(sender.getName(), pwdConcat);
            // 注册玩家
            LoginData.removePlayerName(sender.getName());
            // 解锁玩家
            sender.sendMessage(ChatColor.GREEN + "注册成功!");
            return true;
        }
    }
}

有了注释,这里的代码应该比较简单。唯一需要说明的是 ChatColor,这个枚举中包含了 Minecraft 聊天中用到的颜色和样式,这里我们用到了 ChatColor.REDChatColor.GREEN,看字面意思就知道。颜色直接附加到字符串之前就行了。在 Java 中,字符串的「相加」就是把它们拼到一起。

实际上你也可以使用 §,这是 Minecraft 的颜色表示。ChatColor 实际上只是对应符号的助记版本。虽然使用 § 也没有问题,但如果出现编码不兼容的问题,可有你苦头吃的。对于 PC 键盘,按住 Alt 并连续输入 0167,随后松开 Alt 即可输入该符号。

好,这样命令处理就完成了。

杂项处理

命令处理器准备好了,事件监听器准备好了,我们还得让 Bukkit 知道啊!

在主类中做这些处理:

package rarityeg.harmonyauth;

import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.Objects;

public class HarmonyAuth extends JavaPlugin {
    public static JavaPlugin instance;

    @Override
    public void onLoad() {
        saveDefaultConfig();
        // 如果配置文件不存在,Bukkit 会保存默认的配置
    }

    @Override
    public void onEnable() {
        Bukkit.getPluginManager().registerEvents(new EventListener(), this);
        // 注册事件处理器,这里必须实例化,this 表明注册到本插件上
        Objects.requireNonNull(Bukkit.getPluginCommand("login")).setExecutor(new CommandHandler());
        // 注册事件处理器,也要实例化,requireNonNull 是不必要的,但是万一插件损坏了或者 Bukkit 出错了,我们还能知道是这里出问题
        instance = this;
        // 小技巧:暴露实例
    }

    @Override
    public void onDisable() {
        saveConfig();
        // 保存配置
    }
}

Objects.requireNonNull 方法只接受一个参数,如果它是 null,就抛出异常,如果不是,就将它原封不动地返回。通常用它来确认那些不应该为 null 的东西。使用这个方法,就免去了多一个 if 的麻烦。

saveConfig 将修改切实保存入文件。

这里除了 onEnable 外,我们还重写了 onDisableonLoad 方法,这两个方法分别在插件关闭前和插件加载中调用。一般我们让保存默认文件操作在 onLoad 阶段完成,保存修改后的文件在 onDisable 阶段完成,而主要初始化工作在 onEnable 阶段完成。

最终检查

看看各处的代码,哪里还有问题?IDEA 有没有给你指出什么错误?

FINAL.png

这是笔者的最终成果,没有语法错误了。

构建与编译

像上一章一样在「Project Structure」中添加「Artifact」,注意你双击的是「'HarmonyAuth' compile output」,别选错了。勾选「Include in project build」。

ARTIFACT.png

「Apply」、「OK」,单击工具栏绿色锤子按钮「Build Project」,然后到上面的「Output directory」查收结果吧!

CHECKITOUT.png

好了,准备一下,下面我们要开始调试了哦~