第30批:群控黑名单(软折磨)+ 垃圾箱抢救模式 + 顶部血条软提醒
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m22s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

【一、群控黑名单 BlacklistManager(新增)】
- 数据源:plugins/Sdf1_login/black/黑名单.md 配置区表格
  | 玩家名字 | 拉黑时长 | 理由 |
- 时长口径:空/0/终身=永久;yyyy-MM-dd HH:mm=到点;仅日期=当天23:59:59
- 5秒增量重载(mtime+size 变化才解析),加行5秒生效、删行5秒恢复
- 拉黑处罚三件套:冻结债券账户 + 撤销全部领地管理员身份(admin→visitor)
  + 备份背包并清空(异步落库快照后回主线程清空)
- 解除拉黑两件套:解冻债券 + 从最新自动备份恢复背包
- 新增 DatabaseManager.getLatestAutoBackup(inv_backup_auto 按 save_time 倒序取1)
- 新增 AreaProtection.demoteBlacklistAdmins(normName 定位逐行 UPDATE)

【二、黑名单拦截点】
- CDKManager.redeem 入口:fail:blacklisted(联控 Sdf1 插件 fail:blacklisted 分支)
- WebManager.handleWebLogin / autoGenerateWebLoginToken:禁止签发 PHP 后端 token
- AreaProtection:创建/confirm_create/quickCreateLand 禁止建领地;
  addvisitor 禁止把黑名单玩家加进授权名单
- AreaCLIManager:showLandManage/togglePerm/toggleAdmin/clearPlayerPerm 禁止管理领地
- Main:onBlockBreak/onBlockPlace2/onBlockPlace 禁止破坏与放置方块
- InventoryAutoBackup:黑名单玩家跳过自动备份(保护拉黑前快照)
- BanBroadcastManager:collectBlacklistBans 并入耻辱台公示(独立数据源)
- 聊天消息末尾追加「----我是罪人user」称号

【三、垃圾箱抢救模式 + 顶部血条】
- 指令 /垃圾抢救:跳过本轮清理并重新计时(countdown 提为字段)
- 顶部 BossBar 常驻倒计时:前60%浅绿安全 / 60%~90%黄色警告
  「垃圾箱快动作了,别丢了」/ 最后10%红色危险
- PlayerDropItemEvent:红区发小标题警告;最后10秒单击Q被拦截、
  2秒内双击Q确认放行,其他时段完全不干涉
- 插件设置.txt 新增「垃圾站_启发式拦截」开关(默认true,关闭后撤血条不提示不拦截,
  仅重载后控制台打一条 warn)

【四、指令】
- /addblack <玩家> [时长] [理由]、/removeblack <玩家>
- 插件.yml 注册 addblack/removeblack/垃圾抢救

【五、Paper 26.x 适配】
- BossBar 改用 Adventure API(net.kyori.adventure.bossbar.BossBar),
  org.bukkit.boss.BossBar 在 26.x 已移除
