diff --git a/src/main/java/Sdf1_login/BlacklistManager.java b/src/main/java/Sdf1_login/BlacklistManager.java index 0afecfb..9891382 100644 --- a/src/main/java/Sdf1_login/BlacklistManager.java +++ b/src/main/java/Sdf1_login/BlacklistManager.java @@ -318,16 +318,26 @@ public class BlacklistManager { } // ===== 差异应用 ===== + boolean changed = parsed.size() != entries.size(); // 1) 新增 → 施加处罚 for (BlackEntry e : parsed.values()) { if (!entries.containsKey(e.name)) { applyBlacklist(e); + changed = true; + } else { + BlackEntry old = entries.get(e.name); + if (old == null || old.expiresMs != e.expiresMs + || !old.timeText.equals(e.timeText) + || !old.reason.equals(e.reason)) { + changed = true; // 条目内容改了(理由/时长) + } } } // 2) 移除 → 恢复 for (BlackEntry e : new ArrayList<>(entries.values())) { if (!parsed.containsKey(e.name)) { restoreBlacklist(e.name); + changed = true; } } entries.clear(); @@ -338,6 +348,13 @@ public class BlacklistManager { lastSize = mdFile.length(); } plugin.getLogger().info("[黑名单] 已重载,当前生效 " + parsed.size() + " 人"); + // ★ 第32批(#208):名单变化 → 立即回响PHP(异步;syncBlacklist 内部 hash 门控兜底) + if (changed) { + try { + WebManager wm = plugin.webManager; + if (wm != null) wm.forceSyncBlacklist(); + } catch (Exception ignored) {} + } } /** diff --git a/src/main/java/Sdf1_login/CDKManager.java b/src/main/java/Sdf1_login/CDKManager.java index aca378c..2ca140e 100644 --- a/src/main/java/Sdf1_login/CDKManager.java +++ b/src/main/java/Sdf1_login/CDKManager.java @@ -57,6 +57,12 @@ public class CDKManager { */ public String redeem(String code, String player) { try { + // ★ 第32批:拉黑检查提到查码之前 —— 被拉黑玩家一律拒绝, + // 不管码在本地是否存在,绝不放行进远程验证链 + if (plugin.getBlacklist() != null + && plugin.getBlacklist().isBlacklisted(player)) + return "fail:blacklisted"; + PreparedStatement ps = db.prepareStatement( "SELECT amount,type,used FROM cdk " + "WHERE code=?"); @@ -74,11 +80,6 @@ public class CDKManager { if (used == 1) return "fail:already_used"; - // ★ 第30批:拉黑玩家禁止使用兑换码 - if (plugin.getBlacklist() != null - && plugin.getBlacklist().isBlacklisted(player)) - return "fail:blacklisted"; - if (plugin.getBonds() .isFrozen(player)) return "fail:frozen"; diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index 9bd22db..ee4b5dc 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -58,7 +58,8 @@ public class ConfigManager { public int garbageMaxRounds = 1; - /** ★ 第30批:垃圾站启发式拦截(双击Q抢救窗口 + 倒计时段位提示)。默认开启。 */ + /** ★ 第30批:垃圾站启发式拦截 —— 仅控制「红区title警告 + 双击Q抢救窗口」。 + * ★ 第32批:与顶部血条解耦,关闭时血条照常显示。默认开启。 */ public boolean garbageHeuristic = true; @@ -454,7 +455,7 @@ public class ConfigManager { garbageHeuristic = Boolean.parseBoolean(m.getOrDefault("垃圾站_启发式拦截", "true")); if (!garbageHeuristic) { - Bukkit.getLogger().warning("[Sdf1配置] 垃圾站启发式拦截已关闭:不再做倒计时段位提示与双击Q抢救拦截。"); + Bukkit.getLogger().warning("[Sdf1配置] 垃圾站启发式拦截已关闭:不再做红区title警告与双击Q抢救拦截(顶部血条照常显示)。"); } diff --git a/src/main/java/Sdf1_login/GarbageManager.java b/src/main/java/Sdf1_login/GarbageManager.java index 572e458..94c4f22 100644 --- a/src/main/java/Sdf1_login/GarbageManager.java +++ b/src/main/java/Sdf1_login/GarbageManager.java @@ -36,19 +36,27 @@ public class GarbageManager implements Listener { private final Map recyclePages = new HashMap<>(); - /** ★ 第30批:垃圾站倒计时(秒)。原为 startAutoCleanup 内部局部变量, - * 抢救模式需要外部重置,故提为字段。 */ + /** + * ★ 第30批:垃圾站倒计时(秒)。原为 startAutoCleanup 内部局部变量, + * 抢救模式需要外部重置,故提为字段。 + */ private int countdown = 0; - /** ★ 第31批:本轮开始的墙钟时间戳(毫秒)。 - * 血条色段按真实时间判定,与 tick 计数解耦, - * 避免“第3分30秒还是绿色”的漂移。0 = 本轮未开始。 */ + /** + * ★ 第31批:本轮开始的墙钟时间戳(毫秒)。 + * 血条色段按真实时间判定,与 tick 计数解耦, + * 避免“第3分30秒还是绿色”的漂移。0 = 本轮未开始。 + */ private long roundStartMs = 0L; - /** ★ 第30批:顶部血条(玩家 UUID → BossBar) */ + /** + * ★ 第30批:顶部血条(玩家 UUID → BossBar) + */ private final Map bossBars = new HashMap<>(); - /** ★ 第30批:双击 Q 确认丢弃的首次按键时间(玩家 UUID → 毫秒) */ + /** + * ★ 第30批:双击 Q 确认丢弃的首次按键时间(玩家 UUID → 毫秒) + */ private final Map pendingDrops = new HashMap<>(); @@ -174,9 +182,6 @@ public class GarbageManager implements Listener { cleanupInterval = plugin.getConfig2() .garbageInterval; - boolean heuristic = - plugin.getConfig2() - .garbageHeuristic; int remain = cleanupInterval - countdown; @@ -211,7 +216,8 @@ public class GarbageManager implements Listener { countdown++; // ★ 第30批:顶部血条常驻倒计时(软提醒) // 用 increment 前的 remain,与广播口径完全一致(避免差 1 秒) - updateBossBars(remain, heuristic); + // ★ 第32批:血条只受 垃圾站_启用 控制,与启发式开关解耦 + updateBossBars(remain); } }.runTaskTimer(plugin, 20L, 20L); } @@ -227,13 +233,11 @@ public class GarbageManager implements Listener { *
  • 4:30 ~ 5:00 → 红 = 危险(另:剩余≤30秒兜底红)
  • * * - *

    启发式开关(垃圾站_启发式拦截)关闭时:撤掉血条、不提示、不拦截。 + *

    ★ 第32批:血条与启发式开关解耦 —— 只要垃圾站启用,血条常驻显示; + * 启发式开关(垃圾站_启发式拦截)关闭时只关掉「红区title警告 + 双击Q拦截」, + * 不再撤血条(否则玩家对清理状态一无所知)。 */ - private void updateBossBars(int remain, boolean heuristic) { - if (!heuristic) { - hideAllBossBars(); - return; - } + private void updateBossBars(int remain) { int interval = Math.max(1, cleanupInterval); double progress = (double) remain / (double) interval; if (progress < 0.0D) progress = 0.0D; @@ -283,7 +287,7 @@ public class GarbageManager implements Listener { } } - /** 撤掉所有玩家的顶部血条(启发式关闭时用) */ + /** 撤掉所有玩家的顶部血条(插件关闭/停服时用;启发式开关不再触发) */ private void hideAllBossBars() { for (Player p : Bukkit.getOnlinePlayers()) { BossBar bar = bossBars.get(p.getUniqueId()); @@ -390,8 +394,8 @@ public class GarbageManager implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent e) { - if (!plugin.getConfig2().garbageHeuristic) return; - // 下一轮 tick 会自动建血条,这里只清掉可能残留的按键状态 + // 下一轮 tick 会自动建血条(第32批:不再受启发式开关守卫), + // 这里只清掉可能残留的按键状态 pendingDrops.remove(e.getPlayer().getUniqueId()); } diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index 52226cf..beb3b06 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -331,6 +331,7 @@ public class WebManager { private long lastPushedCutoff = -1; // 上次推送时的播报起点(变了就再强制推一轮) private String lastAdminsHash = ""; private String lastUserRegistrationHash = ""; + private String lastBlacklistHash = ""; // ★ 第32批:拉黑名单同步快照 // 嵌入式HTTP服务器(接收PHP回调) private java.net.ServerSocket callbackServer; @@ -3338,7 +3339,14 @@ public class WebManager { // ★ 直接检查sdf1计分板CDK(bond.db只是钱包,不存CDK) String status = "not_found"; int amount = 0; - try { + // ★ 第32批:被拉黑玩家的兑换请求 → 直接回 blacklisted, + // 不进验证链、不加债券(PHP cdk.php 据此明确提示「你已被拉黑」) + if (plugin.getBlacklist() != null + && plugin.getBlacklist().isBlacklisted(playerName)) { + status = "blacklisted"; + plugin.getLogger().info("[CDK远程验证] " + cdkCode + + " 拉黑拦截 player=" + playerName); + } else try { Object sdf1Plugin = Bukkit.getPluginManager().getPlugin("sdf1"); if (sdf1Plugin != null) { plugin.getLogger().info("[CDK远程验证] 调用sdf1检查计分板: " + cdkCode); @@ -3443,7 +3451,13 @@ public class WebManager { String status = "not_found"; int amount = 0; boolean transientFail = false; // 瞬时故障 -> 可重试 - try { + // ★ 第32批:被拉黑玩家 → 直接回 blacklisted,不发起远程验证 + if (plugin.getBlacklist() != null + && plugin.getBlacklist().isBlacklisted(playerName)) { + status = "blacklisted"; + plugin.getLogger().info("[CDK-Web验证] " + cdkCode + + " 拉黑拦截 player=" + playerName); + } else try { String validateUrl = webBaseUrl + "/api/sync.php?action=check_cdk_exists&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8") + "&code=" + java.net.URLEncoder.encode(cdkCode, "UTF-8"); @@ -5292,6 +5306,8 @@ public class WebManager { // ★ 封禁名单和管理员列表独立于领地hash变化,必须每次都同步 syncBans(); syncAdmins(); + // ★ 第32批:拉黑名单同样每轮兜底(PHP重启丢数据时自愈) + syncBlacklist(); // ★ 双向对账(多退少补):PHP 数据齐全就不推 —— 消除「重启必整包推」 // (lastLandDataHash 是内存字段,重启清零,旧逻辑每次开服必整包一次)。 @@ -5561,6 +5577,62 @@ public class WebManager { syncAdmins(); } + // ★ 第32批(#208):拉黑名单同步到PHP(web_player_blacklist), + // 供 PHP 后台识别被拉黑玩家并给出明确提示(登录/兑换码双链路)。 + + /** + * 同步拉黑名单到PHP。参照 syncBans 模板: + * hash 门控(无变化静默)→ POST → 回执 success 才提交 hash(防 404 写脏)。 + * PHP 侧 sync_blacklist 做全量覆盖,无需对账。 + */ + private void syncBlacklist() { + if (!enabled) return; + try { + BlacklistManager blm = plugin.getBlacklist(); + java.util.List list = + blm != null ? blm.getActiveEntries() : null; + StringBuilder sb = new StringBuilder("["); + StringBuilder hb = new StringBuilder(); + boolean first = true; + if (list != null) { + for (BlacklistManager.BlackEntry e : list) { + if (!first) sb.append(","); + first = false; + sb.append("{\"name\":\"").append(escapeJson(e.name)) + .append("\",\"time\":\"").append(escapeJson(e.timeText)) + .append("\",\"reason\":\"").append(escapeJson(e.reason)) + .append("\",\"expire\":").append(e.expiresMs) + .append(",\"created\":").append(e.createdMs).append("}"); + hb.append(e.name).append(":").append(e.expiresMs) + .append(":").append(e.reason).append("|"); + } + } + sb.append("]"); + String currentHash = hb.toString(); + if (currentHash.equals(lastBlacklistHash)) return; // 无变化静默 + + Map body = new LinkedHashMap<>(); + body.put("secret", secretKey); + body.put("blacks", sb.toString()); + String resp = httpPost("api/sync.php?action=sync_blacklist", mapToJson(body)); + if (resp != null && resp.contains("\"success\":true")) { + lastBlacklistHash = currentHash; + plugin.getLogger().info("[防护-sync] 拉黑名单同步: " + + (list == null ? 0 : list.size()) + "人 → " + resp); + } else { + plugin.getLogger().warning("[防护-sync] 拉黑名单同步未确认成功,下轮重试(hash 不提交)"); + } + } catch (Exception e) { + plugin.getLogger().warning("[防护-sync] 拉黑名单同步异常: " + e.getMessage()); + } + } + + /** ★ 第32批:拉黑/解除时立即异步同步(不等定时轮询;内部 hash 门控兜底) */ + public void forceSyncBlacklist() { + if (!enabled) return; + Bukkit.getScheduler().runTaskAsynchronously(plugin, this::syncBlacklist); + } + /** * ★ 同步封禁名单到PHP(供用户管理页面显示封禁状态) */