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 空 / 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):色段改按「剩余时间」纯数值分段(用户明令):
* ★ 第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)三处改造:
+ *
- *
+ * 第31批的「墙钟绝对时刻」方案废弃;百分比方案更早废弃(间隔一改就漂移)。
*
*
+ *
*/
@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
-此功能为软折磨,用于折磨违反规则的玩家。通过`/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秒后立即生效