This commit is contained in:
youpaishidifu
2026-10-21 10:34:34 +08:00
parent c7d89a972d
commit 3cd1999b85
13 changed files with 1217 additions and 7 deletions
@@ -70,6 +70,33 @@ public class AreaCLIManager {
return Component.text(text).color(color);
}
/**
* ★ 第30批:拉黑玩家禁止管理已有领地(降级访客后不再有任何管理权)。
* @return true=被拦截(调用方应直接 return)
*/
private boolean denyIfBlacklisted(Player p) {
if (p == null) return false;
BlacklistManager bl = plugin.getBlacklist();
if (bl == null) return false;
if (!bl.isBlacklisted(p.getName())) return false;
p.sendMessage("§c§l[罪人] §f你已被拉黑,禁止管理领地");
return true;
}
/**
* ★ 第30批:禁止把被拉黑的玩家加进领地授权名单。
* @return true=目标玩家在黑名单(调用方应直接 return)
*/
private boolean denyIfTargetBlacklisted(Player p, String targetPlayer) {
if (targetPlayer == null || targetPlayer.isEmpty()) return false;
BlacklistManager bl = plugin.getBlacklist();
if (bl == null) return false;
if (!bl.isBlacklisted(targetPlayer)) return false;
p.sendMessage("§c§l[罪人] §f玩家 §e" + targetPlayer
+ " §f已被拉黑,不能被授予领地权限");
return true;
}
public static Component header(String title) {
return Component.text("\n§6§l═══════ " + title + " ═══════\n");
}
@@ -316,6 +343,7 @@ public class AreaCLIManager {
// ==================== 领地管理 ====================
public void showLandManage(Player p, String landName, int permPage) {
if (denyIfBlacklisted(p)) return;
savePageInfo(p, "manage", landName, 1);
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
@@ -532,6 +560,7 @@ public class AreaCLIManager {
// ==================== 切换权限 ====================
public void togglePerm(Player p, String landName, String permKey, int page) {
if (denyIfBlacklisted(p)) return;
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -1029,6 +1058,8 @@ public class AreaCLIManager {
* ★ 切换玩家的管理员身份(仅领地主可操作)
*/
public void toggleAdmin(Player p, String landName, String targetPlayer, int page) {
if (denyIfBlacklisted(p)) return;
if (denyIfTargetBlacklisted(p, targetPlayer)) return;
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -1116,6 +1147,7 @@ public class AreaCLIManager {
* 清除某成员所有per-player权限覆盖
*/
public void clearPlayerPerm(Player p, String landName, String targetPlayer) {
if (denyIfBlacklisted(p)) return;
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
@@ -2960,6 +2960,44 @@ public class AreaProtection implements Listener {
}
}
/**
* 拉黑时撤销该玩家在所有领地的管理员身份(admin -> visitor)。
* 走 normName 定位(历史数据大小写可能不一致),逐行 UPDATE。
* @return 撤销的管理员身份数量
*/
public int demoteBlacklistAdmins(String playerName) {
if (dbConnection == null || playerName == null || playerName.isEmpty()) return 0;
int count = 0;
String target = normName(playerName);
java.util.List<Integer> ids = new java.util.ArrayList<>();
try {
PreparedStatement sel = dbConnection.prepareStatement(
"SELECT id, player_name FROM area_land_permissions WHERE role = 'admin'");
ResultSet rs = sel.executeQuery();
while (rs.next()) {
String pn = rs.getString("player_name");
if (pn != null && normName(pn).equals(target)) {
ids.add(rs.getInt("id"));
}
}
rs.close();
sel.close();
} catch (SQLException e) {
return 0;
}
for (int id : ids) {
try {
PreparedStatement up = dbConnection.prepareStatement(
"UPDATE area_land_permissions SET role = 'visitor' WHERE id = ?");
up.setInt(1, id);
count += up.executeUpdate();
up.close();
} catch (SQLException ignored) {
}
}
return count;
}
/**
* 插入一条领地权限记录(角色为 member/visitor),已存在则忽略
* 用于 PHP 端添加成员时同步到 Java
@@ -7613,6 +7651,12 @@ public class AreaProtection implements Listener {
return true;
}
Player p = (Player) sender;
// ★ 第30批:拉黑玩家禁止创建新领地
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(p.getName())) {
p.sendMessage("§c§l[罪人] §f你已被拉黑,禁止创建领地");
return true;
}
UUID u = p.getUniqueId();
// ★ 无参数 → 快速创建(3×3以自身为中心)
@@ -7723,6 +7767,12 @@ public class AreaProtection implements Listener {
if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; }
if (args.length < 2) { sender.sendMessage("§c用法: /protect confirm_create <领地名>"); return true; }
Player cp = (Player) sender;
// ★ 第30批:拉黑玩家禁止创建新领地(交互链接可被直接拼装,必须再校验一次)
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(cp.getName())) {
cp.sendMessage("§c§l[罪人] §f你已被拉黑,禁止创建领地");
return true;
}
String confirmName = args[1].trim();
// ★ 领地名格式校验(交互链接可被直接拼装,必须在这里再校验一次)
String cnErr = validateAreaName(confirmName);
@@ -9648,6 +9698,13 @@ public class AreaProtection implements Listener {
return true;
}
}
// ★ 第30批:被拉黑的玩家不能被授予领地权限
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(args[1])) {
sender.sendMessage("§c§l[罪人] §f玩家 §e" + args[1]
+ " §f已被拉黑,不能被授予领地权限");
return true;
}
addPlayerToAreaWhitelist(ac.name, args[1]);
insertLandPermission(ac.name, args[1], "visitor");
sender.sendMessage("§a已添加 §e" + args[1] + " §a为 §e" + ac.name + " §a的访客");
@@ -9678,6 +9735,13 @@ public class AreaProtection implements Listener {
cancelPendingDelete(areaName);
p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除");
}
// ★ 第30批:被拉黑的玩家不能被授予领地权限
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(playerName)) {
sender.sendMessage("§c§l[罪人] §f玩家 §e" + playerName
+ " §f已被拉黑,不能被授予领地权限");
return true;
}
addPlayerToAreaWhitelist(areaName, playerName);
insertLandPermission(areaName, playerName, "visitor");
sender.sendMessage("§a已添加 §e" + playerName + " §a为 §e" + areaName + " §a的访客");
@@ -13850,6 +13914,12 @@ public class AreaProtection implements Listener {
*/
public boolean quickCreateLand(Player p) {
if (p == null || dbConnection == null) return false;
// ★ 第30批:拉黑玩家禁止创建新领地
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(p.getName())) {
p.sendMessage("§c§l[罪人] §f你已被拉黑,禁止创建领地");
return false;
}
// ★ 检查领地上限
UserGroupManager ugm = plugin.getUserGroup();
@@ -52,7 +52,6 @@ import java.util.regex.Pattern;
* 间隔或模式为 0 → 关闭播报。
*/
public class BanBroadcastManager implements Listener {
private final Main plugin;
private final Random rnd = new Random();
@@ -736,9 +735,40 @@ public class BanBroadcastManager implements Listener {
return 0L;
}
/**
* ★ 第30批:黑名单数据源(群控黑名单 = 软折磨,与封禁公示并列)。
* 复用 JsonBan 结构,source 标 "黑名单",理由取黑名单条目的理由。
*/
private List<JsonBan> collectBlacklistBans() {
List<JsonBan> out = new ArrayList<>();
BlacklistManager bl = plugin.getBlacklist();
if (bl == null) return out;
try {
for (BlacklistManager.BlackEntry e : bl.getActiveEntries()) {
if (e == null || e.name == null || e.name.isEmpty()) continue;
out.add(new JsonBan(e.name,
e.reason != null && !e.reason.isEmpty()
? e.reason : "违反服务器规则",
"黑名单",
e.createdMs > 0 ? e.createdMs : System.currentTimeMillis(),
e.expiresMs));
}
} catch (Exception ex) {
warn("读取黑名单失败: " + ex.getMessage());
}
return out;
}
/** 轮播当前页并广播给全服 */
private void broadcastOnce() {
List<JsonBan> bans = collectBans();
// ★ 第30批:并入黑名单玩家(独立数据源,不受 banned-players.json 是否存在影响)
int blackCount = 0;
try {
List<JsonBan> bl = collectBlacklistBans();
blackCount = bl.size();
bans.addAll(bl);
} catch (Exception ignored) {}
if (bans.isEmpty()) { log("当前无生效封禁,跳过本轮"); return; }
// 由近到远:最新封禁在前;由远到近:最早封禁在前
@@ -768,6 +798,7 @@ public class BanBroadcastManager implements Listener {
}
Bukkit.broadcastMessage("§7§o请各位玩家引以为戒,遵守服务器规则。");
log("本轮已播报 " + page.size() + "/" + total + " 条封禁"
+ "(含黑名单 " + blackCount + " 条)"
+ "(本次随机间隔 " + lastDelaySec + "s,区间 "
+ minInterval + "~" + maxInterval + "s;banned-players.json 原始 "
+ lastFileRawCount + " 条,过滤后 " + lastFileKeptCount + " 条)");
@@ -0,0 +1,651 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* ★ 第30批:群控黑名单(软折磨)。
*
* <p>数据源:{@code plugins/Sdf1_login/black/黑名单.md} 的「配置区域」表格。
*
* <p>拉黑时长口径:空 / 0 / 终身 / 永久 = 永久;
* {@code yyyy-MM-dd HH:mm} = 到期即该时刻;{@code yyyy-MM-dd} = 当天 23:59:59。
*
* <p>生效规则:每 5 秒比对文件 mtime+size 做增量重载。
* 新增条目 → 立即生效(备份并清空背包 + 冻结债券账户 + 撤销领地管理员身份);
* 删除条目 → 立即恢复(解冻债券 + 从最新自动备份恢复背包)。
*
* <p>指令:{@code /addblack <玩家> [时长] [理由]}、{@code /removeblack <玩家>}。
*/
public class BlacklistManager {
private final Main plugin;
/** 黑名单配置文件:plugins/Sdf1_login/black/黑名单.md */
private File mdFile;
/** 配置区域起止标记 */
private static final String ZONE_START = "### ----配置区域开始----";
private static final String ZONE_END = "### ----配置区域结束";
/** 内存黑名单:玩家名 → 条目 */
private final Map<String, BlackEntry> entries = new LinkedHashMap<>();
/** 已施加处罚的玩家 —— 防重复应用(备份/清空只做一次) */
private final Set<String> applied = ConcurrentHashMap.newKeySet();
/** 上次读取的文件签名(mtime + size),用于增量重载判定 */
private long lastMtime = -1L;
private long lastSize = -1L;
/** 写回 md 时使用的行尾风格(true=CRLF) */
private boolean crlfStyle = true;
public BlacklistManager(Main plugin) {
this.plugin = plugin;
}
// ==================== 条目模型 ====================
/** 黑名单条目 */
public static final class BlackEntry {
/** 玩家名(原始拼写,写回 md 用) */
public final String name;
/** 拉黑时长原文 */
public final String timeText;
/** 理由 */
public final String reason;
/** 到期毫秒;0 = 永久 */
public final long expiresMs;
/** 记录时间毫秒 */
public final long createdMs;
BlackEntry(String name, String timeText, String reason,
long expiresMs, long createdMs) {
this.name = name;
this.timeText = timeText;
this.reason = reason;
this.expiresMs = expiresMs;
this.createdMs = createdMs;
}
public boolean isExpired(long nowMs) {
return expiresMs > 0L && expiresMs <= nowMs;
}
}
// ==================== 生命周期 ====================
/** 初始化:定位/释放配置文件 → 首次加载 → 启动 5 秒增量重载 */
public void init() {
try {
File dir = new File(plugin.getDataFolder(), "black");
if (!dir.exists()) dir.mkdirs();
mdFile = new File(dir, "黑名单.md");
if (!mdFile.exists()) {
try (InputStream in = plugin.getResource("black/黑名单.md")) {
if (in != null) {
Files.copy(in, mdFile.toPath());
}
} catch (IOException e) {
plugin.getLogger().warning("[黑名单] 模板释放失败: " + e.getMessage());
}
}
} catch (Exception e) {
plugin.getLogger().warning("[黑名单] 初始化失败: " + e.getMessage());
return;
}
reload();
// ★ 每 5 秒增量重载(mtime+size 变化才重新解析)
Bukkit.getScheduler().runTaskTimer(plugin, () -> {
try {
if (mdFile == null || !mdFile.exists()) return;
long mt = mdFile.lastModified();
long sz = mdFile.length();
if (mt != lastMtime || sz != lastSize) {
reload();
}
} catch (Exception ignored) {}
}, 100L, 100L);
}
public File getMdFile() {
return mdFile;
}
// ==================== 查询 ====================
/**
* 该玩家是否在生效中的黑名单里。
* <p>★ 精确匹配(本服离线模式,大小写不同 = 不同玩家)。
*/
public boolean isBlacklisted(String playerName) {
if (playerName == null || playerName.isEmpty()) return false;
return findEntry(playerName) != null;
}
/** 精确查条目(过期返回 null) */
public BlackEntry findEntry(String playerName) {
if (playerName == null || playerName.isEmpty()) return null;
BlackEntry e = entries.get(playerName);
if (e == null) return null;
if (e.isExpired(System.currentTimeMillis())) {
entries.remove(playerName);
return null;
}
return e;
}
/** 归一化查询(大小写不敏感兜底,仅用于 md 写回定位) */
public BlackEntry findEntryLoose(String playerName) {
if (playerName == null) return null;
BlackEntry exact = findEntry(playerName);
if (exact != null) return exact;
String want = AreaProtection.normName(playerName);
for (BlackEntry e : new ArrayList<>(entries.values())) {
if (AreaProtection.normName(e.name).equals(want)) return e;
}
return null;
}
/** 当前生效中的黑名单条目快照(供耻辱台播报) */
public List<BlackEntry> getActiveEntries() {
long now = System.currentTimeMillis();
List<BlackEntry> out = new ArrayList<>();
for (BlackEntry e : new ArrayList<>(entries.values())) {
if (e.isExpired(now)) {
entries.remove(e.name);
continue;
}
out.add(e);
}
return out;
}
public int size() {
return getActiveEntries().size();
}
// ==================== 增删(写回 md) ====================
/**
* 添加黑名单条目并写回 md。
*
* @param timeText 拉黑时长(空/0/终身=永久,日期或日期+时间=到期)
* @param reason 理由(可空)
* @return null=成功,否则为错误原因
*/
public String addBlacklist(String playerName, String timeText, String reason) {
if (playerName == null || playerName.trim().isEmpty()) {
return "玩家名不能为空";
}
String name = playerName.trim();
if (mdFile == null) return "黑名单文件未初始化";
List<String> lines = readLines();
if (lines == null) return "读取黑名单文件失败";
// 定位配置区域 + 表头
int[] zone = locateZone(lines);
if (zone == null) return "黑名单文件缺少配置区域标记";
int zoneStart = zone[0], zoneEnd = zone[1];
int headerIdx = locateHeader(lines, zoneStart, zoneEnd);
if (headerIdx < 0) return "黑名单文件缺少表头(| 玩家名字 | 拉黑时长 | 理由 |)";
// 去掉同名旧行(大小写不敏感)
String want = AreaProtection.normName(name);
List<String> cleaned = new ArrayList<>();
for (int i = 0; i < lines.size(); i++) {
if (i > zoneStart && i < zoneEnd && isRowOf(lines.get(i), want)) {
continue;
}
cleaned.add(lines.get(i));
}
lines = cleaned;
// 行数变了 → 重新定位
zone = locateZone(lines);
if (zone == null) return "黑名单文件缺少配置区域标记";
zoneStart = zone[0];
zoneEnd = zone[1];
headerIdx = locateHeader(lines, zoneStart, zoneEnd);
if (headerIdx < 0) return "黑名单文件缺少表头";
// 插入点 = 表头 + 分隔行之后
int insertAt = headerIdx + 1;
if (insertAt < zoneEnd && isSeparatorRow(lines.get(insertAt))) {
insertAt++;
}
String safeTime = (timeText == null || timeText.trim().isEmpty()) ? "终身" : timeText.trim();
String safeReason = (reason == null || reason.trim().isEmpty()) ? "违反服务器规则" : reason.trim();
// 理由里的竖线会破坏表格 → 替换
safeReason = safeReason.replace("|", "/").replace("\r", " ").replace("\n", " ");
lines.add(insertAt, "| " + name + " | " + safeTime + " | " + safeReason + " |");
if (!writeLines(lines)) return "写回黑名单文件失败";
reload();
return null;
}
/**
* 移除黑名单条目并写回 md。
*
* @return null=成功(或本来就没有),否则为错误原因
*/
public String removeBlacklist(String playerName) {
if (playerName == null || playerName.trim().isEmpty()) {
return "玩家名不能为空";
}
if (mdFile == null) return "黑名单文件未初始化";
List<String> lines = readLines();
if (lines == null) return "读取黑名单文件失败";
int[] zone = locateZone(lines);
if (zone == null) return "黑名单文件缺少配置区域标记";
int zoneStart = zone[0], zoneEnd = zone[1];
String want = AreaProtection.normName(playerName.trim());
List<String> out = new ArrayList<>();
boolean removed = false;
for (int i = 0; i < lines.size(); i++) {
if (i > zoneStart && i < zoneEnd && isRowOf(lines.get(i), want)) {
removed = true;
continue;
}
out.add(lines.get(i));
}
if (!removed) {
// 文件里没有 → 仍要解冻(可能只冻了账户没进表)
restoreBlacklist(playerName.trim());
return null;
}
if (!writeLines(out)) return "写回黑名单文件失败";
reload();
return null;
}
// ==================== 解析 ====================
/** 重新解析 md 并应用差异(新增→处罚,移除→恢复) */
public synchronized void reload() {
if (mdFile == null) return;
Map<String, BlackEntry> parsed = new LinkedHashMap<>();
long now = System.currentTimeMillis();
List<String> lines = readLines();
if (lines != null) {
int[] zone = locateZone(lines);
if (zone != null) {
int zoneStart = zone[0], zoneEnd = zone[1];
for (int i = zoneStart + 1; i < zoneEnd; i++) {
String line = lines.get(i).trim();
if (!line.startsWith("|")) continue;
if (isSeparatorRow(line)) continue;
String[] cells = splitRow(line);
if (cells.length < 1) continue;
String name = cells[0].trim();
if (name.isEmpty() || name.contains("玩家名字")) continue;
String timeText = cells.length > 1 ? cells[1].trim() : "";
String reason = cells.length > 2 ? cells[2].trim() : "";
long expires = parseExpireMs(timeText, now);
if (expires > 0 && expires <= now) continue; // 已过期
BlackEntry old = parsed.get(name);
long created = old != null ? old.createdMs : now;
parsed.put(name, new BlackEntry(name, timeText, reason, expires, created));
}
}
}
// ===== 差异应用 =====
// 1) 新增 → 施加处罚
for (BlackEntry e : parsed.values()) {
if (!entries.containsKey(e.name)) {
applyBlacklist(e);
}
}
// 2) 移除 → 恢复
for (BlackEntry e : new ArrayList<>(entries.values())) {
if (!parsed.containsKey(e.name)) {
restoreBlacklist(e.name);
}
}
entries.clear();
entries.putAll(parsed);
if (mdFile.exists()) {
lastMtime = mdFile.lastModified();
lastSize = mdFile.length();
}
plugin.getLogger().info("[黑名单] 已重载,当前生效 " + parsed.size() + " 人");
}
/**
* 拉黑时长 → 到期毫秒;0 = 永久。
* <p>空 / 0 / 终身 / 永久 / forever → 永久;
* {@code yyyy-MM-dd HH:mm[:ss]} → 该时刻;
* {@code yyyy-MM-dd} → 当天 23:59:59。
*/
public static long parseExpireMs(String timeText, long now) {
if (timeText == null) return 0L;
String t = timeText.trim();
if (t.isEmpty()) return 0L;
if ("0".equals(t) || "终身".equals(t) || "永久".equals(t)
|| "forever".equalsIgnoreCase(t) || "never".equalsIgnoreCase(t)) {
return 0L;
}
String[] pats = {
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm",
"yyyy/MM/dd HH:mm:ss",
"yyyy/MM/dd HH:mm"};
for (String p : pats) {
long v = parseStrict(p, t);
if (v > 0L) return v;
}
// 仅日期 → 当天 23:59:59
String[] datePats = {"yyyy-MM-dd", "yyyy/MM/dd"};
for (String p : datePats) {
long v = parseStrict(p, t);
if (v > 0L) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(v);
c.set(Calendar.HOUR_OF_DAY, 23);
c.set(Calendar.MINUTE, 59);
c.set(Calendar.SECOND, 59);
c.set(Calendar.MILLISECOND, 999);
return c.getTimeInMillis();
}
}
// 解析不了 → 永久(宁可拉黑也不放过)
return 0L;
}
private static long parseStrict(String pattern, String value) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.ROOT);
sdf.setLenient(false);
ParsePosition pos = new ParsePosition(0);
Date d = sdf.parse(value, pos);
if (d != null && pos.getIndex() == value.length()) return d.getTime();
} catch (Exception ignored) {}
return 0L;
}
// ==================== 处罚 / 恢复 ====================
/**
* 施加拉黑处罚:备份并清空背包 + 冻结债券账户 + 撤销领地管理员身份(降级访客)。
*/
public void applyBlacklist(BlackEntry e) {
if (e == null) return;
final String name = e.name;
if (!applied.add(name)) {
return; // 已施加过,不重复备份/清空
}
plugin.getLogger().info("[黑名单] 拉黑生效: " + name
+ "(" + e.timeText + " / " + e.reason + ")");
// 1) 冻结债券账户(按拉黑到期时间;永久=0)
try {
BondManager bm = plugin.getBonds();
if (bm != null) bm.freezeAccount(name, e.expiresMs);
} catch (Exception ex) {
plugin.getLogger().warning("[黑名单] 冻结债券失败 (" + name + "): " + ex.getMessage());
}
// 2) 撤销其在所有领地的管理员身份(降级访客)
try {
AreaProtection ap = plugin.getAreaProtection();
if (ap != null) {
int n = ap.demoteBlacklistAdmins(name);
if (n > 0) {
plugin.getLogger().info("[黑名单] 已撤销 " + name + " 的 " + n + " 个领地管理员身份");
}
}
} catch (Exception ex) {
plugin.getLogger().warning("[黑名单] 撤销领地权限失败 (" + name + "): " + ex.getMessage());
}
// 3) 在线玩家:备份并清空背包
Player p = Bukkit.getPlayerExact(name);
if (p == null || !p.isOnline()) return;
backupAndClear(p, name);
}
/**
* 解除拉黑:解冻债券 + 从最新自动备份恢复背包。
*/
public void restoreBlacklist(String playerName) {
if (playerName == null || playerName.isEmpty()) return;
if (!applied.remove(playerName)) {
// 未必处罚过,但仍确保解冻
try {
BondManager bm = plugin.getBonds();
if (bm != null) bm.unfreezeAccount(playerName);
} catch (Exception ignored) {}
return;
}
plugin.getLogger().info("[黑名单] 拉黑解除: " + playerName);
try {
BondManager bm = plugin.getBonds();
if (bm != null) bm.unfreezeAccount(playerName);
} catch (Exception ex) {
plugin.getLogger().warning("[黑名单] 解冻债券失败 (" + playerName + "): " + ex.getMessage());
}
Player p = Bukkit.getPlayerExact(playerName);
if (p == null || !p.isOnline()) return;
restoreInventory(p, playerName);
}
/**
* 备份并清空背包。
* <p>已有备份时不覆盖(保留拉黑时刻那一份),只清空。
*/
private void backupAndClear(final Player p, final String name) {
boolean hasBackup = false;
try {
hasBackup = plugin.getDb() != null
&& plugin.getDb().getLatestAutoBackup(name) != null;
} catch (Exception ignored) {}
if (!hasBackup) {
final ItemStack[] contents = p.getInventory().getContents();
final ItemStack[] armor = p.getInventory().getArmorContents();
final ItemStack[] extra = p.getInventory().getExtraContents();
final int level = p.getLevel();
final double exp = p.getExp();
final String c64, a64, e64;
try {
c64 = InventorySerializer.toBase64(contents);
a64 = InventorySerializer.toBase64(armor);
e64 = InventorySerializer.toBase64(extra);
} catch (IOException ex) {
plugin.getLogger().warning("[黑名单] 背包序列化失败 (" + name + "): " + ex.getMessage());
return;
}
// ★ 落库是阻塞 DB 写 → 异步;写完再回主线程清空(保证备份的是拉黑前快照)
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.getDb().saveAutoBackup(name, c64, a64, e64, level, exp);
} catch (Throwable t) {
plugin.getLogger().warning("[黑名单] 背包备份落库失败 (" + name + "): " + t.getMessage());
}
Bukkit.getScheduler().runTask(plugin, () -> {
Player cur = Bukkit.getPlayerExact(name);
if (cur == null || !cur.isOnline()) return;
clearInventory(cur);
cur.sendMessage("§c§l[罪人] §f你已被拉黑,背包已清空(已备份)");
cur.sendMessage("§7背包物品将在解除拉黑后自动返还");
});
});
return;
}
// 已有备份 → 只清空,不覆盖
Bukkit.getScheduler().runTask(plugin, () -> {
Player cur = Bukkit.getPlayerExact(name);
if (cur == null || !cur.isOnline()) return;
clearInventory(cur);
cur.sendMessage("§c§l[罪人] §f你已被拉黑,背包已清空");
});
}
/** 从最新自动备份恢复背包 */
private void restoreInventory(Player p, String name) {
String[] data;
try {
data = plugin.getDb() == null ? null : plugin.getDb().getLatestAutoBackup(name);
} catch (Exception ex) {
plugin.getLogger().warning("[黑名单] 读取背包备份失败 (" + name + "): " + ex.getMessage());
return;
}
if (data == null) {
p.sendMessage("§e§l[罪人] §f拉黑已解除,但没有找到可返还的背包备份");
return;
}
try {
ItemStack[] contents = InventorySerializer.fromBase64(data[0]);
ItemStack[] armor = InventorySerializer.fromBase64(data[1]);
ItemStack[] extra = InventorySerializer.fromBase64(data[2]);
p.getInventory().setContents(contents);
p.getInventory().setArmorContents(armor);
p.getInventory().setExtraContents(extra);
if (data.length > 3) {
try { p.setLevel(Integer.parseInt(data[3])); } catch (Exception ignored) {}
}
if (data.length > 4) {
try { p.setExp((float) Double.parseDouble(data[4])); } catch (Exception ignored) {}
}
p.sendMessage("§a§l[罪人] §f拉黑已解除,背包已返还");
} catch (IOException ex) {
plugin.getLogger().warning("[黑名单] 背包反序列化失败 (" + name + "): " + ex.getMessage());
p.sendMessage("§e§l[罪人] §f拉黑已解除,但背包返还失败,请联系管理员");
}
}
private void clearInventory(Player p) {
p.getInventory().clear();
p.getInventory().setArmorContents(null);
p.getInventory().setExtraContents(null);
p.setLevel(0);
p.setExp(0);
}
// ==================== md 读写工具 ====================
/** 读全部行(去掉行尾 \r) */
private List<String> readLines() {
if (mdFile == null || !mdFile.exists()) return null;
List<String> out = new ArrayList<>();
try {
byte[] raw = Files.readAllBytes(mdFile.toPath());
crlfStyle = new String(raw, StandardCharsets.UTF_8).contains("\r\n");
try (BufferedReader br = new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(raw), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
out.add(line);
}
}
} catch (IOException e) {
plugin.getLogger().warning("[黑名单] 读取失败: " + e.getMessage());
return null;
}
return out;
}
/** 写回全部行(保持原行尾风格) */
private boolean writeLines(List<String> lines) {
if (mdFile == null) return false;
String sep = crlfStyle ? "\r\n" : "\n";
StringBuilder sb = new StringBuilder();
for (String l : lines) {
sb.append(l).append(sep);
}
try {
File parent = mdFile.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
Files.write(mdFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
return true;
} catch (IOException e) {
plugin.getLogger().warning("[黑名单] 写入失败: " + e.getMessage());
return false;
}
}
/** 定位配置区域,返回 {起始行, 结束行};找不到返回 null */
private int[] locateZone(List<String> lines) {
int zoneStart = -1, zoneEnd = -1;
for (int i = 0; i < lines.size(); i++) {
String t = lines.get(i).trim();
if (t.startsWith(ZONE_START)) {
zoneStart = i;
} else if (t.startsWith(ZONE_END) && zoneStart >= 0) {
zoneEnd = i;
break;
}
}
if (zoneStart < 0 || zoneEnd < 0) return null;
return new int[]{zoneStart, zoneEnd};
}
/** 定位表头行索引;找不到返回 -1 */
private int locateHeader(List<String> lines, int zoneStart, int zoneEnd) {
for (int i = zoneStart + 1; i < zoneEnd; i++) {
String t = lines.get(i).trim();
if (t.startsWith("|") && t.contains("玩家名字")) {
return i;
}
}
return -1;
}
/** 表格行是否属于该玩家(第一列匹配,大小写不敏感) */
private boolean isRowOf(String line, String wantNorm) {
String[] cells = splitRow(line.trim());
if (cells.length < 1) return false;
String c0 = cells[0].trim();
if (c0.isEmpty() || c0.contains("玩家名字")) return false;
return AreaProtection.normName(c0).equals(wantNorm);
}
/** 分隔行判断(| --- | --- |) */
private boolean isSeparatorRow(String line) {
String t = line.trim();
if (!t.startsWith("|")) return false;
String[] cells = splitRow(t);
for (String c : cells) {
String v = c.trim();
if (!v.isEmpty() && !v.startsWith("---") && !v.matches("[-:]+")) return false;
}
return true;
}
/** 拆表格行(去掉首尾竖线,按 | 切分) */
private String[] splitRow(String line) {
String t = line.trim();
if (t.startsWith("|")) t = t.substring(1);
if (t.endsWith("|")) t = t.substring(0, t.length() - 1);
return t.split("\\|", -1);
}
}
+5
View File
@@ -74,6 +74,11 @@ 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";
@@ -57,6 +57,12 @@ public class ConfigManager {
public int garbageInterval = 300;
public int garbageMaxRounds = 1;
/** ★ 第30批:垃圾站启发式拦截(双击Q抢救窗口 + 倒计时段位提示)。默认开启。 */
public boolean garbageHeuristic = true;
// ===== 区域防护配置 =====
public String areaProtectAdminMode = "tag"; // "tag" 或 "op",不共存
public String areaProtectAdminTag = "admin"; // 管理员Tag名
@@ -439,6 +445,14 @@ public class ConfigManager {
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
garbageHeuristic = Boolean.parseBoolean(m.getOrDefault("垃圾站_启发式拦截", "true"));
if (!garbageHeuristic) {
Bukkit.getLogger().warning("[Sdf1配置] 垃圾站启发式拦截已关闭:不再做倒计时段位提示与双击Q抢救拦截。");
}
checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0);
checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0);
checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0);
@@ -531,6 +545,10 @@ public class ConfigManager {
managed.put("垃圾站_启用", String.valueOf(garbageEnabled));
managed.put("垃圾站_清理间隔秒", String.valueOf(garbageInterval));
managed.put("垃圾站_保留轮数", String.valueOf(garbageMaxRounds));
managed.put("垃圾站_启发式拦截", String.valueOf(garbageHeuristic));
managed.put("签到固定金额", String.valueOf(checkinRewardFixed));
managed.put("签到最小金额", String.valueOf(checkinRewardMin));
managed.put("签到最大金额", String.valueOf(checkinRewardMax));
@@ -619,6 +637,10 @@ public class ConfigManager {
L.add("垃圾站_启用=true");
L.add("垃圾站_清理间隔秒=300");
L.add("垃圾站_保留轮数=1");
L.add("垃圾站_启发式拦截=true");
L.add("签到固定金额=0");
L.add("签到最小金额=50");
L.add("签到最大金额=500");
@@ -2171,6 +2171,44 @@ public class DatabaseManager {
}
/**
* 获取玩家最新一份自动背包备份(黑名单解除时返还用)。
* @return String[]{contents_b64, armor_b64, extra_b64, level, experience},无备份返回 null
*/
public String[] getLatestAutoBackup(String playerName) {
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT contents_b64, "
+ "armor_b64, "
+ "extra_b64, "
+ "level, "
+ "experience "
+ "FROM inv_backup_auto "
+ "WHERE player_name=? "
+ "ORDER BY save_time DESC "
+ "LIMIT 1");
ps.setString(1, playerName);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String[] result = new String[5];
result[0] = rs.getString("contents_b64");
result[1] = rs.getString("armor_b64");
result[2] = rs.getString("extra_b64");
result[3] = String.valueOf(rs.getInt("level"));
result[4] = String.valueOf(rs.getDouble("experience"));
rs.close();
ps.close();
return result;
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取玩家最新背包备份
*/
+200 -3
View File
@@ -9,6 +9,14 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.scheduler.BukkitRunnable;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.io.*;
import java.nio.charset.StandardCharsets;
@@ -17,7 +25,7 @@ import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.*;
public class GarbageManager {
public class GarbageManager implements Listener {
private final Main plugin;
private Connection db;
@@ -28,6 +36,16 @@ public class GarbageManager {
private final Map<String, Integer>
recyclePages = new HashMap<>();
/** ★ 第30批:垃圾站倒计时(秒)。原为 startAutoCleanup 内部局部变量,
* 抢救模式需要外部重置,故提为字段。 */
private int countdown = 0;
/** ★ 第30批:顶部血条(玩家 UUID → BossBar) */
private final Map<UUID, BossBar> bossBars = new HashMap<>();
/** ★ 第30批:双击 Q 确认丢弃的首次按键时间(玩家 UUID → 毫秒) */
private final Map<UUID, Long> pendingDrops = new HashMap<>();
public GarbageManager(Main plugin) {
this.plugin = plugin;
@@ -133,8 +151,6 @@ public class GarbageManager {
public void startAutoCleanup() {
if (!enabled) return;
new BukkitRunnable() {
int countdown = 0;
@Override
public void run() {
// 在线人数≤0时暂停
@@ -148,6 +164,9 @@ public class GarbageManager {
cleanupInterval =
plugin.getConfig2()
.garbageInterval;
boolean heuristic =
plugin.getConfig2()
.garbageHeuristic;
int remain =
cleanupInterval - countdown;
@@ -169,10 +188,183 @@ public class GarbageManager {
return;
}
countdown++;
// ★ 第30批:顶部血条常驻倒计时(软提醒)
// 用 increment 前的 remain,与广播口径完全一致(避免差 1 秒)
updateBossBars(remain, heuristic);
}
}.runTaskTimer(plugin, 20L, 20L);
}
/**
* ★ 第30批:顶部血条常驻倒计时 + 色段软提醒。
*
* <p>色段按「已过时间」elapsed = interval - remain 划分(默认 5 分钟一轮):
* <ul>
* <li>前 60%(剩 3 分钟以上)→ 浅绿 = 安全期</li>
* <li>60% ~ 90%(剩 3 分 01 秒 ~ 1 分 30 秒)→ 黄色 = 警告「垃圾箱快动作了,别丢了」</li>
* <li>最后 10%(剩 30 秒内)→ 红色 = 危险</li>
* </ul>
*
* <p>启发式开关(垃圾站_启发式拦截)关闭时:撤掉血条、不提示、不拦截。
*/
private void updateBossBars(int remain, boolean heuristic) {
if (!heuristic) {
hideAllBossBars();
return;
}
int interval = Math.max(1, cleanupInterval);
double progress = (double) remain / (double) interval;
if (progress < 0.0D) progress = 0.0D;
if (progress > 1.0D) progress = 1.0D;
int elapsed = interval - remain;
BossBar.Color color;
String label;
if (elapsed < (int) (interval * 0.6D)) {
color = BossBar.Color.GREEN;
label = "§a§l安全期 §7| §a还早,放心丢";
} else if (elapsed < (int) (interval * 0.9D)) {
color = BossBar.Color.YELLOW;
label = "§e§l注意 §7| §e垃圾箱快动作了,别丢了";
} else {
color = BossBar.Color.RED;
label = "§c§l危险 §7| §c马上清理,抓紧捡";
}
String title = "§6§l垃圾站 §8| " + label
+ " §8| §f剩余 " + fmtRemain(remain);
Component comp = LegacyComponentSerializer
.legacySection().deserialize(title);
for (Player p : Bukkit.getOnlinePlayers()) {
UUID uuid = p.getUniqueId();
BossBar bar = bossBars.get(uuid);
if (bar == null) {
// Adventure 的 BossBar 按观众分发,必须每人一份
bar = BossBar.bossBar(comp, (float) progress,
color, BossBar.Overlay.NOTCHED_10);
bossBars.put(uuid, bar);
p.showBossBar(bar);
} else {
bar.name(comp);
bar.color(color);
if (Math.abs(bar.progress() - progress) > 0.001D) {
bar.progress((float) progress);
}
}
}
}
/** 撤掉所有玩家的顶部血条(启发式关闭时用) */
private void hideAllBossBars() {
for (Player p : Bukkit.getOnlinePlayers()) {
BossBar bar = bossBars.get(p.getUniqueId());
if (bar != null) {
try {
p.hideBossBar(bar);
} catch (Throwable ignored) {
}
}
}
bossBars.clear();
}
/** 剩余秒数格式化为 mm:ss */
private String fmtRemain(int remain) {
if (remain < 0) remain = 0;
int m = remain / 60;
int s = remain % 60;
return (m < 10 ? "0" + m : String.valueOf(m))
+ ":" + (s < 10 ? "0" + s : String.valueOf(s));
}
/**
* ★ 第30批:抢救模式 —— 跳过本轮清理并重新开始计时。
* 玩家物品被错误投入垃圾箱时可急停抢救。
*
* @return true=抢救成功(本轮清理已取消)
*/
public boolean rescueRound(Player p) {
if (!enabled) {
if (p != null)
p.sendMessage("§c§l[垃圾站] §f垃圾站未启用,无法抢救");
return false;
}
if (countdown <= 0) {
if (p != null)
p.sendMessage("§c§l[垃圾站] §f本轮清理已经结束了,来不及抢救");
return false;
}
countdown = 0;
pendingDrops.clear();
String who = p != null ? p.getName() : "控制台";
broadcast("§a§l[垃圾站] §f玩家 §e" + who
+ " §f发起抢救,本轮清理已取消,计时重新开始");
if (p != null)
p.sendMessage("§a§l[垃圾站] §f抢救成功!本轮清理已取消,"
+ "倒计时重新开始");
return true;
}
/** 当前剩余秒数(指令/UI 显示用) */
public int getRemainSeconds() {
int r = cleanupInterval - countdown;
return r < 0 ? 0 : r;
}
/**
* ★ 第30批:垃圾箱丢弃拦截(仅活跃于本轮最后 10 秒窗口)。
*
* <p>最后 30 秒(红区):对丢弃物品的玩家发小标题警告。
* <p>最后 10 秒(拦截窗口):单击 Q 被拦截,需 2 秒内再按一次 Q 确认才放行;
* 其他时段完全不干涉。
*/
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent e) {
if (!enabled) return;
if (!plugin.getConfig2().garbageHeuristic) return;
int remain = cleanupInterval - countdown;
if (remain > 30) return; // 不在红区 → 不干涉
Player p = e.getPlayer();
// 小标题警告(红区)
p.sendTitle("", "§c§l垃圾站警告 §f马上就清理了,别乱丢!",
3, 30, 5);
if (remain > 10) return; // 拦截仅在最后 10 秒窗口
long now = System.currentTimeMillis();
UUID uuid = p.getUniqueId();
Long first = pendingDrops.get(uuid);
if (first != null && now - first <= 2000L) {
pendingDrops.remove(uuid);
return; // 双击确认 → 放行
}
pendingDrops.put(uuid, now);
e.setCancelled(true);
p.sendMessage("§e§l[垃圾站] §f马上就清理了!再按一次 §eQ§f 确认丢弃,"
+ "或用 §e/垃圾抢救§f 取消本轮清理");
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) {
if (!plugin.getConfig2().garbageHeuristic) return;
// 下一轮 tick 会自动建血条,这里只清掉可能残留的按键状态
pendingDrops.remove(e.getPlayer().getUniqueId());
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
UUID uuid = e.getPlayer().getUniqueId();
pendingDrops.remove(uuid);
BossBar bar = bossBars.remove(uuid);
if (bar != null) {
try {
e.getPlayer().hideBossBar(bar);
} catch (Throwable ignored) {
}
}
}
private void doCleanup() {
// ★ 过期轮次删除是阻塞 DB -> 先异步执行;
// 轮次递增 / 实体收集 / 广播 必须主线程 -> 异步完成后回主线程
@@ -622,6 +814,11 @@ public class GarbageManager {
}
public void close() {
try {
hideAllBossBars();
pendingDrops.clear();
} catch (Exception ignored) {
}
try {
if (db != null && !db.isClosed())
db.close();
@@ -45,6 +45,12 @@ public class InventoryAutoBackup {
private void backupPlayer(Player p) {
String name = p.getName();
// ★ 第30批:黑名单玩家跳过自动备份
// (拉黑时的背包快照由 BlacklistManager 单独落库,不能被自动备份覆盖)
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(name)) {
return;
}
// ★ 跳过 PVP 竞技场:竞技场内不备份,避免 PVP 装备覆盖真实背包
if (plugin.getPVPArenaManager().isInPVPArena(name)) {
return;
+113 -3
View File
@@ -195,6 +195,8 @@ public class Main extends JavaPlugin
public ChestCarryManager chestCarryManager;
/** 26批:/json JSON消息构造器(tellraw/大标题/小标题 + 颜色 + 报错接管) */
public JsonMessageGui jsonGui;
/** ★ 第30批:群控黑名单(软折磨) */
public BlacklistManager blacklist;
private TeleportManager teleportMgr;
private HomeManager homeMgr;
private DeathReport deathReport;
@@ -353,6 +355,17 @@ public class Main extends JavaPlugin
return garbage;
}
/** ★ 第30批:黑名单管理器 */
public BlacklistManager getBlacklist() {
return blacklist;
}
/** ★ 第30批:判断玩家是否被拉黑(未初始化时返回 false) */
public boolean isBlacklisted(Player p) {
if (blacklist == null || p == null) return false;
return blacklist.isBlacklisted(p.getName());
}
public Set<String> getLoggedIn() {
return loggedIn;
}
@@ -490,8 +503,12 @@ public class Main extends JavaPlugin
// ===== 6. 垃圾箱 =====
garbage = new GarbageManager(this);
garbage.init();
garbage.loadConfig();
garbage.startAutoCleanup();
// ★ 第30批:垃圾站事件(顶部血条 / 双击Q抢救拦截)
getServer().getPluginManager()
.registerEvents(garbage, this);
// ===== 7. Radio =====
radio = new RadioManager(this);
@@ -740,7 +757,11 @@ public class Main extends JavaPlugin
// 15.1.1 ====MC官方中文翻译加载(异步下载 zh_cn.json)====
OfficialTranslations.init(this);
// 15.2 ====背包滚动自动备份(每小时 0/15/30/45 分准点)====
// ★ 第30批:群控黑名单(依赖 bondManager + areaProtection,必须在其后初始化)
blacklist = new BlacklistManager(this);
blacklist.init();
// 15.2 ====背包滚动自动备份(每小时 0/15/30/45 分准点)====
inventoryAutoBackup = new InventoryAutoBackup(this);
// ★ 第26批:改准点备份。原 runTaskTimer 从【启动时刻】起算 15 分钟周期,
// 落点永远带启动偏移(如 10:07/10:22/10:37…)。改为墙钟自对齐、每轮
@@ -3833,6 +3854,12 @@ public class Main extends JavaPlugin
e.setCancelled(true);
return;
}
// ★ 第30批:拉黑玩家禁止破坏方块
if (isBlacklisted(p)) {
e.setCancelled(true);
p.sendMessage("§c§l[罪人] §f你已被拉黑,禁止破坏方块");
return;
}
afk.recordAction(p.getUniqueId());
// [ADDED] 任务计数
if (questTracker != null)
@@ -3845,6 +3872,12 @@ public class Main extends JavaPlugin
public void onBlockPlace2(BlockPlaceEvent e) {
Player p = e.getPlayer();
if (isFrozen(p)) return;
// ★ 第30批:拉黑玩家禁止放置方块
if (isBlacklisted(p)) {
e.setCancelled(true);
p.sendMessage("§c§l[罪人] §f你已被拉黑,禁止放置方块");
return;
}
afk.recordAction(p.getUniqueId());
if (questTracker != null)
questTracker.onBlockPlaced(
@@ -3975,6 +4008,12 @@ public class Main extends JavaPlugin
e.setCancelled(true);
return;
}
// ★ 第30批:拉黑玩家禁止放置方块(兜底)
if (isBlacklisted(e.getPlayer())) {
e.setCancelled(true);
e.getPlayer().sendMessage("§c§l[罪人] §f你已被拉黑,禁止放置方块");
return;
}
}
// ★ 未登录(冻结)玩家只允许「与注册登录直接相关」的命令(含中文版):
@@ -6190,13 +6229,20 @@ public class Main extends JavaPlugin
}
// ★ 用户组聊天前缀:检查玩家所属组,在名字前加 [组名] 前缀(颜色由组配置决定)
String chatPrefix = "";
if (userGroupManager != null) {
UserGroupManager.UserGroupConfig highestGroup = userGroupManager.getHighestGroup(p.getName());
if (highestGroup != null) {
String prefix = highestGroup.displayColor + "[" + highestGroup.name + "]§f";
e.setFormat(prefix + "%1$s: %2$s");
chatPrefix = highestGroup.displayColor + "[" + highestGroup.name + "]§f";
}
}
// ★ 第30批:拉黑玩家消息末尾加「----我是罪人user」称号
if (isBlacklisted(p)) {
chatPrefix = chatPrefix + "----我是罪人" + p.getName() + "§f ";
}
if (!chatPrefix.isEmpty()) {
e.setFormat(chatPrefix + "%1$s: %2$s");
}
// ===== 聊天过滤 - 开关检查 =====
if (!chatFilter.isEnabled()) {
@@ -9195,6 +9241,70 @@ public class Main extends JavaPlugin
}
// ★ 第30批:/addblack <玩家> [时长] [理由] —— 拉黑
if (cmdName.equals("addblack")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (args.length < 1) {
sender.sendMessage("§c用法: /addblack <玩家名> [时长] [理由]");
sender.sendMessage("§7时长: 空/0/终身=永久,2026-10-21 18:00=到点,2026-10-21=当天23:59:59");
return true;
}
if (blacklist == null) {
sender.sendMessage("§c黑名单模块未初始化");
return true;
}
String bTime = args.length >= 2 ? args[1] : "";
String bReason = args.length >= 3 ? String.join(" ",
java.util.Arrays.copyOfRange(args, 2, args.length)) : "";
String err = blacklist.addBlacklist(args[0], bTime, bReason);
if (err == null) {
sender.sendMessage("§a§l[黑名单] §f已拉黑 §e" + args[0]);
} else {
sender.sendMessage("§c§l[黑名单] §f添加失败: §e" + err);
}
return true;
}
// ★ 第30批:/removeblack <玩家> —— 解除拉黑
if (cmdName.equals("removeblack")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (args.length < 1) {
sender.sendMessage("§c用法: /removeblack <玩家名>");
return true;
}
if (blacklist == null) {
sender.sendMessage("§c黑名单模块未初始化");
return true;
}
String err = blacklist.removeBlacklist(args[0]);
if (err == null) {
sender.sendMessage("§a§l[黑名单] §f已解除 §e" + args[0] + " §f的拉黑");
} else {
sender.sendMessage("§c§l[黑名单] §f解除失败: §e" + err);
}
return true;
}
// ★ 第30批:/垃圾抢救 —— 紧急取消本轮垃圾站清理
if (cmdName.equals("垃圾抢救")) {
if (!isAdmin(sender) && !(sender instanceof Player)) {
sender.sendMessage("§c权限不足");
return true;
}
if (garbage == null) {
sender.sendMessage("§c垃圾站模块未初始化");
return true;
}
garbage.rescueRound(sender instanceof Player ? (Player) sender : null);
return true;
}
// /垃圾清理
if (cmdName.equals("垃圾清理")) {
if (!isAdmin(sender)) {
+11
View File
@@ -8583,6 +8583,12 @@ public class WebManager {
sender.sendMessage("§c[Web] Web通信未启用");
return;
}
// ★ 第30批:拉黑玩家禁止访问 PHP 后端
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(sender.getName())) {
sender.sendMessage("§c§l[罪人] §f你已被拉黑,禁止使用网页端功能");
return;
}
String playerName = sender.getName();
long now = System.currentTimeMillis();
@@ -8678,6 +8684,11 @@ public class WebManager {
*/
public void autoGenerateWebLoginToken(Player player) {
if (!enabled) return;
// ★ 第30批:拉黑玩家禁止访问 PHP 后端(不签发 token)
if (plugin.getBlacklist() != null
&& plugin.getBlacklist().isBlacklisted(player.getName())) {
return;
}
String playerName = player.getName();
// 生成weblogin专用Token
+30
View File
@@ -0,0 +1,30 @@
### ----配置区域开始----
| 玩家名字 | 拉黑时长 | 理由 |
| --- | --- | --- |
### ----配置区域结束
## 软折磨黑名单配置
功能说明:<br>
此功能为软折磨,用于折磨违反规则的玩家。通过`/addblack user time(可选) msg(可选)`添加违规玩家或直接编辑本文件即可。被拉黑的玩家不能消费债券、不能登录PHP后端、不能创建领地,不能成为他人领地的授权玩家。不能破坏、放置方块,没有背包(清空前备份,自动备份跳过该玩家)。能说话但,会被系统加上"---我是罪人user"的签名
## 示例配置:
| 玩家名字 | 拉黑时长 | 理由 |
| --- | --- | --- |
| steve | 终身 | 违反服务器规则 |
| Dream | 2026-10-20 14:30 | 随意殴打他人 |
## 指令集
| 前缀 | 参数1 | 参数2 | 参数3 |
| --- | --- | ---| --- |
| /addblack | user | time | msg |
| /removeblack | user | 空 | 空 |
| /addblack | steve | 2026-10-20 10:00 | 违反服务器规则 |
| /addblack | Dream | 0 | 空 |
| /removeblack | Notch | 空 | 空 |
- 备注:0表示永久拉黑,填入日期+时间则表示遵循到期时间。填入日期则表示以当天23:59:59为界限<br>
- 生效规则: 定时,每分钟的第0秒、5秒、10秒...以此类推、55秒定时增量重载,删了5秒后立即恢复背包和各项权限,加了5秒后立即生效
+7
View File
@@ -81,6 +81,13 @@ commands:
垃圾清理:
description: 手动清理掉落物
usage: /垃圾清理
垃圾抢救:
description: 紧急取消本轮垃圾站清理(物品误投时抢救)
usage: /垃圾抢救
addblack:/r/n description: 拉黑玩家(禁止建领地/破坏/兑换码/网页端,债券冻结,背包备份清空)
usage: /addblack <玩家名> [时长] [理由]
removeblack:/r/n description: 解除拉黑
usage: /removeblack <玩家名>
# menu:
# description: 雪球菜单开关控制
# usage: /menu <开/关>