diff --git a/src/main/java/Sdf1_login/ChatFilterManager.java b/src/main/java/Sdf1_login/ChatFilterManager.java index 120b4db..c6f61c0 100644 --- a/src/main/java/Sdf1_login/ChatFilterManager.java +++ b/src/main/java/Sdf1_login/ChatFilterManager.java @@ -109,7 +109,7 @@ public class ChatFilterManager { static class VerificationData { long createTime; - int a, b, op; // 0=加法, 1=减法, 2=乘法(九九表), 3=除法(九九表) + int a, b, op; // 0=加法, 1=减法, 2=乘法, 3=除法(均为99以内小学运算) String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔) VerificationType type; // 验证码类型 boolean completed; @@ -188,7 +188,7 @@ public class ChatFilterManager { /** * 生成随机验证码:加 / 减 / 乘 / 除 / GUI 五种每次随机抽一种。 - * 乘法与除法严格取自九九乘法表。 + * ★ 算术题统一为小学水平「99以内加减乘除」(和/积≤99、减法不为负、除法整除)。 * * ★ 出题前必须先关掉可能还开着的旧验证码GUI:旧GUI 的 InventoryCloseEvent * 会清 verificationData,让它先跑完,否则刚生成的新题会被清掉(点哪儿都没用)。 @@ -237,26 +237,30 @@ public class ChatFilterManager { } } - /** 生成数学题验证码(op: 0加 1减 2乘 3除;乘/除每次从九九乘法表随机抽一题) */ + /** + * 生成数学题验证码(op: 0加 1减 2乘 3除) + * ★ 小学水平「99以内加减乘除」:加法和≤99、减法不出现负数、乘法积≤99、除法整除且被除数≤99 + */ private void generateMathChallenge(String playerName, int op, Random rand) { int a, b; switch (op) { - case 0: // 两位数加法 - a = 10 + rand.nextInt(90); - b = 10 + rand.nextInt(90); + case 0: // 加法:两数之和不超过99 + a = 1 + rand.nextInt(98); // a ∈ [1, 98] + b = 1 + rand.nextInt(99 - a); // b ∈ [1, 99-a] → a+b ≤ 99 break; - case 1: // 两位数减法(保证结果仍是两位数,不出现负数) - a = 20 + rand.nextInt(80); - b = 10 + rand.nextInt(a - 19); // b ∈ [10, a-10],结果 ≥ 10 + case 1: // 减法:被减数不超过99,结果≥1(小学不出现负数/零) + a = 2 + rand.nextInt(98); // a ∈ [2, 99] + b = 1 + rand.nextInt(a - 1); // b ∈ [1, a-1] → 结果 ∈ [1, a-1] break; - case 2: // 九九乘法表:1×1 ~ 9×9 - a = 1 + rand.nextInt(9); - b = 1 + rand.nextInt(9); + case 2: // 乘法:积不超过99(如 12×7=84、11×9=99) + b = 2 + rand.nextInt(8); // b ∈ [2, 9] + a = 2 + rand.nextInt((99 / b) - 1); // a ∈ [2, floor(99/b)] → a*b ≤ 99 break; - default: // 九九乘法表除法(整除):(i×j) ÷ j = i - b = 1 + rand.nextInt(9); - a = (1 + rand.nextInt(9)) * b; + default: // 除法:整除且被除数不超过99(如 96÷8=12、99÷9=11) + b = 2 + rand.nextInt(8); // b ∈ [2, 9] + int q = 2 + rand.nextInt((99 / b) - 1); // 商 ∈ [2, floor(99/b)] + a = b * q; // a ≤ 99 break; } @@ -484,7 +488,7 @@ public class ChatFilterManager { case 0 -> expected = vd.a + vd.b; case 1 -> expected = vd.a - vd.b; case 2 -> expected = vd.a * vd.b; - default -> expected = vd.a / vd.b; // 九九表整除 + default -> expected = vd.a / vd.b; // 生成时保证整除(a % b == 0) } if (ans == expected) { diff --git a/src/main/java/Sdf1_login/PVPArenaManager.java b/src/main/java/Sdf1_login/PVPArenaManager.java index 7f6c2d4..9f26f2a 100644 --- a/src/main/java/Sdf1_login/PVPArenaManager.java +++ b/src/main/java/Sdf1_login/PVPArenaManager.java @@ -1235,7 +1235,10 @@ public class PVPArenaManager implements Listener { GameMode backMode = spectatorBackups.remove(playerName); if (backMode != null && !isDisconnect && player.isOnline() && player.getGameMode() == GameMode.SPECTATOR) { - player.setGameMode(backMode == GameMode.SPECTATOR ? GameMode.SURVIVAL : backMode); + // ★ 兜底:还原目标为观察者或创造时一律回生存(退出观赛绝不回创造,防止作弊) + GameMode restore = (backMode == GameMode.SPECTATOR + || backMode == GameMode.CREATIVE) ? GameMode.SURVIVAL : backMode; + player.setGameMode(restore); } // 清理所有状态(含取消超时定时器) @@ -2850,7 +2853,12 @@ public class PVPArenaManager implements Listener { // 2) 记录原模式并进入 MC 原版观察者 // ★ 必须【先记再切】:观察者模式下 addItem 会掉地上,故不再发放任何物品 - spectatorBackups.put(p.getName(), p.getGameMode()); + // ★ 绝不把【创造模式】当作还原目标:以创造进入观赛的,退出一律回生存(防止退出观赛变成创造作弊) + GameMode before = p.getGameMode(); + if (before == GameMode.CREATIVE || before == GameMode.SPECTATOR) { + before = GameMode.SURVIVAL; + } + spectatorBackups.put(p.getName(), before); p.setGameMode(GameMode.SPECTATOR); p.sendMessage("§a§l[PVP观赛] §f已进入观察者模式,可穿墙自由观赛"); @@ -2866,7 +2874,10 @@ public class PVPArenaManager implements Listener { if (p.getGameMode() != GameMode.SPECTATOR) return; GameMode back = spectatorBackups.remove(p.getName()); - if (back == null || back == GameMode.SPECTATOR) back = GameMode.SURVIVAL; + // ★ 兜底:备份缺失/观察者/创造 一律还原为生存(退出观赛绝不回创造,防止作弊) + if (back == null || back == GameMode.SPECTATOR || back == GameMode.CREATIVE) { + back = GameMode.SURVIVAL; + } // 观察者常悬在高空或钻进方块里 → 切回可玩模式前先挪到可站立的空气格 safeRecoverPosition(p); diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index f29fdd4..44a33d3 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -5215,6 +5215,8 @@ public class WebManager { } final String[] pollData = {sessionId}; + // ★ 连续无效响应(非JSON/结构不符)计数:只用于日志与提示,任何情况下都不会据此放行 + final int[] invalidPolls = {0}; final long startTime = System.currentTimeMillis(); final long maxPollTime = 660000; // 11分钟(比会话过期多1分钟) @@ -5262,16 +5264,69 @@ public class WebManager { String response = doGet(url); if (response == null) return; - com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(response).getAsJsonObject(); - if (!json.get("success").getAsBoolean()) return; - + // ★ fail-closed:非JSON / 结构不符 一律视为"无效响应",绝不判为通过 + com.google.gson.JsonObject json; + try { + json = com.google.gson.JsonParser.parseString(response).getAsJsonObject(); + } catch (Exception parseEx) { + invalidPolls[0]++; + if (invalidPolls[0] == 1 || invalidPolls[0] % 15 == 0) { + String preview = response.length() > 200 ? response.substring(0, 200) + "..." : response; + plugin.getLogger().warning("[正版验证] 响应非JSON,忽略(第" + invalidPolls[0] + "次): " + preview); + } + if (invalidPolls[0] == 5) { + Bukkit.getScheduler().runTask(plugin, () -> { + if (player.isOnline()) { + player.sendMessage("§c验证服务响应异常,正在自动重试…"); + } + }); + } + return; + } + if (!json.has("success") || !json.get("success").isJsonPrimitive() + || !json.get("success").getAsBoolean()) { + // PHP明确返回失败(会话不存在/密钥错误等结构性错误)→ 终止轮询,不放行 + String err = (json.has("error") && json.get("error").isJsonPrimitive()) + ? json.get("error").getAsString() : "未知错误"; + plugin.getLogger().warning("[正版验证] PHP返回失败,终止轮询: " + err); + minecraftAuthSessions.remove(playerName); + minecraftAuthPollers.remove(playerName); + Bukkit.getScheduler().runTask(plugin, () -> { + if (player.isOnline()) { + player.sendMessage("§c验证失败: " + err); + } + }); + this.cancel(); + return; + } + if (!json.has("data") || !json.get("data").isJsonObject()) { + invalidPolls[0]++; + plugin.getLogger().warning("[正版验证] 响应缺少data,忽略(第" + invalidPolls[0] + "次)"); + return; + } com.google.gson.JsonObject data = json.getAsJsonObject("data"); + if (!data.has("status") || !data.get("status").isJsonPrimitive()) { + invalidPolls[0]++; + plugin.getLogger().warning("[正版验证] 响应缺少status,忽略(第" + invalidPolls[0] + "次)"); + return; + } String status = data.get("status").getAsString(); + invalidPolls[0] = 0; // 收到有效响应 → 重置无效计数 if ("verified".equals(status)) { // 验证成功! - String mcUuid = data.get("mc_uuid").getAsString(); - String mcUsername = data.get("mc_username").getAsString(); + String mcUuid = data.has("mc_uuid") ? data.get("mc_uuid").getAsString() : ""; + String mcUsername = data.has("mc_username") ? data.get("mc_username").getAsString() : ""; + String sessionPlayer = data.has("player_name") ? data.get("player_name").getAsString() : ""; + + // ★ fail-closed:凭证不全 / 会话归属玩家不符 → 一律不放行(继续轮询直到超时) + if (mcUuid.isEmpty() || mcUsername.isEmpty() + || (sessionPlayer != null && !sessionPlayer.isEmpty() + && !sessionPlayer.equalsIgnoreCase(playerName))) { + plugin.getLogger().warning("[正版验证] verified但校验不通过,拒绝放行: session.player=" + + sessionPlayer + ", uuid=" + mcUuid + ", name=" + mcUsername); + return; + } plugin.getLogger().info("[正版验证] 验证成功: " + playerName + " -> " + mcUsername + " (" + mcUuid + ")");