我们接着上一节,上一节中我们已经完成了命令处理,并将数据保存在了 RuntimeDataManager
中。
接下来我们就应该监听事件啦~
我给我们的事件监听器起了个新名字:EventHarmony
。
首先我们完成限制移动。
// EventHarmony 节选
public static Map<UUID, Float> originSpeed = new HashMap<>(); // 保存初始速度
@EventHandler
public void antiMove(PlayerMoveEvent e) {
UUID id = e.getPlayer().getUniqueId();
if (RuntimeDataManager.hasRestrictUUID(id)) {
// 限制列表
float oSpeed = e.getPlayer().getWalkSpeed();
if (oSpeed > 0.0001) {
originSpeed.put(id, oSpeed);
e.getPlayer().setWalkSpeed((float) 0.0001);
// 把速度设极小防止回弹
}
if (e.getFrom().distance(e.getTo()) != 0) {
e.setCancelled(true);
// 允许玩家转动视角
}
} else {
if (originSpeed.containsKey(id)) {
e.getPlayer().setWalkSpeed(originSpeed.get(id));
originSpeed.remove(id);
// 速度要复原
}
}
}
很简单,和之前基本一样。
这些在登录前应该都禁止,直接取消掉就好了。
// EventHarmony 节选
@EventHandler
public void onPlayerInteract(PlayerInteractEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
@EventHandler
public void noHurt(EntityDamageEvent e) {
// 这是挨打!不是打人!
if (e.getEntity() instanceof Player) {
if (RuntimeDataManager.hasRestrictUUID(e.getEntity().getUniqueId())) {
e.setCancelled(true);
e.getEntity().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
}
@EventHandler
public void noInteract(PlayerInteractEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
@EventHandler
public void noBreak(BlockBreakEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
@EventHandler
public void noInventory(InventoryOpenEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
@EventHandler
public void noTeleport(PlayerTeleportEvent e) {
if (RuntimeDataManager.hasRestrictUUID(e.getPlayer().getUniqueId())) {
e.setCancelled(true);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint"));
}
}
基本上很简单,没有什么需要说明的。
聊天的处理遵循这个逻辑:
-
在未登录模式下:
- 若不以
/hl
,/L
,/l
,/reg
,/register
,/login
,/log
,/iforgot
,/ifg
开头,则直接取消。 - 否则,放行。
- 若不以
-
在已登录模式下:
- 放行。
-
在密码恢复模式 1(输入新密码)下:
- 取消。如果不含空格,不为空且不以
/
开头,将新密码的哈希存入数据。否则重新提示一次。 - 玩家如果已登录,退出密码恢复模式并应用新密码。
- 否则,进入恢复模式 2 并提示。
- 取消。如果不含空格,不为空且不以
-
在密码恢复模式 2(输入理由)下:
- 取消。如果不为空,将该数据保存并提示一次,随后将该玩家的
IForgotState
置为true
,退出恢复模式。否则重新提示输入。
- 取消。如果不为空,将该数据保存并提示一次,随后将该玩家的
-
在审核模式(仅 OP)下:
- 若不以
/
开头,取消。
- 若不以
-
如果是表示「通过」,将该玩家的
IForgotState
置为false
,应用新密码,并将原因设为<Internal> Accepted.
。随后从数据库载入下一条。如果没有则退出。- 如果是表示「拒绝」,同样将
IForgotState
置为false
,原因设为<Internal> Rejected.
。随后从数据库载入下一条。如果没有则退出。 - 如果是表示「退出」,将 OP 移出审核模式。
- 如果是表示「拒绝」,同样将
这里没有涉及命令处理的逻辑,那些已经完成了。
为了从数据库查询下一条,我们需要再次修改 FileDataManager
和 DBDataManager
,以及 IDataManager
接口,我们先完成修改:
// IDataManager 节选
UUID getNextRequest();
// FileDataManager 节选
@Override
public UUID getNextRequest() {
Set<String> keys = Objects.requireNonNull(data.getConfigurationSection("iforgot-states")).getKeys(false);
for (String s : keys) {
if (data.getBoolean("iforgot-states." + s)) {
return UUID.fromString(s);
}
}
return UUID.fromString("00000000-0000-0000-0000-000000000000");
}
// DBDataManager 节选
@Override
public UUID getNextRequest() {
try {
Connection connection = DriverManager.getConnection(db_url, username, password);
Statement statement = connection.createStatement();
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");
}
}
COUNT(列名)
是一个 SQL 函数,它表示「对应列的元素个数」。实际上我们只是要数一数有多少条恢复请求正在等待中,因此 COUNT
了 UUID
,实际上 COUNT
别的也是一样的。(COUNT
主键会快一点)
为了记录 OP 上次操作的是哪一个 UUID,我们设置了一个类变量:
// EventHarmony 节选
public static Map<UUID, UUID> lastJudgeUUID = new HashMap<>();
Map<K, V>
同样是个模板类,由于 Map
是「A to B」的结构,因此需要两个类型进行模板化,这里我们是从 UUID
映射到 UUID
。
哦对了,我们还要修改上次的命令处理,将上次处理的 UUID 放进去:
// CommandHandler 节选
RuntimeDataManager.toReadMode(id);
player.sendMessage(Util.getAndTranslate("msg.audit-in"));
UUID firstId = idm.getNextRequest();
EventHarmony.lastJudgeUUID.put(player.getUniqueId(), firstId);
// 这里!
if (firstId.equals(UUID.fromString("00000000-0000-0000-0000-000000000000"))) {
RuntimeDataManager.exitReadMode(id);
player.sendMessage(Util.getAndTranslate("msg.audit-out"));
} else {
player.sendMessage(Util.getAndTranslate("audit-uuid" + firstId.toString()));
player.sendMessage(Util.getAndTranslate("audit-reason" + idm.getIForgotManualReason(firstId)));
player.sendMessage(Util.getAndTranslate("audit-hint"));
}
然后实现这个方法:
// EventHarmony 节选
public static final List<String> allowCmd = Arrays.asList("/hl", "/L", "/l", "/reg", "/register", "/login", "/log");
@EventHandler
public void onPlayerChat(AsyncPlayerChatEvent e) {
UUID id = e.getPlayer().getUniqueId();
if (RuntimeDataManager.getIForgotMode(id) != 0) {
// 恢复模式
switch (RuntimeDataManager.getIForgotMode(id)) {
case 1: // 等待输入新密码
e.setCancelled(true);
if (e.getMessage().split(" ").length != 1 || e.getMessage().startsWith("/")) {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-newpwd"));
return;
}
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
if (!RuntimeDataManager.hasRestrictUUID(id)) {
// 已经登录,修改密码
idm.setPasswordHash(id, idm.getIForgotNewPasswordHash(id));
RuntimeDataManager.exitIForgotMode(id);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-accepted"));
return;
}
idm.setIForgotNewPasswordHash(id, Util.calculateMD5(e.getMessage()));
RuntimeDataManager.toIForgotMode(id, 2);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-hint"));
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
break;
case 2:
// 等待输入原因
e.setCancelled(true);
if (e.getMessage().equals("")) {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-hint"));
return;
}
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
idm.setIForgotManualReason(id, e.getMessage());
idm.setIForgotState(id, true);
RuntimeDataManager.exitIForgotMode(id);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-commit"));
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
break;
}
} else {
// 非恢复模式
if (RuntimeDataManager.isInReadMode(id)) {
// 审核模式
if (e.getMessage().startsWith("/")) {
if (!e.getMessage().startsWith("/iforgot")) {
return;
} // 万一 OP 急需使用一个命令呢?
}
e.setCancelled(true);
if (e.getMessage().toLowerCase().startsWith("q")) {
RuntimeDataManager.exitReadMode(id);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.audit-out"));
} else {
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
UUID jid = lastJudgeUUID.get(e.getPlayer().getUniqueId());
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"));
return;
}
Player player = e.getPlayer();
UUID nextId = idm.getNextRequest();
lastJudgeUUID.put(player.getUniqueId(), nextId);
// 发送下一条
if (nextId.equals(UUID.fromString("00000000-0000-0000-0000-000000000000"))) {
RuntimeDataManager.exitReadMode(id);
player.sendMessage(Util.getAndTranslate("msg.audit-out"));
} else {
player.sendMessage(Util.getAndTranslate("audit-uuid" + nextId.toString()));
player.sendMessage(Util.getAndTranslate("audit-reason" + idm.getIForgotManualReason(nextId)));
player.sendMessage(Util.getAndTranslate("audit-hint"));
}
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
}
} else {
// 常规模式
if (RuntimeDataManager.hasRestrictUUID(id)) {
// 未登录时看看是不是允许的命令
e.setCancelled(true);
for (String a : allowCmd) {
if (e.getMessage().startsWith(a)) {
e.setCancelled(false);
break;
}
}
}
// 已登录不做处理
}
}
}
很长,但是不难,相信你应该看得懂~
这里我们没有使用 cli
和 sti
,因为大部分操作都在进入异步前做完了,即使玩家重复发送命令也不要紧。
玩家进入服务器时,如果密码恢复请求被处理了,应该告知玩家。
如果最近一次登录在允许的时间内,则自动登录。否则,发送登录/注册提示。
// EventHarmony 节选
@EventHandler
public void onPlayerLogin(PlayerLoginEvent e) {
UUID id = e.getPlayer().getUniqueId();
new BukkitRunnable() {
@Override
public void run() {
Date crDate = new Date();
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
if (idm.isExist(id)) {
Date lLogin = idm.getLastLoginTime(id);
double seconds = (crDate.getTime() - lLogin.getTime()) / 1000.0;
// 转换为秒
if (seconds <= HarmonyAuthSMART.instance.getConfig().getInt("auto-login")) {
RuntimeDataManager.removeRestrictUUID(id);
e.getPlayer().sendMessage(Util.getAndTranslate("msg.login-success"));
List<String> hooks = Util.generateHooks("hook.on-login-success", e.getPlayer().getName());
for (String cmd : hooks) {
Util.dispatchCommandAsServer(cmd);
}
} else {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint-login"));
String ifState = idm.getIForgotManualReason(id);
if (ifState.equals("<Internal> Accepted.")) {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-accepted"));
} else if (ifState.equals("<Internal> Rejected.")) {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.iforgot-rejected"));
}
}
} else {
e.getPlayer().sendMessage(Util.getAndTranslate("msg.hint-register"));
}
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
}
如果不进行额外的设定,Date
对象就代表它被创建时的时间,也就是当前时间。
只剩一点点了,在玩家离开时更新最后登录时间:
// EventHarmony 节选
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
new BukkitRunnable() {
@Override
public void run() {
IDataManager idm;
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
idm.setLastLoginTime(e.getPlayer().getUniqueId(), new Date());
}
}.runTaskAsynchronously(HarmonyAuthSMART.instance);
}
这样就完成了。
这里基本上就是启动时全部加载,关闭时全部保存,修改主类:
package rarityeg.harmonyauthsmart;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.logging.Level;
public class HarmonyAuthSMART extends JavaPlugin {
public static JavaPlugin instance;
public static boolean dbError = false;
@Override
public void onEnable() {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
getLogger().log(Level.WARNING, "数据库驱动加载失败,将使用备用存储方法。");
e.printStackTrace();
dbError = true;
}
saveDefaultConfig();
saveResource("data.yml", false);
instance = this;
new DBDataManager().loadAll();
new FileDataManager().loadAll();
}
@Override
public void onDisable() {
new DBDataManager().saveAll();
new FileDataManager().saveAll();
}
}
创建工件,这一次除了「'HarmonyAuth SMART compile output'」以外,还要添加「'mysql-connector-java-8.0.23'」,因为 Bukkit 本身不提供该驱动。
记得勾选「Include in project build」,「Apply」、「OK」。
回到代码页面,按下那个我们已经按过多次的按钮(构建)~
结束了……?