[Minecraft 插件] 服务器插件开发教程(二)- 事件机制(下)

关键词:示例(登录插件的实现)


  在本篇教程中我们将从零开始实现一个简单的登录插件。(上一篇教程其实已经给出了部分实现,因此这篇文章可以看作是对它的补充)

  请注意,以下内容更多地只是我学习过程的记录,顺便充当教程的功能,因此会不太严谨,代码也存在很多不规范的地方,细节上和市面上主流的插件实现也可能大相径庭,不过我保证它能实现预期的效果。

  我们的插件将具有如下特性:(1)对于未注册以及已注册但未登录的玩家,限制他们的行动(比如无法破坏方块或者严格一点——无法移动),并提示他们进行注册 / 登录。(2)对于登录成功的玩家,向他们发送欢迎消息。(3)登录成功后一定时间(这里假定一天,即 86400 秒)内不需要再次登录。(4)提示和欢迎消息均支持彩色文字。

 


  打开 IDEA ,新建项目,注意选择合适的 Java SDK 版本服务端类型

 

  填写相关信息(此处按个人喜好即可),构建工具我选了 gradle,不过选 maven 也没什么很大的区别:

 

  注意选择合适的版本(依你的服务端版本而定),下面的 Optional Settings 部分依个人喜好填写,也可以像我一样留空:

 

  项目创建完成后先 build 一下,确认环境配置无问题(因为我选的是 gradle,所以这里在如图所示的目录下可以找到编译后的插件文件。如果你创建项目时选的是 maven,那么插件文件在 target 目录下):

 


  我们需要解决的第一个问题是:如何区分未注册、已注册未登录和已注册且已登录的玩家?很显然,我们需要存储已注册玩家的 UUID 、对应的密码、上次登录的时间以及其他你想存储的东西(除非有意为之,否则不要存储名字,因为会出现同名的现象),而且相关的数据应当是持久化的,也就是说当服务端关闭再打开之后,已有数据并不会因此发生改变。由此我们可以得出大概的思路:在插件启动时,从文件中读取用户数据到内存;在插件关闭时,将内存中的数据保存到文件中。

  如果想存储 UUID 和与之关联的数据,方法之一是使用标准库的 HashMap。为了简化问题(毕竟我们这里实现的是一个“简单”的插件)这里我们只存储密码。当然,我们不会简单地存储密码的明文,而是对其进行单向加密然后存储相应的密文,这样无论是服务器的管理者还是攻击者都无法轻易得到密码的明文。这里我使用 MD5 算法,因为标准库内有现成的方法可供调用。

  同样,简单起见,这里我们直接让插件主类充当了事件侦听器的功能。

  首先我们利用 Java 标准库内置的 MessageDigest 类进行 MD5 加密,并将这个过程封装为插件主类的一个成员方法。因为代码细节和本教程关系不大,故不在此赘述。

复制代码
public static String getMD5(String s) {
        try {
            MessageDigest digest = MessageDigest.getInstance("md5");
            digest.update(s.getBytes());
            byte[] md5 = digest.digest();
            StringBuilder result = new StringBuilder();
            for (byte b : md5) {
                if (b < 0)
                    b += 256;
                result.append(Integer.toHexString(b));
            }
            return result.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("Error occurred during md5 encryption!");
        }
    }
复制代码

 

  定义一个新的类 UserData,用于存储已注册用户的信息(密码的密文和登录状态),它应当支持序列化和反序列化(这样才能在文件中存取):

复制代码
public class UserData implements Serializable {
    UserData(String _encrypted_password, long _last_login_timestamp) {
        super();
        encrypted_password = _encrypted_password;
        last_login_timestamp=_last_login_timestamp;
    }
    String encrypted_password;
    long last_login_timestamp;
}
复制代码

 

  然后在我们的插件主类里面增加一个成员变量 user_data:

private HashMap<UUID, UserData> user_data;

 

  定义成员方法 loadUserData 和 saveUserData,分别在 onEnable 和 onDisable 方法中调用:

