- 垃圾箱保留轮数: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 模板补相对时长格式说明
770 lines
31 KiB
Java
770 lines
31 KiB
Java
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();
|
||
}
|
||
// ★ 第33批(#213):文件没动也要扫 wall-clock 到期
|
||
// (原实现只比 mtime+size,时长拉黑永远不自动解除)
|
||
sweepExpired();
|
||
} 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));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== 差异应用 =====
|
||
boolean changed = parsed.size() != entries.size();
|
||
// 1) 新增 → 施加处罚
|
||
for (BlackEntry e : parsed.values()) {
|
||
if (!entries.containsKey(e.name)) {
|
||
applyBlacklist(e);
|
||
changed = true;
|
||
} else {
|
||
BlackEntry old = entries.get(e.name);
|
||
if (old == null || old.expiresMs != e.expiresMs
|
||
|| !old.timeText.equals(e.timeText)
|
||
|| !old.reason.equals(e.reason)) {
|
||
changed = true; // 条目内容改了(理由/时长)
|
||
}
|
||
}
|
||
}
|
||
// 2) 移除 → 恢复
|
||
for (BlackEntry e : new ArrayList<>(entries.values())) {
|
||
if (!parsed.containsKey(e.name)) {
|
||
restoreBlacklist(e.name);
|
||
changed = true;
|
||
}
|
||
}
|
||
entries.clear();
|
||
entries.putAll(parsed);
|
||
|
||
if (mdFile.exists()) {
|
||
lastMtime = mdFile.lastModified();
|
||
lastSize = mdFile.length();
|
||
}
|
||
plugin.getLogger().info("[黑名单] 已重载,当前生效 " + parsed.size() + " 人");
|
||
// ★ 第32批(#208):名单变化 → 立即回响PHP(异步;syncBlacklist 内部 hash 门控兜底)
|
||
if (changed) {
|
||
try {
|
||
WebManager wm = plugin.webManager;
|
||
if (wm != null) wm.forceSyncBlacklist();
|
||
} catch (Exception ignored) {}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ★ 第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 → 永久;
|
||
* {@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();
|
||
}
|
||
}
|
||
// ★ 第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;
|
||
}
|
||
|
||
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 + ")");
|
||
|
||
// ★ 第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();
|
||
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);
|
||
}
|
||
}
|