From d9fe2818abafd3873419e778801a46b49bcfb516 Mon Sep 17 00:00:00 2001 From: youpaishidifu <1@ypshidifu.cn> Date: Wed, 21 Oct 2026 17:05:38 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC33=E6=89=B9=EF=BC=9A=E5=9E=83=E5=9C=BE?= =?UTF-8?q?=E7=AE=B1=E4=BF=9D=E7=95=99=E6=AC=A1=E6=95=B0=E5=B7=AE=E4=B8=80?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E8=A1=80=E6=9D=A1=E6=8C=89=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E6=97=B6=E9=97=B4=E5=88=86=E6=AE=B5+=E6=8B=89?= =?UTF-8?q?=E9=BB=91=E5=88=B0=E6=9C=9F=E8=87=AA=E5=8A=A8=E8=A7=A3=E9=99=A4?= =?UTF-8?q?=E4=B8=8E=E8=80=BB=E8=BE=B1=E5=8F=B0=E6=8F=92=E6=92=AD+?= =?UTF-8?q?=E6=8B=89=E9=BB=91=E7=A1=AC=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 垃圾箱保留轮数:deleteRound=currentRound+1-maxRounds(旧公式少算1,R=1要到第3次清理才删第1批) - 血条色段改纯剩余时间:>=180绿/30~180黄(快动作了注意安全控好量)/<=30红;Q拦截改末5秒拦第一次、1秒窗口内第二次放行 - 黑名单时长:parseExpireMs 识别相对时长(30分/2小时/3天/1周)带双护栏;5秒定时器新增 sweepExpired 扫墙钟到期→自动解除+删md行+回响PHP - 耻辱台:applyBlacklist 即时插播拉黑公示(轮播6页x110~400s短时拉黑轮不到)+轮播排序黑名单置顶 - 拉黑硬拦截:不能打开任何容器/不能拾取物品/不能与村民交易(2秒节流提示) - addblack 帮助与黑名单.md 模板补相对时长格式说明 --- .../java/Sdf1_login/BanBroadcastManager.java | 13 ++- .../java/Sdf1_login/BlacklistManager.java | 101 ++++++++++++++++++ src/main/java/Sdf1_login/GarbageManager.java | 63 ++++++----- src/main/java/Sdf1_login/Main.java | 56 ++++++++++ src/main/resources/black/黑名单.md | 4 +- 5 files changed, 203 insertions(+), 34 deletions(-) diff --git a/src/main/java/Sdf1_login/BanBroadcastManager.java b/src/main/java/Sdf1_login/BanBroadcastManager.java index 652ae66..3cc299a 100644 --- a/src/main/java/Sdf1_login/BanBroadcastManager.java +++ b/src/main/java/Sdf1_login/BanBroadcastManager.java @@ -772,9 +772,16 @@ public class BanBroadcastManager implements Listener { if (bans.isEmpty()) { log("当前无生效封禁,跳过本轮"); return; } // 由近到远:最新封禁在前;由远到近:最早封禁在前 - bans.sort((a, b) -> mode == 1 - ? Long.compare(b.createdMs, a.createdMs) // 由近到远 - : Long.compare(a.createdMs, b.createdMs)); // 由远到近 + // ★ 第33批(#213):黑名单条目一律置顶(source="黑名单"), + // 防止其被分页轮播淹没 —— 短时拉黑等不到自己那一页。 + bans.sort((a, b) -> { + boolean ab = "黑名单".equals(a.source); + boolean bb = "黑名单".equals(b.source); + if (ab != bb) return ab ? -1 : 1; + return mode == 1 + ? Long.compare(b.createdMs, a.createdMs) // 由近到远 + : Long.compare(a.createdMs, b.createdMs); // 由远到近 + }); int total = bans.size(); int start = cursor % total; diff --git a/src/main/java/Sdf1_login/BlacklistManager.java b/src/main/java/Sdf1_login/BlacklistManager.java index 9891382..289fde3 100644 --- a/src/main/java/Sdf1_login/BlacklistManager.java +++ b/src/main/java/Sdf1_login/BlacklistManager.java @@ -127,6 +127,9 @@ public class BlacklistManager { if (mt != lastMtime || sz != lastSize) { reload(); } + // ★ 第33批(#213):文件没动也要扫 wall-clock 到期 + // (原实现只比 mtime+size,时长拉黑永远不自动解除) + sweepExpired(); } catch (Exception ignored) {} }, 100L, 100L); } @@ -357,6 +360,73 @@ public class BlacklistManager { } } + /** + * ★ 第33批(#213):墙钟到期扫描 —— 时长拉黑自动解除。 + *

根因:5 秒定时器只比对 mtime+size,md 文件不动就永不 reload, + * 而 reload 里只在「解析时」过滤已过期行 —— 于是绝对日期/相对时长 + * 到期后,处罚(applied、债券冻结、md 行)永远挂着 = 用户反馈的 + * 「配置时长不生效」。本方法独立于文件变化,每 5 秒按当前时间扫。 + *

到期条目:内存移除 → 恢复处罚 → 删 md 对应行 → 回响 PHP。 + */ + private synchronized void sweepExpired() { + if (entries.isEmpty()) return; + long now = System.currentTimeMillis(); + List expired = new ArrayList<>(); + for (BlackEntry e : new ArrayList<>(entries.values())) { + if (e.isExpired(now)) expired.add(e); + } + if (expired.isEmpty()) return; + + List names = new ArrayList<>(); + for (BlackEntry e : expired) { + entries.remove(e.name); + names.add(e.name); + plugin.getLogger().info("[黑名单] 拉黑到期自动解除: " + e.name + + "(" + e.timeText + ")"); + restoreBlacklist(e.name); + } + + // 同步删 md 里对应行(防止文件签名不变、行永远留着) + try { + List lines = readLines(); + if (lines != null) { + int[] zone = locateZone(lines); + if (zone != null) { + List out = new ArrayList<>(); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + boolean drop = false; + if (i > zone[0] && i < zone[1] && line.trim().startsWith("|") + && !isSeparatorRow(line.trim())) { + for (String n : names) { + if (isRowOf(line, AreaProtection.normName(n))) { + drop = true; + break; + } + } + } + if (!drop) out.add(line); + } + if (out.size() != lines.size() && writeLines(out)) { + // 写回成功 → 立即刷新签名,免得下个 tick 又整表 reload 一遍 + if (mdFile.exists()) { + lastMtime = mdFile.lastModified(); + lastSize = mdFile.length(); + } + } + } + } + } catch (Exception ex) { + plugin.getLogger().warning("[黑名单] 删除到期 md 行失败: " + ex.getMessage()); + } + + // 名单变了 → 回响 PHP(web_player_blacklist 同步移除) + try { + WebManager wm = plugin.webManager; + if (wm != null) wm.forceSyncBlacklist(); + } catch (Exception ignored) {} + } + /** * 拉黑时长 → 到期毫秒;0 = 永久。 *

空 / 0 / 终身 / 永久 / forever → 永久; @@ -394,6 +464,21 @@ public class BlacklistManager { return c.getTimeInMillis(); } } + // ★ 第33批(#213):相对时长(3天 / 7d / 2小时 / 30分 …)—— + // 「配置了时长 = 临时拉黑,到期自动解除」。交 DurationParser 统一解析。 + // 两个护栏: + // ① 只认「数字+单位」形态 —— DurationParser 的纯数字分支=Unix秒时间戳(1970), + // 裸写 "100" 会从原语义「永久」变成「立即过期」,比不识别更危险; + // ② 排除含「年」的中文日期(2026年10月20日)—— 那属绝对日期语义, + // DurationParser 会取当天 00:00 = 配完即过期。 + if (t.matches(".*\\d+\\s*(周|天|日|时|分|秒|小时|分钟|星期|礼拜|w|d|h|m|s|W|D|H|M|S).*") + && !t.contains("年")) { + try { + long v = DurationParser.parseToExpireMs(t); + if (v > 0L) return v; + } catch (Throwable ignored) { + } + } // 解析不了 → 永久(宁可拉黑也不放过) return 0L; } @@ -423,6 +508,22 @@ public class BlacklistManager { plugin.getLogger().info("[黑名单] 拉黑生效: " + name + "(" + e.timeText + " / " + e.reason + ")"); + // ★ 第33批(#213):拉黑当下即时插播一条「拉黑公示」。 + // 根因:耻辱台轮播 6 页 × 110~400s 随机间隔,短时拉黑 + // (如测试用 30 秒/13 秒)永远轮不到那一页 = 用户反馈 + // 「没能进入耻辱台」。插播保证拉黑瞬间全服可见。 + try { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + String when = df.format(new Date(e.createdMs > 0 ? e.createdMs + : System.currentTimeMillis())); + String expStr = e.expiresMs > 0 ? ("§7至§f" + df.format(new Date(e.expiresMs))) + : "§c永久"; + Bukkit.broadcastMessage("§c§l⚠ §6拉黑公示 §c☠ §f" + name + + " §7- " + (e.reason == null || e.reason.isEmpty() + ? "违反服务器规则" : e.reason) + + " §8| §7拉黑于§f" + when + " §8| " + expStr); + } catch (Exception ignored) {} + // 1) 冻结债券账户(按拉黑到期时间;永久=0) try { BondManager bm = plugin.getBonds(); diff --git a/src/main/java/Sdf1_login/GarbageManager.java b/src/main/java/Sdf1_login/GarbageManager.java index 94c4f22..5ca1244 100644 --- a/src/main/java/Sdf1_login/GarbageManager.java +++ b/src/main/java/Sdf1_login/GarbageManager.java @@ -225,16 +225,17 @@ public class GarbageManager implements Listener { /** * ★ 第30批:顶部血条常驻倒计时 + 色段软提醒。 * - *

色段按「墙钟已过时间」的绝对时刻判定(第31批 #200 修正, - * 不再用百分比,不随清理间隔漂移): + *

★ 第33批(#211):色段改按「剩余时间」纯数值分段(用户明令): *

+ * 第31批的「墙钟绝对时刻」方案废弃;百分比方案更早废弃(间隔一改就漂移)。 * *

★ 第32批:血条与启发式开关解耦 —— 只要垃圾站启用,血条常驻显示; - * 启发式开关(垃圾站_启发式拦截)关闭时只关掉「红区title警告 + 双击Q拦截」, + * 启发式开关(垃圾站_启发式拦截)关闭时只关掉「黄/红区title警告 + 末5秒双击Q拦截」, * 不再撤血条(否则玩家对清理状态一无所知)。 */ private void updateBossBars(int remain) { @@ -242,23 +243,15 @@ public class GarbageManager implements Listener { double progress = (double) remain / (double) interval; if (progress < 0.0D) progress = 0.0D; if (progress > 1.0D) progress = 1.0D; - // ★ 第31批(#200):色段改按「墙钟已过时间」的绝对时刻判定: - // 0:00~3:00 绿 / 3:01~4:29 黄 / 4:30~5:00 红(另:剩余≤30秒兜底红)。 - // 不用 elapsed = interval - remain 的百分比,否则间隔一改、色段就跟着漂移。 - long wallMs = roundStartMs > 0L - ? System.currentTimeMillis() - roundStartMs - : (long) (interval - remain) * 1000L; - int elapsed = (int) Math.min((long) interval, - Math.max(0L, wallMs / 1000L)); BossBar.Color color; String label; - if (remain <= 30 || elapsed >= 270) { + if (remain <= 30) { color = BossBar.Color.RED; label = "§c§l危险 §7| §c马上清理,抓紧捡"; - } else if (elapsed > 180) { + } else if (remain < 180) { color = BossBar.Color.YELLOW; - label = "§e§l注意 §7| §e垃圾箱快动作了,别丢了"; + label = "§e§l注意 §7| §e快动作了,注意安全控好量"; } else { color = BossBar.Color.GREEN; label = "§a§l安全期 §7| §a还早,放心丢"; @@ -359,36 +352,42 @@ public class GarbageManager implements Listener { } /** - * ★ 第30批:垃圾箱丢弃拦截(仅活跃于本轮最后 10 秒窗口)。 + * ★ 第30批:垃圾箱丢弃拦截。 * - *

最后 30 秒(红区):对丢弃物品的玩家发小标题警告。 - *

最后 10 秒(拦截窗口):单击 Q 被拦截,需 2 秒内再按一次 Q 确认才放行; - * 其他时段完全不干涉。 + *

★ 第33批(#211)三处改造: + *

*/ @EventHandler public void onPlayerDropItem(PlayerDropItemEvent e) { if (!enabled) return; if (!plugin.getConfig2().garbageHeuristic) return; int remain = cleanupInterval - countdown; - if (remain > 30) return; // 不在红区 → 不干涉 + if (remain > 30) return; // 安全区+警告期(>30秒)→ 不干涉 Player p = e.getPlayer(); - // 小标题警告(红区) - p.sendTitle("", "§c§l垃圾站警告 §f马上就清理了,别乱丢!", + // 危险期(红区 ≤30秒)小标题:按用户口径「注意安全控好量」,不写「别丢」 + p.sendTitle("", "§c§l垃圾站警告 §f马上清理了,注意安全控好量!", 3, 30, 5); - if (remain > 10) return; // 拦截仅在最后 10 秒窗口 + if (remain > 5) return; // ★ 拦截仅在最后 5 秒窗口 long now = System.currentTimeMillis(); UUID uuid = p.getUniqueId(); Long first = pendingDrops.get(uuid); - if (first != null && now - first <= 2000L) { + if (first != null && now - first <= 1000L) { pendingDrops.remove(uuid); - return; // 双击确认 → 放行 + return; // ★ 1 秒窗口内连按 2 次 Q = 确认丢弃 → 放行 } + // 首次按键(或上次按键已超 1 秒窗口 → 视为过期,重新计时) pendingDrops.put(uuid, now); e.setCancelled(true); - p.sendMessage("§e§l[垃圾站] §f马上就清理了!再按一次 §eQ§f 确认丢弃," + p.sendMessage("§e§l[垃圾站] §e马上清理了!1秒内再按一次 §eQ§f 确认丢弃," + "或用 §e/垃圾抢救§f 取消本轮清理"); } @@ -415,8 +414,14 @@ public class GarbageManager implements Listener { private void doCleanup() { // ★ 过期轮次删除是阻塞 DB -> 先异步执行; // 轮次递增 / 实体收集 / 广播 必须主线程 -> 异步完成后回主线程 + // ★ 第33批(#215):保留轮数语义修正 —— 保留次数 R = 垃圾存活 R 轮, + // 第 N 次清理(本方法入口时 currentRound = N-1)删除第 (N-R) 批, + // 即 deleteRound = (N-1)+1-R。旧公式 currentRound-maxRounds 少算 1, + // 导致 R=1 要到第 3 次清理才删第 1 批(保留轮数形同虚设)。 + // 验证:R=1 → 第2次清理删第1批;R=3 → 第4次清理删第1批; + // 永久清理轮次 = 当前轮 + 保留次数。 final int deleteRound = - currentRound - maxRounds; + currentRound + 1 - maxRounds; Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { final int deleted = diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index be53fbd..883ceb2 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -5915,6 +5915,61 @@ public class Main extends JavaPlugin } } + // ==================== ★ 第33批(#213):拉黑玩家行为硬拦截 ==================== + + /** 拉黑玩家提示节流(同一玩家 2 秒最多一条,防刷屏) */ + private final Map blackMsgAt = new ConcurrentHashMap<>(); + + /** 是否为生效中的拉黑玩家 */ + private boolean isBlackPlayer(Player p) { + if (p == null) return false; + BlacklistManager bl = getBlacklist(); + return bl != null && bl.isBlacklisted(p.getName()); + } + + /** 拉黑拦截统一提示(2 秒节流) */ + private void blackBlocked(Player p, String what) { + long now = System.currentTimeMillis(); + Long last = blackMsgAt.get(p.getUniqueId()); + if (last != null && now - last < 2000L) return; + blackMsgAt.put(p.getUniqueId(), now); + p.sendMessage("§c§l[罪人] §f你已被拉黑," + what); + } + + // ★ 拉黑玩家不能打开任何容器(箱子/木桶/熔炉/末影箱…含村民交易界面) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlackOpenInventory( + org.bukkit.event.inventory.InventoryOpenEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + if (!isBlackPlayer(p)) return; + e.setCancelled(true); + blackBlocked(p, "不能打开任何容器"); + } + + // ★ 拉黑玩家不能拾取任何物品 + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlackPickup( + org.bukkit.event.entity.EntityPickupItemEvent e) { + if (!(e.getEntity() instanceof Player)) return; + Player p = (Player) e.getEntity(); + if (!isBlackPlayer(p)) return; + e.setCancelled(true); + blackBlocked(p, "不能拾取任何物品"); + } + + // ★ 拉黑玩家不能与村民交易(村民交易界面走 InventoryOpenEvent, + // 此处拦右键交互入口兜底,防交易界面闪出) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlackVillagerTrade( + org.bukkit.event.player.PlayerInteractEntityEvent e) { + if (!(e.getRightClicked() instanceof org.bukkit.entity.Villager)) return; + Player p = e.getPlayer(); + if (!isBlackPlayer(p)) return; + e.setCancelled(true); + blackBlocked(p, "不能与村民交易"); + } + // ★ 防止玩家右键实体时交出菜单雪球(防恶魂投喂) @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerInteractEntity( @@ -9252,6 +9307,7 @@ public class Main extends JavaPlugin if (args.length < 1) { sender.sendMessage("§c用法: /addblack <玩家名> [时长] [理由]"); sender.sendMessage("§7时长: 空/0/终身=永久,2026-10-21 18:00=到点,2026-10-21=当天23:59:59"); + sender.sendMessage("§7 相对时长: 30分 / 2小时 / 3天 / 1周(到期自动解除,无需改文件)"); return true; } if (blacklist == null) { diff --git a/src/main/resources/black/黑名单.md b/src/main/resources/black/黑名单.md index fdede1f..199b0c0 100644 --- a/src/main/resources/black/黑名单.md +++ b/src/main/resources/black/黑名单.md @@ -8,7 +8,7 @@ ## 软折磨黑名单配置 功能说明:
-此功能为软折磨,用于折磨违反规则的玩家。通过`/addblack user time(可选) msg(可选)`添加违规玩家或直接编辑本文件即可。被拉黑的玩家不能消费债券、不能登录PHP后端、不能创建领地,不能成为他人领地的授权玩家。不能破坏、放置方块,没有背包(清空前备份,自动备份跳过该玩家)。能说话但,会被系统加上"---我是罪人user"的签名 +此功能为软折磨,用于折磨违反规则的玩家。通过`/addblack user time(可选) msg(可选)`添加违规玩家或直接编辑本文件即可。被拉黑的玩家不能消费债券、不能登录PHP后端、不能创建领地,不能成为他人领地的授权玩家。不能破坏、放置方块,不能打开任何容器、不能拾取任何物品、不能与村民交易,没有背包(清空前备份,自动备份跳过该玩家)。能说话但,会被系统加上"---我是罪人user"的签名 ## 示例配置: | 玩家名字 | 拉黑时长 | 理由 | @@ -26,5 +26,5 @@ | /addblack | Dream | 0 | 空 | | /removeblack | Notch | 空 | 空 | -- 备注:0表示永久拉黑,填入日期+时间则表示遵循到期时间。填入日期则表示以当天23:59:59为界限
+- 备注:0表示永久拉黑,填入日期+时间则表示遵循到期时间。填入日期则表示以当天23:59:59为界限。填相对时长(30分 / 2小时 / 3天 / 1周)= 从拉黑那一刻起算,到期自动解除并自动恢复背包与权限
- 生效规则: 定时,每分钟的第0秒、5秒、10秒...以此类推、55秒定时增量重载,删了5秒后立即恢复背包和各项权限,加了5秒后立即生效