复制代码
    public void loadUserData() {
        String path = getDataFolder() + File.separator + "user_data.dat";
        if (new File(path).exists()) { // 首次启动时文件并不存在,因此需要预先判断。
            try {
                FileInputStream fis = new FileInputStream(path);
                ObjectInputStream ois = new ObjectInputStream(fis);
                user_data = (HashMap<UUID, UserData>) ois.readObject();
                ois.close();
                fis.close();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public void saveUserData() {
        String path = getDataFolder() + File.separator + "user_data.dat";
        try {
            getDataFolder().mkdirs(); // 如果没有事先创建目录,实测下面的操作可能会失败。
            FileOutputStream fos = new FileOutputStream(path);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(user_data);
            oos.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
复制代码
复制代码
    @Override
    public void onEnable() {
        // Plugin startup logic
        user_data = new HashMap<>();
        getServer().getPluginManager().registerEvents(this, this);
        loadUserData();
    }

    @Override
    public void onDisable() {
        // Plugin shutdown logic
        saveUserData();
    }
复制代码

 


  接下来我们解决第二个问题:在玩家进入服务器时,限制玩家的行动,并提示他们进行注册或登录。我们需要编写相关回调方法,并在回调方法内取得用户的 UUID,与内存内的数据进行比对,判断玩家是否注册,然后进行相应的操作。先写一个大致的框架(这里用中文输出信息会乱码,暂时还没有找到解决方案):

复制代码
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        UUID uuid = player.getUniqueId();
        if (!user_data.containsKey(uuid)) {
            player.sendMessage(ChatColor.RED + "[LoginPlugin] Please register to get full access to the functions of this server!");
            // 引导玩家注册
            // ...
        } else {
            UserData userdata = user_data.get(uuid);
            if (System.currentTimeMillis() - userdata.last_login_timestamp <= 86400) {
                player.sendMessage(ChatColor.AQUA + "[LoginPlugin] Welcome to this server!");
            } else {
                player.sendMessage(ChatColor.RED + "[LoginPlugin] Please login first.");
                // 引导玩家登录
                // ...
            }
        }
    }

    @EventHandler
    public void onBreakBlock(BlockBreakEvent event) {
        Player player = event.getPlayer();
        UUID uuid = player.getUniqueId();
        if (!user_data.containsKey(uuid)) {
            event.setCancelled(true);
        } else {
            UserData userdata = user_data.get(uuid);
            if (System.currentTimeMillis() - userdata.last_login_timestamp > 86400) {
                event.setCancelled(true);
            }
        }
    }
复制代码

 


  然后我们再来解决第三个问题:引导玩家注册 / 登录。这里涉及了一个新的知识点——命令。这里我们将引入两条新命令 "/register <password>" 和 "/login <password>" 来分别实现注册和登录操作。

  命令的处理和事件回调方法类似,但它并不属于 EventHandler ,而是继承自 JavaPlugin 类,我们可以重写它来实现命令的处理:

复制代码
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (command.getName().equalsIgnoreCase("register")) {
            return true;
        } else if(command.getName().equalsIgnoreCase("login")) {
            return true;
        }
        return false;
    }
复制代码

 

  以上代码有两个注意点:① onCommand 方法的返回值表示命令的执行结果,即“成功”或“失败”,分别对应 true 或 false。 ② 只要执行某个命令,该方法就会被调用,但是我们并不知道命令具体是什么,所以一般来说需要用若干 if-else 来判断,就像上面代码所写的那样 —— 当然,如果需要判断的命令多起来,可以使用表驱动法。

 

  如果要自定义新命令,还需要在 plugin.yml 里面登记,否则会不起作用,格式如下图所示:

 

 

  然后我们再回到 onCommond 方法上来。前面提到“只要执行某个命令,该方法就会被调用”,因为执行命令的主体不仅仅是玩家,还有可能是命令方块,因此我们也要做相应的判断,受篇幅限制,按照注册 / 登录的处理逻辑,这里直接给出完整代码:

复制代码
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (command.getName().equalsIgnoreCase("register")) {
            if (!(sender instanceof Player))
                sender.sendMessage(ChatColor.RED + "[LoginPlugin] This command can only be issued by a player!");
            else {
                if (user_data.containsKey(((Player) sender).getUniqueId()))
                    sender.sendMessage("[LoginPlugin] You have registered already!");
                else {
                    if (args.length > 1)
                        return false;
                    else {
                        UserData userdata = new UserData(getMD5(args[0]), 0);
                        user_data.put(((Player) sender).getUniqueId(), userdata);
                        sender.sendMessage("[LoginPlugin] Successfully registered. Now you can use " + ChatColor.AQUA + "\"/login <password>\"" + ChatColor.WHITE + " to login.");
                    }
                }
            }
            return true;
        } else if (command.getName().equalsIgnoreCase("login")) {
            if (!(sender instanceof Player))
                sender.sendMessage(ChatColor.RED + "[LoginPlugin] This command can only be issued by a player!");
            else {
                if (args.length > 1)
                    return false;
                else {
                    if (!user_data.containsKey(((Player) sender).getUniqueId())) {
                        sender.sendMessage("[LoginPlugin] You have not registered. Please register first!");
                        sender.sendMessage("Use " + ChatColor.AQUA + "\"/register <password>\"" + ChatColor.WHITE + " to" +
                                " register.");
                    } else if (System.currentTimeMillis() - user_data.get(((Player) sender).getUniqueId()).last_login_timestamp <= 86400) {
                        sender.sendMessage("[LoginPlugin] You have logged-in already!");
                    } else if (user_data.get(((Player) sender).getUniqueId()).encrypted_password.equals(getMD5(args[0]))) {
                        UserData userdata = new UserData(getMD5(args[0]), System.currentTimeMillis());
                        user_data.put(((Player) sender).getUniqueId(), userdata);
                        sender.sendMessage("[LoginPlugin] Successfully logged-in. HAVE FUN!");
                    } else {
                        sender.sendMessage(ChatColor.RED + "[LoginPlugin] Incorrect password.");
                    }
                }
            }
            return true;
        }
        return false;
    }
复制代码

 

  同时我们补全前面的 onPlayerJoin 方法中的提示部分:

复制代码
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        UUID uuid = player.getUniqueId();
        if (!user_data.containsKey(uuid)) {
            player.sendMessage(ChatColor.RED + "[LoginPlugin] Please register to get full access to the functions of this server!");
            player.sendMessage("Use " + ChatColor.AQUA + "\"/register <password>\"" + ChatColor.WHITE + " to" +
                    " register.");
        } else {
            UserData userdata = user_data.get(uuid);
            if (System.currentTimeMillis() - userdata.last_login_timestamp <= 86400) {
                player.sendMessage(ChatColor.AQUA + "[LoginPlugin] Welcome to this server!");
            } else {
                player.sendMessage(ChatColor.RED + "[LoginPlugin] Please login first.");
                player.sendMessage("Use " + ChatColor.AQUA + "\"/login <password>" + ChatColor.WHITE + " to login.\"");
            }
        }
    }
复制代码

 

  测试效果:

 


  完整代码(github)  完整代码 + 预编译文件(蓝奏云)

posted @   ZXPrism  阅读(766)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示