前面几次演习中,调试似乎都很无聊,那是因为那些插件很简单,不容易出错。
但真正开发插件时几乎不可能一次写对。上面两节中的代码有很多漏洞,接下来请诸位跟随笔者,看看我是如何逐一发现并除掉这些 Bug 的。
把构建出来的文件放到 plugins
目录下。这一次建议删除其它插件。
启动一次……
呃,好像爆掉了……
其实这个问题笔者心里已经有数了,我们在构建时貌似是直接将依赖打包进去的,这会导致整个 JAR 包作为一个文件放到我们的插件中, 这是不行的。
输入 stop
停止服务端,停止时似乎还出现了什么错误信息……不管了,我们先把 SQL 弄到位。
回到 IDEA 中,在「HarmonyAuth SMART」工件中,选择「mysql-connector-java-8.0.23」,按「-」移除它,接着在右侧「Available Elements」中找到它,右键,「Extract Into Output Root」将它解压到最终产品中。
「Apply」、「OK」,按下绿色锤子重新构建,再将插件放到 plugins
目录下。
再启动一次……
验证失败是自然的(MySQL 没有设置),但我们默认不是没有启用 MySQL 吗?
哦!对了!虽然配置文件中有这个选项,但我们没有处理这个设置,直接初始化了 DBDataManager
,这里需要修改一下。
// HarmonyAuthSMART 节选
if (getConfig().getBoolean("mysql.enabled")) {
new DBDataManager().loadAll();
}
再构建一次。再试试。
成功!
现在关闭服务器,我们把 MySQL 配置好。如果你的 test
数据库没有删除,那配置很快就完成了。否则,你需要创建新的数据库。
配置如下:
mysql:
enabled: true
host: localhost
port: 3306
username: root
password: mylittlepony # 这是我的密码
db-name: "test" # 数据库名
除了修改一下数据库以外,其它配置我们保持默认。
然后启动一下~
无法保存 data.yml
是正常的,但为什么 Paper 要发一个警告呢……不管了,应该不要紧。
打开客户端,这里我用的是 1.16.5 无正版验证。进入多人游戏,输入 localhost
连接……
呃?为什么我可以移动?
检查代码,我很快就发现了问题:玩家登录时要加入限制列表啊!!!
关闭服务器,修改代码:
// EventHarmony 节选
RuntimeDataManager.addRestrictUUID(e.getPlayer().getUniqueId());
另外我们还将玩家退出时也进行了修改:
// EventHarmony 节选
RuntimeDataManager.removeRestrictUUID(e.getPlayer().getUniqueId());
RuntimeDataManager.exitIForgotMode(e.getPlayer().getUniqueId());
RuntimeDataManager.exitReadMode(e.getPlayer().getUniqueId());
再构建一次。
为了避免数据库污染,我们登录到 MySQL 中删除创建的表 harmony_auth_data
。
USE test;
DROP TABLE harmony_auth_data;
现在再次启动服务端,加入……
(图略)
还是不行!
仔细想想,我们……
哦!我们忘了注册事件处理器和命令处理器!
笔者犯了这么一个大错误实在是不应该啊……
赶紧注册,修改主类:
// HarmonyAuthSMART 节选
Bukkit.getPluginManager().registerEvents(new EventHarmony(), this);
Objects.requireNonNull(Bukkit.getPluginCommand("hl")).setExecutor(new CommandHandler());
Objects.requireNonNull(Bukkit.getPluginCommand("iforgot")).setExecutor(new CommandHandler());
然后重新构建,运行,加入……
也不回弹了,很好!但是同时我们又发现了新错误:
(图略)
Operation not allowed for a result set of type ResultSet.TYPE_FORWARD_ONLY.
看来这是个数据库错误,笔者果然还是对 SQL 了解不深啊……
这就是在 executeQuery
中缺失了那两个参数的后果。
查阅了相关资料后,我发现 createStatement
参数中应当传入 ResultSet.TYPE_SCROLL_SENSITIVE
用于上下滚动和 ResultSet.CONCUR_READ_ONLY
表示只读,这些问题都只出现在 getXXX
方法中,我们修改它们就好……先关闭服务器。
// DBDataManager 节选
PreparedStatement preparedStatement = connection.prepareStatement("SELECT PwdHash FROM harmony_auth_data WHERE UUID=?;", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
这里只列出了一处,但所有的 getXXX
方法我们都要如此修改。
另外,还有 isExist
和 getNextRequest
要修改:
// DBDataManager 节选
@Override
public boolean isExist(UUID id) {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(UUID) FROM harmony_auth_data WHERE UUID=?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
// 这里!
preparedStatement.setString(1, id.toString());
ResultSet rs = preparedStatement.executeQuery();
rs.first();
preparedStatement.close();
connection.close();
return rs.getInt(1) != 0;
} catch (SQLException e) {
putError(e);
return false;
}
}
@Override
public UUID getNextRequest() {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
// 这里!
ResultSet rsc = statement.executeQuery("SELECT COUNT(UUID) FROM harmony_auth_data WHERE IForgotState=true LIMIT 1");
if (rsc.getInt(1) == 0) {
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
ResultSet rs = statement.executeQuery("SELECT UUID FROM harmony_auth_data WHERE IForgotState=true LIMIT 1");
rs.first();
String uString = rs.getString("UUID");
return UUID.fromString(uString);
} catch (SQLException e) {
putError(e);
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
}
重新构建,运行,加入……
java.sql.SQLException: Operation not allowed after ResultSet closed
糟糕!我们在查询数据前就断开了连接!
这很简单,我们只需要把关闭挪到取值之前就可以了。由于 return
会直接离开函数,我们需要用一个变量存储。
// DBDataManager 节选
rs.first();
String res = rs.getString("PwdHash"); // 这里!
preparedStatement.close();
connection.close();
return Objects.requireNonNullElse(res, ""); // 这里!
其它部分也要这样修改。另外我们还将 getNextRequest
中忘记关闭的连接关闭了:
@Override
public UUID getNextRequest() {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rsc = statement.executeQuery("SELECT COUNT(UUID) FROM harmony_auth_data WHERE IForgotState=true LIMIT 1");
if (rsc.getInt(1) == 0) {
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
ResultSet rs = statement.executeQuery("SELECT UUID FROM harmony_auth_data WHERE IForgotState=true LIMIT 1");
rs.first();
String uString = rs.getString("UUID");
statement.close(); // 这里!
connection.close();
return UUID.fromString(uString);
} catch (SQLException e) {
putError(e);
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
}
构建,运行,加入……
好,不出错了!
我们注册……
(图略)
注册成功了,为什么不能动呢?
从上面的测试中我们了解到,注册后可以跳动,说明移动事件没有 setCancelled
。经过检查,我发现我们的代码有点问题。
我们是在玩家移动时才降低玩家移动速度的,而如果玩家没有移动,最后移动速度也恢复不回来。
所以这部分代码应该移动到在 PlayerLoginEvent
中。
// EventHarmony 节选
RuntimeDataManager.addRestrictUUID(e.getPlayer().getUniqueId());
UUID id = e.getPlayer().getUniqueId();
originSpeed.put(id, e.getPlayer().getWalkSpeed()); // 这里!
e.getPlayer().setWalkSpeed((float) 0.00001);
然后把恢复速度的代码放到 CommandHandler
中,注册和登录两部分都要放:
// CommandHandler 节选
RuntimeDataManager.removeRestrictUUID(id);
player.setWalkSpeed(EventHarmony.originSpeed.get(id)); // 这里!
删除数据表,清理脏数据,构建,再次运行。
成功了,同时我们也看到,在登录成功的瞬间客户端有一个加速的动作,这和我们之前见到的其它登录插件很像。
继续测试,看看自动登录能否生效。
话说,如果不正常才麻烦呢,因为笔者实在找不到出错的地方了……
接下来测试一下不自动登录,将 auto-login
改为 0
。重启服务器(这一次不要删数据库)。
系统识别出来了我们已经注册。并且没有自动登录。很好!
接下来模拟密码恢复。
并且数据已经保存到了数据库(用 SELECT * FROM harmony_auth_data
可知)。
我们需要模拟 OP 进行测试……
退出客户端,换一个 ID 登录。
客户端的启动远没有服务端那么快。
我们给这个新 ID 以 OP 权限,登录,运行 /iforgot
:
还是不行?
根据指出的错误行数,我们找到了 getNextRequest
方法。根据错误提示,我觉得应该是忘记了 rsc.first()
。
// DBDataManager 节选
ResultSet rsc = statement.executeQuery("SELECT COUNT(UUID) FROM harmony_auth_data WHERE IForgotState=true LIMIT 1");
rsc.first(); // 这里!
if (rsc.getInt(1) == 0) {
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
再试试。
(图略)
进入和退出审核模式正常了,但发送的消息全是空。怎么回事?
这个问题出现在 CommandHandler
中。
实际上笔者又犯了一个低级错误,这是原来的代码:
// CommandHandler
player.sendMessage(Util.getAndTranslate("audit-uuid" + firstId.toString()));
看见了吧?firstId.toString()
应该移动到括号外去。而且还忘了 msg.
!赶紧修改。改完后:
// CommandHandler 节选
player.sendMessage(Util.getAndTranslate("msg.audit-uuid") + firstId.toString());
// 这里!
player.sendMessage(Util.getAndTranslate("msg.audit-reason") + idm.getIForgotManualReason(firstId));
// 这里!
player.sendMessage(Util.getAndTranslate("msg.audit-hint"));
不光是这里,还有 EventHarmony
中的 onPlayerChat
监听器也要修改。
// EventHarmony 节选
player.sendMessage(Util.getAndTranslate("msg.audit-uuid") + nextId.toString());
// 这里!
player.sendMessage(Util.getAndTranslate("msg.audit-reason") + idm.getIForgotManualReason(nextId));
// 这里!
player.sendMessage(Util.getAndTranslate("msg.audit-hint"));
另外我还顺便检查了聊天处理器,结果又发现了问题。我们修改一部分代码:
// EventHarmony 节选
if (e.getMessage().startsWith("/")) {
if (!e.getMessage().startsWith("/iforgot")) {
return;
} else {
RuntimeDataManager.exitReadMode(id); // 这里!
e.getPlayer().sendMessage(Util.getAndTranslate("msg.audit-out"));
}
}
当 OP 输入 /iforgot
时退出审核模式。
还删掉了一部分:
// EventHarmony 节选
if (e.getMessage().toLowerCase().startsWith("y")) {
idm.setPasswordHash(jid, idm.getIForgotNewPasswordHash(jid));
idm.setIForgotState(jid, false);
idm.setIForgotManualReason(jid, "<Internal> Accepted.");
} else if (e.getMessage().toLowerCase().startsWith("n")) {
idm.setIForgotState(jid, false);
idm.setIForgotManualReason(jid, "<Internal> Rejected.");
} // else {
// RuntimeDataManager.exitReadMode(id);
// e.getPlayer().sendMessage(Util.getAndTranslate("msg.audit-out"));
// }
这样使得输入的不是 y
或 n
时将自动跳过,而不是退出。
重新构建,运行。
其实退出审核只是因为只有一个请求啦,我按的是 y
哦~
我们用原来的身份登录,看看密码有没有被修改……
(数据库中的数据是正常的哦)
很好!修改后的密码登录也很正常。
但我们发现了两个新问题:
- 玩家又开始回弹了
- 玩家仍旧可以输入命令,并使用
/say
聊天。
我们需要解决它们。
第一个问题实际上比较好解决。该错误位于处理玩家登录时设置的值。
// EventHarmony 节选
e.getPlayer().setWalkSpeed((float) 0.0001);
float
的精度可能没有那么高,因此应将 0.0001
改为 0.001
。
这个问题比较微妙。
玩家聊天是 AsyncPlayerChatEvent
,相当于「事后诸葛亮」,这时服务器已经进行了初步的处理,我们能做的只是阻止聊天信息的广播,而命令处理在这个事件之前已经完成了。
因此我们需要一个能够「及时汇报」的事件,这个事件就是 PlayerCommandPreprocessEvent
,在命令处理前触发。
于是我们修改 EventHarmony
,删除原来的有关命令处理,添加新的处理函数。
// EventHarmony 节选
@EventHandler
public void onPlayerCommand(PlayerCommandPreprocessEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
// 没登录
String msg = e.getMessage();
e.setCancelled(true);
// 先给你取消了
for (String a : allowCmd) {
if (msg.startsWith(a)) {
// 如果发现命令可以使用,你是冤枉的,那就再复原
e.setCancelled(false);
break;
}
}
}
}
构建,重新运行。
测试后证明命令确实生效了,但回弹问题没有解决!
事实上笔者一下就找到了问题所在。
我们会发现,玩家的登录是在玩家加入服务器之前的,也就是说,PlayerLoginEvent
发生时,玩家的实体还没有出现,因此我们应该将减速代码写在 PlayerJoinEvent
中。
既然不是 float
的问题,我们就提升一点精度吧。
// EventHarmony 节选
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) {
originSpeed.put(e.getPlayer().getUniqueId(), e.getPlayer().getWalkSpeed());
e.getPlayer().setWalkSpeed((float) 0.00001);
}
运行测试,这个问题终于解决了!
如果玩家没登录就退出,会导致移动速度卡在 0.00001
,这个数据保存在了存档里,以后玩家即使成功登录了,也无法正常移动,这可不行啊。
只要加两行就能解决这个问题了,写在 PlayerQuitEvent
中。
// EventHarmony 节选
e.getPlayer().setWalkSpeed(originSpeed.get(id));
originSpeed.remove(id);
然后还有一个 Bug,登录时似乎无法使用 /iforgot
和 /ifg
,经检查,我忘了把它写到 allowCmd
中了。
// EventHarmony 节选
public static final List<String> allowCmd = Arrays.asList("/hl", "/L", "/l", "/reg", "/register", "/login", "/log", "/iforgot", "/ifg");
这样就除掉了 Bug。
对了,在测试前一定要删除 world
,world_nether
和 world_end
啊。
我们的配置文件中有一处拼写错误:
hint-register: "请输入 /hl <密码> <再输入一次密码> 进行或注册!"
这显得很不专业,因此我们改过来。
?> 这种小事也要强调吗?
是的!往往用户拿到一个插件,如果配置文件写得清晰简洁,用户会觉得这个插件比较好,而如果配置文件有像上面这样的拼写错误,用户会觉得「这是什么垃圾」,会影响使用体验。
因此,为了方便服主设置颜色,我们把颜色表附在配置文件中……
# 颜色及样式表
# 使用 & 代替 § 指定样式
# &0 黑色
# &1 深蓝
# &2 深绿
# &3 湖蓝
# &4 深红
# &5 紫色
# &6 金色
# &7 灰色
# &8 深灰
# &9 蓝色
# &a 绿色
# &b 天蓝
# &c 红色
# &d 粉红
# &e 黄色
# &f 白色
# &k 乱码字符
# &l 加粗
# &m 划掉
# &n 下划线
# &o 斜体
# &r 全部重置
# 设置的样式能够应用到接着这些消息后的文字,例如 audit-uuid,请小心使用!
# 颜色代码必须在格式代码之前,并且修改颜色时必须再写一遍格式代码!!!
就这么多了。至此,所有的 Bug 都已经除掉了。有些项目没来得及测试,例如登录时取消已经创建的请求,重新创建请求时自动覆盖。当然,笔者事后测试成功了(查数据库可知)。至于钩子能否运行……既然配置空命令也没出错,我们就权且相信它可以正常运行吧——开玩笑的,笔者测试过没有问题的啦~
至于登录时按着 w
移动鼠标会回弹的现象……应该不会有人这么无聊吧?如果有,那回弹也是她自作自受(笑)。
密码中不能包含空格,设计如此。如果你的密码包含空格并且每一段都相同,那也不影响注册和登录。重设密码时,如果包含空格将无法通过。基本上没有大问题了。
本次行动的源代码:https://github.com/Andy-K-Sparklight/PluginDiaryCode/tree/master/HarmonyAuth%20SMART/src
战斗还是很艰难的,但我们做到了!
笔者写这两节写了两遍,并且好几次都有过放弃的念头,但最终坚持下来了。其实笔者能够在这里写出教程并非因为笔者技艺精湛或者经验丰富,只是我能够一直坚持。
这个插件已经能够使用了,笔者把它安装在了自己的服务器上,也算是一个纪念吧。
确认行动结束
行动结果:胜利
不容易啊,笔者写到这里花了一周,各位读者大概会比我要快一点吧?(毕竟笔者需要打字)
笔者认为,到目前为止,你已经掌握了插件开发需要的全部基础知识。接下来我们将学习一些高级知识和小技巧,并且由此开始通向最终的旅程。
回顾你编写的「Hello World」,记忆犹新?很好,那一章是「基础之基础」,而我们下一章的内容,叫做「终极之开端」。
最后感谢你的坚持,也感谢笔者自己。现在的你一定成就感满满吧?那我们来一首应景的歌~
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width="100%" height=86 src="//music.163.com/outchain/player?type=2&id=28077562&auto=0&height=66"></iframe>下一章,内容更精彩!