diff --git a/src/main/java/Sdf1_login/BanBroadcastManager.java b/src/main/java/Sdf1_login/BanBroadcastManager.java index 35382cc..32e52d5 100644 --- a/src/main/java/Sdf1_login/BanBroadcastManager.java +++ b/src/main/java/Sdf1_login/BanBroadcastManager.java @@ -59,6 +59,7 @@ public class BanBroadcastManager { private long startCutoffMs = 0; // 播报起点(毫秒):只播该时刻之后创建的封禁 private boolean startCutoffAuto = true; // 起点未配置 → 自动取插件数据目录创建时间(=插件安装时间) private long autoCutoffMs = -1; // 自动起点缓存(-1=未取) + private boolean jsonSyntaxOk = false; // json 文本语法是否有效(区分「语法坏」与「语法好但值空」) public BanBroadcastManager(Main plugin) { this.plugin = plugin; @@ -92,6 +93,7 @@ public class BanBroadcastManager { /** 配置文件丢失就补一份(自愈) */ private void ensureConfigExists() { + jsonFile = findJsonConfig(); // ★ 每轮重查:运行中新增/删除的 json 都要跟上 try { if (jsonFile != null) { if (!banDir.exists()) banDir.mkdirs(); @@ -122,12 +124,27 @@ public class BanBroadcastManager { mode = 1; startCutoffAuto = true; startCutoffMs = 0; - if (jsonFile == null) jsonFile = findJsonConfig(); + jsonFile = findJsonConfig(); // ★ 每轮重查(不再只在 null 时查一次) if (jsonFile != null && jsonFile.exists()) { String txt = readFile(jsonFile); if (parseJsonText(txt)) { log("已加载 json 配置: " + jsonFile.getName()); return; } + if (jsonSyntaxOk) { + // ★ 第十八批:语法有效但间隔值为空(文档语义「表格/json 二者选一填」) + // → 继承 json 里已解析的播报模式/播报起点,间隔回退 md 表格 + // (只取间隔,绝不覆盖已继承的模式/起点)或出厂默认,不判损毁。 + boolean iv = false; + if (mdFile != null && mdFile.exists()) { + String md0 = readFile(mdFile); + if (md0 != null) iv = parseIntervalTable(md0); + } + normalize(); + log("json 配置语法有效,已继承模式/起点(间隔回退" + + (iv ? "md 表格" : "出厂默认") + "): " + jsonFile.getName()); + return; + } + // 语法真损坏 → 备份+移除,继续回退 md 分支(不再就此 return) backupAndReset(jsonFile, jsonFile.getName()); - return; + jsonFile = null; } if (!mdFile.exists()) { ensureConfigExists(); } String md = readFile(mdFile); @@ -160,8 +177,10 @@ public class BanBroadcastManager { * @return true = json 配置生效;false = 视为未配置,调用方回退表格 */ private boolean parseJsonText(String txt) { + jsonSyntaxOk = false; try { JsonObject o = JsonParser.parseString(txt).getAsJsonObject(); + jsonSyntaxOk = true; // 语法解析成功(值是否为空另行判定) boolean hasIntervalKey = o.has("最小间隔") || o.has("最大间隔") || o.has("轮播最低间隔") || o.has("轮播最大间隔"); boolean anyInterval = false; @@ -219,6 +238,27 @@ public class BanBroadcastManager { return any; } + /** 只从 md 表格取间隔值(不碰播报模式/播报起点 —— 继承 json 时仅回退间隔用) */ + private boolean parseIntervalTable(String md) { + boolean any = false; + for (String line : md.split("\n")) { + String t = line.trim(); + if (!t.startsWith("|") || t.startsWith("| ---") || t.startsWith("|---")) continue; + String[] cells = t.split("\\|"); + if (cells.length < 4) continue; + String key = cells[1].trim(); + String val = cells[cells.length - 2].trim(); + if (key.equals("轮播最低间隔")) { minInterval = toSec(val); any = true; } + else if (key.equals("轮播最大间隔")) { maxInterval = toSec(val); any = true; } + } + if (any) { + if (minInterval < 0) minInterval = 0; + if (maxInterval < 0) maxInterval = 0; + if (maxInterval > 0 && minInterval > maxInterval) maxInterval = minInterval; + } + return any; + } + /** 播报模式文本/布尔/数字 → 0/1/2 */ private int parseMode(String s) { if (s == null) return 1; @@ -457,6 +497,11 @@ public class BanBroadcastManager { if (f != null && f.exists()) f.delete(); plugin.saveResource("ban/" + resourceName, false); log("已释放一份新配置: " + resourceName); + } catch (IllegalArgumentException e) { + // jar 里没有同名模板(如用户自建 cfg.json):原文件已备份并移除, + // 不算失败 —— 调用方会继续回退 md / 出厂默认 + warn("jar 内无模板资源 ban/" + resourceName + + ",原文件已备份并移除,回退其它配置源"); } catch (Exception e) { warn("备份/重置失败: " + e.getMessage()); } diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 5778dcc..6352607 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -1942,6 +1942,114 @@ public class Main extends JavaPlugin sender.sendMessage(sb.toString()); } + /** + * /banget 点名查询封禁:全量原生 banlist(不应用播报起点过滤, + * 插件安装之前的历史封禁也能查到),替代原版 /banlist 一屏全量打印。 + * 用法:/banget <玩家名|IP>(自动识别)| /banget user <名> | /banget ip + */ + @SuppressWarnings("unchecked") + private void doBanGet(CommandSender sender, String[] args) { + if (!isPrivileged(sender)) { + sender.sendMessage("§c权限不足"); + return; + } + String kind = null; // user / ip / null=自动识别 + String target = null; + if (args.length == 1) { + String a = args[0].toLowerCase(); + if (a.equals("user") || a.equals("ip")) { + sender.sendMessage("§c请指定查询目标: /banget " + a + " <玩家名或IP>"); + return; + } + target = args[0]; + } else if (args.length >= 2) { + String a = args[0].toLowerCase(); + if (!a.equals("user") && !a.equals("ip")) { + sender.sendMessage("§c用法: /banget [user|ip] <玩家名或IP>"); + return; + } + kind = a; + target = args[1]; + } else { + sender.sendMessage("§c用法: /banget [user|ip] <玩家名或IP>"); + sender.sendMessage("§7示例: §f/banget Notch §7| §f/banget user Notch" + + " §7| §f/banget ip 127.0.0.1"); + return; + } + boolean ipMode = kind != null ? kind.equals("ip") : isIpTarget(target); + + sender.sendMessage("§8========== §e封禁查询: §f" + target + + (ipMode ? " §7[IP]" : " §7[玩家名]") + " §8=========="); + int hits = 0; + int expiredHits = 0; + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + try { + org.bukkit.BanList bl = Bukkit.getBanList(ipMode + ? org.bukkit.BanList.Type.IP : org.bukkit.BanList.Type.NAME); + // ★ 全量查询:不应用播报起点,历史封禁照查; + // getEntries() 实际返回 Set,按 Collection 中转强转(防 ClassCastException) + for (Object oo : (java.util.Collection) bl.getEntries()) { + org.bukkit.BanEntry e = (org.bukkit.BanEntry) oo; + if (!banEntryMatches(e, target)) continue; + hits++; + Date created = e.getCreated(); + Date exp = e.getExpiration(); + boolean expired = exp != null + && exp.getTime() <= System.currentTimeMillis(); + if (expired) expiredHits++; + String reason = e.getReason(); + if (reason == null || reason.trim().isEmpty()) { + reason = "违反服务器规则"; + } + String src; + try { src = e.getSource(); } catch (Throwable t) { src = null; } + if (src == null || src.trim().isEmpty()) src = "未知"; + sender.sendMessage((expired ? "§8⌛ §f" : "§c☠ §f") + + target + " §7- " + reason); + sender.sendMessage("§7 封于§f" + + (created != null ? df.format(created) : "未知时间") + + " §8| " + (expired ? "§a已过期" + : (exp != null ? "§7至§f" + df.format(exp) : "§c永久")) + + " §8| §7来源§f" + src); + } + } catch (Exception ex) { + sender.sendMessage("§c查询封禁名单失败: " + ex.getMessage()); + return; + } + if (hits == 0) { + sender.sendMessage("§7未查到相关封禁(全量查询,含插件安装前的历史数据)"); + } else { + sender.sendMessage("§7共§f" + hits + "§7条记录" + + (expiredHits > 0 + ? " §8(其中§f" + expiredHits + "§8条已过期)" : "")); + } + } + + /** + * banget 目标匹配:getTarget() 字符串直比 + GameProfile[id=..,name=..] 兜底。 + * (NAME 型 getBanEntry(String) 新 API 已不可靠,故遍历 getEntries 自行匹配) + */ + private boolean banEntryMatches(org.bukkit.BanEntry e, String target) { + try { + String t = e.getTarget(); + if (t != null && t.equalsIgnoreCase(target)) return true; + } catch (Throwable ignored) { } + try { + Object bt = e.getBanTarget(); + if (bt == null) return false; + String bs = String.valueOf(bt); + if (bs.equalsIgnoreCase(target)) return true; + int ni = bs.indexOf("name="); + if (ni >= 0) { + String nm = bs.substring(ni + 5); + int end = nm.indexOf(']'); + if (end >= 0) nm = nm.substring(0, end); + if (nm.equalsIgnoreCase(target)) return true; + } + } catch (Throwable ignored) { } + return false; + } + private long asLong(Object o) { if (o == null) return 0; if (o instanceof Number) return ((Number) o).longValue(); @@ -4243,7 +4351,7 @@ public class Main extends JavaPlugin private static final Set ADMIN_ONLY = new HashSet<>( java.util.Arrays.asList( // 封禁 / 改判 / 注销 - "tempban", "tempbanip", "banip", "whois", "clear", + "tempban", "tempbanip", "banip", "banget", "whois", "clear", "cleartake", "清空队列", // 账号管理 "删除", "删除账号", "通过", "拒绝", "accept", "deny", @@ -8026,6 +8134,12 @@ public class Main extends JavaPlugin return true; } + // ===== /banget 点名查询封禁(全量 banlist,含插件安装前的历史封禁) ===== + if (cmd.getName().equalsIgnoreCase("banget")) { + doBanGet(sender, args); + return true; + } + String sub = (args.length > 0) ? args[0].toLowerCase() : ""; // ===== 在 onCommand 方法体内,最开头加: ===== @@ -10316,6 +10430,13 @@ public class Main extends JavaPlugin } return list; } + // ★ /banget 点名查询封禁:第一层 user/ip 提示 + if (cmd.getName().equalsIgnoreCase("banget")) { + if (args.length == 1) { + return filterTab(Arrays.asList("user", "ip"), args[0]); + } + return list; + } if (cmd.getName().equalsIgnoreCase( "sdf1_login") && args.length == 1) { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 52ec6a7..d919061 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -184,6 +184,9 @@ commands: who: description: 查看在线玩家列表 usage: /who + banget: + description: 点名查询封禁(全量,含插件安装前的历史封禁) + usage: /banget [user|ip] <玩家名或IP> 清空队列: description: 清空DB队列中未回应的web同步任务(管理员) usage: /清空队列