[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.\""); } } }
测试效果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)