第30批:群控黑名单(软折磨)+ 垃圾箱抢救模式 + 顶部血条软提醒
【一、群控黑名单 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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家最新背包备份
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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秒后立即生效
|
||||
@@ -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 <开/关>
|
||||
|
||||
Reference in New Issue
Block a user