第33批:垃圾箱保留次数差一修复+血条按剩余时间分段+拉黑到期自动解除与耻辱台插播+拉黑硬拦截
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

- 垃圾箱保留轮数: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 模板补相对时长格式说明
This commit is contained in:
youpaishidifu
2026-10-21 17:05:38 +08:00
parent 8311c412a1
commit d9fe2818ab
5 changed files with 203 additions and 34 deletions
@@ -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;
@@ -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):墙钟到期扫描 —— 时长拉黑自动解除。
* <p>根因:5 秒定时器只比对 mtime+size,md 文件不动就永不 reload,
* 而 reload 里只在「解析时」过滤已过期行 —— 于是绝对日期/相对时长
* 到期后,处罚(applied、债券冻结、md 行)永远挂着 = 用户反馈的
* 「配置时长不生效」。本方法独立于文件变化,每 5 秒按当前时间扫。
* <p>到期条目:内存移除 → 恢复处罚 → 删 md 对应行 → 回响 PHP。
*/
private synchronized void sweepExpired() {
if (entries.isEmpty()) return;
long now = System.currentTimeMillis();
List<BlackEntry> expired = new ArrayList<>();
for (BlackEntry e : new ArrayList<>(entries.values())) {
if (e.isExpired(now)) expired.add(e);
}
if (expired.isEmpty()) return;
List<String> 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<String> lines = readLines();
if (lines != null) {
int[] zone = locateZone(lines);
if (zone != null) {
List<String> 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 = 永久。
* <p>空 / 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();
+34 -29
View File
@@ -225,16 +225,17 @@ public class GarbageManager implements Listener {
/**
* ★ 第30批:顶部血条常驻倒计时 + 色段软提醒。
*
* <p>色段按「墙钟已过时间」的绝对时刻判定(第31批 #200 修正,
* 不再用百分比,不随清理间隔漂移):
* <p>★ 第33批(#211):色段改按「剩余时间」纯数值分段(用户明令):
* <ul>
* <li>0:00 ~ 3:00(墙钟前 3 分钟)→ 绿 = 安全期</li>
* <li>3:01 ~ 4:29 → 黄 = 警告「垃圾箱快动作了,别丢了」</li>
* <li>4:30 ~ 5:00 → 红 = 危险(另:剩余≤30秒兜底红)</li>
* <li>剩余 ≥ 180 秒 → 绿 = 安全期</li>
* <li>30 秒 < 剩余 < 180 秒 → 黄 = 警告(只提示快动作了,
* 丢的时候注意安全控好量,不误导「别丢」)</li>
* <li>剩余 ≤ 30 秒 → 红 = 危险</li>
* </ul>
* 第31批的「墙钟绝对时刻」方案废弃;百分比方案更早废弃(间隔一改就漂移)。
*
* <p>★ 第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批:垃圾箱丢弃拦截。
*
* <p>最后 30 秒(红区):对丢弃物品的玩家发小标题警告。
* <p>最后 10 秒(拦截窗口):单击 Q 被拦截,需 2 秒内再按一次 Q 确认才放行;
* 其他时段完全不干涉。
* <p>★ 第33批(#211)三处改造:
* <ul>
* <li>警告期(黄区 30~180 秒)不加任何 title 干预 —— 该期的提示
* 只走头顶血条(updateBossBars 文案已改「注意安全控好量」);</li>
* <li>拦截窗口从末 10 秒收窄到末 5 秒,且仅启发式开关启用时生效;</li>
* <li>确认窗口从 2 秒收窄到 1 秒:拦下第一次 Q,1 秒内连按
* 第二次 = 确认丢弃放行;超过 1 秒视为过期跳过(重新计时)。</li>
* </ul>
*/
@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 =
+56
View File
@@ -5915,6 +5915,61 @@ public class Main extends JavaPlugin
}
}
// ==================== ★ 第33批(#213):拉黑玩家行为硬拦截 ====================
/** 拉黑玩家提示节流(同一玩家 2 秒最多一条,防刷屏) */
private final Map<UUID, Long> 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) {
+2 -2
View File
@@ -8,7 +8,7 @@
## 软折磨黑名单配置
功能说明:<br>
此功能为软折磨,用于折磨违反规则的玩家。通过`/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为界限<br>
- 备注:0表示永久拉黑,填入日期+时间则表示遵循到期时间。填入日期则表示以当天23:59:59为界限。填相对时长(30分 / 2小时 / 3天 / 1周)= 从拉黑那一刻起算,到期自动解除并自动恢复背包与权限<br>
- 生效规则: 定时,每分钟的第0秒、5秒、10秒...以此类推、55秒定时增量重载,删了5秒后立即恢复背包和各项权限,加了5秒后立即生效