Skip to content

Latest commit

 

History

History
596 lines (408 loc) · 20.1 KB

AC-1-3.md

File metadata and controls

596 lines (408 loc) · 20.1 KB

AC-1-3 调试 HarmonyAuth SMART

前面几次演习中,调试似乎都很无聊,那是因为那些插件很简单,不容易出错。

但真正开发插件时几乎不可能一次写对。上面两节中的代码有很多漏洞,接下来请诸位跟随笔者,看看我是如何逐一发现并除掉这些 Bug 的。

安装插件

把构建出来的文件放到 plugins 目录下。这一次建议删除其它插件。

启动一次……

START 1

呃,好像爆掉了……

其实这个问题笔者心里已经有数了,我们在构建时貌似是直接将依赖打包进去的,这会导致整个 JAR 包作为一个文件放到我们的插件中, 这是不行的。

输入 stop 停止服务端,停止时似乎还出现了什么错误信息……不管了,我们先把 SQL 弄到位。

回到 IDEA 中,在「HarmonyAuth SMART」工件中,选择「mysql-connector-java-8.0.23」,按「-」移除它,接着在右侧「Available Elements」中找到它,右键,「Extract Into Output Root」将它解压到最终产品中。

REBUILD

「Apply」、「OK」,按下绿色锤子重新构建,再将插件放到 plugins 目录下。

再启动一次……

第二次启动

START 2

验证失败是自然的(MySQL 没有设置),但我们默认不是没有启用 MySQL 吗?

哦!对了!虽然配置文件中有这个选项,但我们没有处理这个设置,直接初始化了 DBDataManager,这里需要修改一下。

// HarmonyAuthSMART 节选
if (getConfig().getBoolean("mysql.enabled")) {
    new DBDataManager().loadAll();
}

再构建一次。再试试。

第三次启动

START 3

成功!

现在关闭服务器,我们把 MySQL 配置好。如果你的 test 数据库没有删除,那配置很快就完成了。否则,你需要创建新的数据库。

配置如下:

mysql:
  enabled: true
  host: localhost
  port: 3306
  username: root
  password: mylittlepony # 这是我的密码
  db-name: "test" # 数据库名

除了修改一下数据库以外,其它配置我们保持默认。

然后启动一下~

OK

无法保存 data.yml 是正常的,但为什么 Paper 要发一个警告呢……不管了,应该不要紧。

加入服务器

打开客户端,这里我用的是 1.16.5 无正版验证。进入多人游戏,输入 localhost 连接……

CLI 1

呃?为什么我可以移动?

检查代码,我很快就发现了问题:玩家登录时要加入限制列表啊!!!

关闭服务器,修改代码:

// 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());

然后重新构建,运行,加入……

EFFECTIVE

也不回弹了,很好!但是同时我们又发现了新错误:

(图略)

Operation not allowed for a result set of type ResultSet.TYPE_FORWARD_ONLY.

看来这是个数据库错误,笔者果然还是对 SQL 了解不深啊……

这就是在 executeQuery 中缺失了那两个参数的后果。

修复 SQLException 1

查阅了相关资料后,我发现 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 方法我们都要如此修改。

另外,还有 isExistgetNextRequest 要修改:

// 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

糟糕!我们在查询数据前就断开了连接!

修复 SQLException 2

这很简单,我们只需要把关闭挪到取值之前就可以了。由于 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)); // 这里!

删除数据表,清理脏数据,构建,再次运行。

MOVE

成功了,同时我们也看到,在登录成功的瞬间客户端有一个加速的动作,这和我们之前见到的其它登录插件很像。

继续测试,看看自动登录能否生效。

AUTORELOG

话说,如果不正常才麻烦呢,因为笔者实在找不到出错的地方了……

接下来测试一下不自动登录,将 auto-login 改为 0。重启服务器(这一次不要删数据库)。

LOGIN

系统识别出来了我们已经注册。并且没有自动登录。很好!

接下来模拟密码恢复。

IFORGOT

并且数据已经保存到了数据库(用 SELECT * FROM harmony_auth_data 可知)。

我们需要模拟 OP 进行测试……

退出客户端,换一个 ID 登录。

客户端的启动远没有服务端那么快。

我们给这个新 ID 以 OP 权限,登录,运行 /iforgot

FAILED

还是不行?

修改 SQLException 3

根据指出的错误行数,我们找到了 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"));
// }

这样使得输入的不是 yn 时将自动跳过,而不是退出。

重新构建,运行。

OK-AUDIT

其实退出审核只是因为只有一个请求啦,我按的是 y 哦~

我们用原来的身份登录,看看密码有没有被修改……

(数据库中的数据是正常的哦)

OK-RESET

很好!修改后的密码登录也很正常。

但我们发现了两个新问题:

  • 玩家又开始回弹了
  • 玩家仍旧可以输入命令,并使用 /say 聊天。

我们需要解决它们。

修复回弹 1

第一个问题实际上比较好解决。该错误位于处理玩家登录时设置的值。

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

构建,重新运行。

测试后证明命令确实生效了,但回弹问题没有解决

修复回弹 2

事实上笔者一下就找到了问题所在。

我们会发现,玩家的登录是在玩家加入服务器之前的,也就是说,PlayerLoginEvent 发生时,玩家的实体还没有出现,因此我们应该将减速代码写在 PlayerJoinEvent 中。

既然不是 float 的问题,我们就提升一点精度吧。

// EventHarmony 节选
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) {
    originSpeed.put(e.getPlayer().getUniqueId(), e.getPlayer().getWalkSpeed());
    e.getPlayer().setWalkSpeed((float) 0.00001);
}

运行测试,这个问题终于解决了!

还有两个 Bug

如果玩家没登录就退出,会导致移动速度卡在 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。

对了,在测试前一定要删除 worldworld_netherworld_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>

下一章,内容更精彩!