diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index 57535e9..b4161e2 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -1,14018 +1,14264 @@ -package Sdf1_login; - -import org.bukkit.*; -import org.bukkit.block.Block; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.*; -import org.bukkit.event.player.*; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.block.BlockSpreadEvent; -import org.bukkit.event.block.SignChangeEvent; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Animals; -import org.bukkit.event.player.PlayerInteractAtEntityEvent; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.block.BlockState; -import org.bukkit.block.Container; -import java.sql.*; -import org.bukkit.GameMode; -import org.bukkit.event.EventPriority; - - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import org.bukkit.Bukkit; -import java.util.Iterator; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEvent; -import java.util.Set; -import java.util.HashSet; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.event.entity.EntitySpawnEvent; -import com.destroystokyo.paper.event.entity.EntityPathfindEvent; -import org.bukkit.entity.Item; -import org.bukkit.event.inventory.InventoryOpenEvent; - -public class AreaProtection implements Listener { - - // 三级权限枚举 - public enum PermissionLevel { - ADMIN, // 管理员:可以做所有事 - OWNER, // 领地所有者:可以管理自己的领地 - MEMBER, // 已授权成员(DB role='member'):不受领地全局默认权限限制,可被per-player单独收紧 - VISITOR // 访客:只能在被授权的领地内活动 - } - - private final Main plugin; - public List giveEffects = new ArrayList<>(); - private final Map> playerAreas - = new HashMap<>(); - private final Map areas = - new ConcurrentHashMap<>(); - private Connection dbConnection; - // ★ 防止"无法识别效果"刷屏:同一名称只警告一次 - private static final Set UNKNOWN_EFFECT_WARNINGS = ConcurrentHashMap.newKeySet(); - - // 全局白名单(玩家) - private final Set globalPlayerWhitelist = - ConcurrentHashMap.newKeySet(); - // 区域白名单 key=区域名 value=玩家名集合 - private final Map> areaPlayerWhitelist = - new ConcurrentHashMap<>(); - // 全局黑名单(物品ID) - private final Set globalItemBlacklist = - ConcurrentHashMap.newKeySet(); - // 区域黑名单 - private final Map> areaItemBlacklist = - new ConcurrentHashMap<>(); - - // ★ 领地删除延迟队列(1分钟冷却) - private static class PendingDelete { - final String areaName; - final String playerName; - final long startTime; - final BukkitTask task; - PendingDelete(String areaName, String playerName, long startTime, BukkitTask task) { - this.areaName = areaName; - this.playerName = playerName; - this.startTime = startTime; - this.task = task; - } - } - private final Map pendingDeletes = new ConcurrentHashMap<>(); - // ★ 圈地工具冷却:UUID → 上次获取时间(package-private供AreaGUIManager访问) - final Map wandCooldownMap = new ConcurrentHashMap<>(); - // ★ 管理员配置输入等待:UUID → 配置key(GUI点击后等待玩家聊天输入) - private final Map pendingConfigInput = new ConcurrentHashMap<>(); - // ★ 添加成员输入等待:UUID → 领地名 - private final Map pendingAddMemberInput = new ConcurrentHashMap<>(); - // ★ 效果管理待处理输入:UUID → [landName, inputType] - public final Map pendingEffectInput = new ConcurrentHashMap<>(); - // ★ 违禁品没收暂存(2026-10-11 违禁品管理面板):UUID → (领地名 → 被没收物品) - // 玩家进入领地被没收时暂存;退出领地时若 returnOnLeave=true 则归还,否则清空(不退) - public final Map>> confiscatedStore - = new ConcurrentHashMap<>(); - // ★ 过户领地待输入新所有者:UUID → [landName] - public final Map pendingTransferInput = new ConcurrentHashMap<>(); - // ★ 用户组配置编辑待输入:UUID → [groupName, field("price"|"maxlands"|"priority")] - public final Map pendingGroupEditInput = new ConcurrentHashMap<>(); - // ★ 领地改名冷却:UUID → 改名时间戳列表(自然滚动窗口,1小时限5次) - private final Map> renameCooldownMap = new ConcurrentHashMap<>(); - private static final int RENAME_LIMIT = 5; - private static final long RENAME_WINDOW_MS = 3600_000L; // 1小时 - - // ★ 全局配置默认值 - private int globalCreatePricePerSqm = 10; // 每㎡创建价格 - private int globalMaxLandsPerPlayer = 5; // 每人最多领地数 - private int globalMaxHomePerPlayer = 5; // 每人最多传送点(home)数量 - private int globalDefaultHeight = 255; // 默认高度 - private int globalRefundRatioPerCent = 0; // 删除领地退费比例(0=关闭退费,100=全额退款,其他=原价×比例%) - // ★ 面积限制(2026-09-29,PHP update_config → config_change 下发): - // 单次扩建/创建超过 free 面积,或边长超过 side 上限 → 转管理员现场审批 - private int globalMaxFreeAreaSqm = 3500; // 单次扩建(含创建选区)免审面积上限(㎡) - private int globalMaxLandSideBlocks = 7000; // 单领地免审边长上限(X/Z 各向,格) - - // 选地 - private static final Material WAND = Material.BLAZE_ROD; - - private final Map pos1 = - new ConcurrentHashMap<>(); - private final Map pos2 = - new ConcurrentHashMap<>(); - private final Set selecting = - ConcurrentHashMap.newKeySet(); - - public boolean hasPos1(UUID uuid) { return pos1.containsKey(uuid); } - public boolean hasPos2(UUID uuid) { return pos2.containsKey(uuid); } - public boolean denyRaid = false; - // ===== 统一文件夹 ===== - private final File rootDir; - private final File whitelistDir; - private final File globalWhitelistFile; - private final File oldDbFile; - private final File newDbFile; - - // 边框显示 - private final Map displayTaskIds = - new ConcurrentHashMap<>(); - private final Map> globalWhitelist - = new HashMap<>(); - private final Map> areaWhitelist - = new HashMap<>(); - private final Map protectedEntities - = new ConcurrentHashMap<>(); - // 区域给予的效果记录(贴标) - private final Map> playerAppliedEffects = new HashMap<>(); - // 延时清理任务 - private static final long MOVE_DEBOUNCE_MS = 100; - // 新:全部改为线程安全 - private final Map playerCurrentArea - = new ConcurrentHashMap<>(); - private final Map> playerMarkedEffects - = new ConcurrentHashMap<>(); - private final Map pendingClearTask - = new ConcurrentHashMap<>(); - private final Map pendingClearArea - = new ConcurrentHashMap<>(); - private final Map lastMoveProcess - = new ConcurrentHashMap<>(); - // ★ denyMove 豁免诊断日志节流(每玩家10秒) - private final Map denyExemptLogTs = new ConcurrentHashMap<>(); - // 防止传送时重复处理 - private final Set teleporting - = ConcurrentHashMap.newKeySet(); - // 待清理的效果列表(与延时任务配合) - private final Map> pendingClearEffects - = new ConcurrentHashMap<>(); - // ★ 拾取消息节流(5~10秒随机间隔) - private final Map lastPickupMsgTime = new ConcurrentHashMap<>(); - // ★ 效果热更新节流(每2秒最多检查一次) - private final Map lastEffectHotUpdate = new ConcurrentHashMap<>(); - private static final long EFFECT_HOT_UPDATE_INTERVAL_MS = 2000; - private static final String[] PICKUP_MSGS = { - "§c§l[区域防护] §f禁止拾取物品", - "§c§l[区域防护] §f此区域不允许拾取", - "§c§l[区域防护] §f拾取功能已禁用", - "§c§l[区域防护] §f你无法在此区域拾取物品", - "§c§l[区域防护] §f请离开领地后再拾取" - }; - - - -// ==================== 强制游戏模式 ==================== - - /** - * 监听玩家尝试切换游戏模式 - * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 - */ - // 防止递归的标记 - private final Set modeSwitching - = ConcurrentHashMap.newKeySet(); - @EventHandler(priority = EventPriority.HIGHEST) - public void onGameModeChange( - PlayerGameModeChangeEvent event) { - if (event.isCancelled()) return; - - Player p = event.getPlayer(); - UUID uid = p.getUniqueId(); - - if (modeSwitching.contains(uid)) return; - - String areaName = playerCurrentArea.get(uid); - if (areaName == null) return; - - AreaConfig ac = areas.get(areaName); - if (ac == null) return; - if (ac.enforceGameMode == null - || ac.enforceGameMode.isEmpty()) return; - if (isExemptFromGameMode(p, areaName)) return; - - event.setCancelled(true); - - modeSwitching.add(uid); - try { - GameMode target = GameMode.valueOf( - ac.enforceGameMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - } - } finally { - modeSwitching.remove(uid); - } - - // ★ 切换后发一次中文提示 - p.sendMessage(formatAreaMsg( - "&c该区域强制 " + ac.enforceGameMode - + " 模式,无法切换")); - } - - public Set getAlreadyForced() { - return alreadyForced; - } - - - // ==================== 定时强制游戏模式 ==================== - - private BukkitTask enforceTask; - - /** - * 启动定时强制任务 - * 每秒检查一次所有在线玩家 - */ - private BukkitTask enforceTask1; - - // 记录上次强制模式的玩家(避免重复发消息) - private final Set alreadyForced - = ConcurrentHashMap.newKeySet(); - - public void startEnforceTask() { - if (enforceTask != null) { - enforceTask.cancel(); - } - enforceTask = new BukkitRunnable() { - @Override - public void run() { - for (Player p : Bukkit.getOnlinePlayers()) { - UUID uid = p.getUniqueId(); - String areaName = - playerCurrentArea.get(uid); - if (areaName == null) { - // 不在任何区域,清除标记 - alreadyForced.remove(uid); - continue; - } - - AreaConfig ac = areas.get(areaName); - if (ac == null) continue; - if (ac.enforceGameMode == null - || ac.enforceGameMode - .isEmpty()) continue; - if (isExemptFromGameMode( - p, areaName)) { - alreadyForced.remove(uid); - continue; - } - - GameMode target = - GameMode.valueOf( - ac.enforceGameMode); - if (p.getGameMode() != target) { - modeSwitching.add(uid); - try { - p.setGameMode(target); - } finally { - modeSwitching.remove(uid); - } - - // ★ 只在玩家已被强制过时才发消息 - // 首次不发,后续才提醒 - if (alreadyForced.contains(uid)) { - p.sendMessage(formatAreaMsg( - "&c该区域强制 " - + ac.enforceGameMode - + " 模式,无法切换")); - } - alreadyForced.add(uid); - } else { - // 模式一致,清除标记 - alreadyForced.remove(uid); - } - } - } - }.runTaskTimer(plugin, 20L, 20L); - } - - public void stopEnforceTask() { - if (enforceTask != null) { - enforceTask.cancel(); - enforceTask = null; - } - } - - - - /** - * 检查玩家是否豁免于游戏模式强制 - * 豁免条件:全局白名单 / 区域白名单 / 模式排除名单 - */ - private boolean isExemptFromGameMode( - Player p, String areaName) { - String name = p.getName(); - - // 管理员豁免 - if (isAreaAdmin(p)) { - return true; - } - - // ★ 白名单判定统一走 whiteContains(2026-10-10 起为精确匹配) - if (whiteContains(globalPlayerWhitelist, name)) { - return true; - } - Set aw = - areaPlayerWhitelist.get(areaName); - if (whiteContains(aw, name)) { - return true; - } - AreaConfig ac = areas.get(areaName); - if (ac != null && ac.modeExempt.contains(name)) { - return true; - } - // 领地所有者豁免 - if (ac != null && ac.owner != null && ac.owner.equals(name)) { - return true; - } - return false; - } - - /** - * 玩家进入区域时,如果该区域强制游戏模式, - * 则立即设置(排除豁免玩家) - */ - private void applyEnforcedGameMode(Player p, - String areaName) { - AreaConfig ac = areas.get(areaName); - if (ac == null) { - /* plugin.getLogger().info( - "[防护-调试] applyMode: ac=null, area=" - + areaName);*/ - return; - } - if (ac.enforceGameMode == null - || ac.enforceGameMode.isEmpty()) { - /* plugin.getLogger().info( - "[防护-调试] applyMode: 未配置强制模式, area=" - + areaName);*/ - return; - } - - boolean exempt = isExemptFromGameMode( - p, areaName); - /* plugin.getLogger().info( - "[防护-调试] applyMode: 玩家=" + p.getName() - + " 区域=" + areaName - + " 强制=" + ac.enforceGameMode - + " 当前=" + p.getGameMode() - + " 豁免=" + exempt - + " OP=" + p.isOp() - + " 全白=" + globalPlayerWhitelist - .contains(p.getName()));*/ - - if (exempt) return; - - try { - GameMode target = GameMode.valueOf( - ac.enforceGameMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - /* plugin.getLogger().info( - "[防护] 强制切换 " + p.getName() - + " → " + ac.enforceGameMode);*/ - } - } catch (Exception e) { - /* plugin.getLogger().warning( - "[防护] 强制模式失败: " - + e.getMessage());*/ - } - } - - public AreaProtection(Main plugin) { - this.plugin = plugin; - - // 统一根目录 - rootDir = new File(plugin.getDataFolder(), "区域防护"); - if (!rootDir.exists()) rootDir.mkdirs(); - - // 白名单子目录 - whitelistDir = new File(rootDir, "whitelists"); - if (!whitelistDir.exists()) whitelistDir.mkdirs(); - globalWhitelistFile = new File( - whitelistDir, "全局白名单.txt"); - - // 数据库迁移 - oldDbFile = new File( - plugin.getDataFolder(), "area_effects.db"); - newDbFile = new File(rootDir, "area_effects.db"); - migrateDatabase(); - - // 初始化 - initDatabase(); - writeDefaultConfig(); - migrateTxtToDb(); // ★ 迁移txt文件到数据库 - loadAllAreas(); - loadWhitelists(); - recoverPendingEffects(); - // ★ 封禁/注销领主的领地自动清理(每5分钟扫描) - startBannedOwnerSweepTask(); - } - public void recoverPendingEffects() { - try { - // 1. 遍历所有在线玩家(而不是遍历DB记录) - for (Player p : Bukkit.getOnlinePlayers()) { - UUID uid = p.getUniqueId(); - String playerName = p.getName(); - - // 2. 获取玩家当前所在区域 - AreaConfig currentAc = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - - // 3. 获取该玩家在DB中的所有历史记录 - List recordedAreas = - getPlayerAreas(uid); - - // 4. 清理不在当前区域的旧记录 - for (String recordedArea : recordedAreas) { - if (currentAc == null - || !currentAc.name - .equals(recordedArea)) { - // 玩家不在这个区域了 → 清DB - removePlayerEffects( - uid, recordedArea); - plugin.getLogger().info( - "[防护] 重连: " + playerName - + " 不在" + recordedArea - + ", 清理旧记录"); - } - } - - // 5. 如果在区域中 → 恢复/给予效果 - if (currentAc != null) { - plugin.getLogger().info( - "[防护] 重连: " + playerName - + " 在" + currentAc.name - + ", 恢复效果"); - applyRegionEffects(p, uid, - currentAc.name, currentAc); - } else { - plugin.getLogger().info( - "[防护] 重连: " + playerName - + " 不在任何区域, 跳过"); - } - } - } catch (Exception e) { - plugin.getLogger().warning( - "[防护] 恢复效果失败: " - + e.getMessage()); - } - } - - - @EventHandler - public void onQuit(PlayerQuitEvent event) { - Player p = event.getPlayer(); - UUID uid = p.getUniqueId(); - - // 取消延时清理任务 - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - pendingClearArea.remove(uid); - pendingClearEffects.remove(uid); - } - - // 清标记 - playerMarkedEffects.remove(uid); - playerCurrentArea.remove(uid); - } - - /** - * 玩家上线时恢复区域效果 - * 如果玩家在有效果的区域中,恢复药水效果 - */ - public void onPlayerJoin(Player p) { - if (p == null || !p.isOnline()) return; - UUID uid = p.getUniqueId(); - String playerName = p.getName(); - - // ★ 管理员上线提醒:有超限待审批提案(延迟60tick,等计分板标签恢复) - try { - org.bukkit.Bukkit.getScheduler().runTaskLater(plugin, () -> { - if (!p.isOnline() || !isAreaAdmin(p)) return; - pruneExpiredPending(); - if (pendingExpands.isEmpty()) return; - p.sendMessage("§6§l[领地审批] §f当前有 §e" + pendingExpands.size() - + " §f条面积超限待审批申请,用 §a/protect accept §f查看列表"); - }, 60L); - } catch (Exception ignored) {} - - // 获取玩家当前所在区域 - AreaConfig currentAc = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - - if (currentAc != null) { - plugin.getLogger().info( - "[防护] 上线: " + playerName - + " 在" + currentAc.name - + ", 恢复效果"); - applyRegionEffects(p, uid, - currentAc.name, currentAc); - } - } - - -// ==================== 数据库迁移 ==================== - - /** - * 如果旧位置有 db 文件,移到新位置 - * 如果新位置已有,不动 - * 如果旧位置也没有,在新位置创建 - */ - - private void migrateDatabase() { - if (newDbFile.exists()) { - if (oldDbFile.exists() - && !oldDbFile.equals(newDbFile)) { - oldDbFile.delete(); - } - return; - } - if (oldDbFile.exists() - && !oldDbFile.equals(newDbFile)) { - try { - java.nio.file.Files.move( - oldDbFile.toPath(), - newDbFile.toPath(), - java.nio.file.StandardCopyOption - .REPLACE_EXISTING); - } catch (Exception e) { - plugin.getLogger().warning( - "[防护] 迁移数据库失败: " - + e.getMessage()); - } - } - } - - - // 保存玩家进入区域时给予的效果 - private void savePlayerEffects(UUID uid, String playerName, - String areaName, List effects) { - try { - // 先删除该区域旧记录,避免重复 - PreparedStatement del = dbConnection.prepareStatement( - "DELETE FROM player_effects " - + "WHERE uuid = ? AND area_name = ?"); - del.setString(1, uid.toString()); - del.setString(2, areaName); - del.executeUpdate(); - del.close(); - - PreparedStatement ps = dbConnection.prepareStatement( - "INSERT INTO player_effects " - + "(uuid, player_name, area_name, " - + "effect_type, effect_level, " - + "effect_duration, enter_time) " - + "VALUES (?, ?, ?, ?, ?, ?, ?)"); - long now = System.currentTimeMillis(); - for (PotionEffectType type : effects) { - ps.setString(1, uid.toString()); - ps.setString(2, playerName); - ps.setString(3, areaName); - ps.setString(4, type.getName()); - ps.setInt(5, 0); - ps.setInt(6, 999); - ps.setLong(7, now); - ps.addBatch(); - } - ps.executeBatch(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 保存效果失败: " + e.getMessage()); - } - } - - // 查询玩家在某个区域获得的效果 - private List getPlayerEffects( - UUID uid, String areaName) { - List list = new ArrayList<>(); - try { - PreparedStatement ps = dbConnection.prepareStatement( - "SELECT effect_type FROM player_effects " - + "WHERE uuid = ? AND area_name = ?"); - ps.setString(1, uid.toString()); - ps.setString(2, areaName); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - PotionEffectType type = PotionEffectType - .getByName(rs.getString("effect_type")); - if (type != null) list.add(type); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 查询效果失败: " + e.getMessage()); - } - return list; - } - - // 删除玩家在某个区域的效果记录 - private void removePlayerEffects(UUID uid, String areaName) { - try { - PreparedStatement ps = dbConnection.prepareStatement( - "DELETE FROM player_effects " - + "WHERE uuid = ? AND area_name = ?"); - ps.setString(1, uid.toString()); - ps.setString(2, areaName); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 删除效果失败: " + e.getMessage()); - } - } - - // 删除玩家所有效果记录 - private void removeAllPlayerEffects(UUID uid) { - try { - PreparedStatement ps = dbConnection.prepareStatement( - "DELETE FROM player_effects " - + "WHERE uuid = ?"); - ps.setString(1, uid.toString()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 删除全部效果失败: " + e.getMessage()); - } - } - - // 获取玩家所有记录的区域 - private List getPlayerAreas(UUID uid) { - List list = new ArrayList<>(); - try { - PreparedStatement ps = dbConnection.prepareStatement( - "SELECT DISTINCT area_name " - + "FROM player_effects WHERE uuid = ?"); - ps.setString(1, uid.toString()); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - list.add(rs.getString("area_name")); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 查询区域失败: " + e.getMessage()); - } - return list; - } - - // 查询所有待处理的效果(跨服重启也能恢复) - private List getAllPendingEffects() { - List list = new ArrayList<>(); - try { - Statement stmt = dbConnection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT uuid, player_name, area_name, " - + "effect_type FROM player_effects"); - while (rs.next()) { - list.add(new String[]{ - rs.getString("uuid"), - rs.getString("player_name"), - rs.getString("area_name"), - rs.getString("effect_type")}); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 查询全部效果失败: " + e.getMessage()); - } - return list; - } - - - private void writeDefaultConfig() { - File f = new File(rootDir, "末地保护区.txt"); - if (f.exists()) return; - - String endWorld = "world_the_end"; - for (org.bukkit.World w : Bukkit.getWorlds()) { - if (w.getEnvironment() - == org.bukkit.World.Environment.THE_END) { - endWorld = w.getName(); - break; - } - } - - final String ew = endWorld; - try { - PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(f), - StandardCharsets.UTF_8)); - pw.println("# 末地保护区"); - pw.println("起点: " + ew + ",-30000000,-30000000"); - pw.println("终点: " + ew + ",30000000,30000000"); - pw.println("高度范围: 0-255"); - pw.println(); - pw.println("没收物品: BROWN_MUSHROOM,RED_MUSHROOM,MUSHROOM_STEW"); - pw.println("没收提示: 你携带了违禁品「蘑菇」,已被全部没收!"); - pw.println("通报批评: {player} 在末地违规携带蘑菇!"); - pw.println(); - pw.println("禁止放置方块"); - pw.println("禁止破坏方块"); - pw.println("禁止PVP"); - pw.println("禁止袭击"); - pw.println(); - pw.println("效果: 夜视 1 999"); - pw.println("进入提示: 末地保护区 - 请遵守规则"); - pw.println("离开提示: 已离开末地保护区"); - pw.close(); - plugin.getLogger().info("[防护] 生成末地配置, 世界: " + ew); - } catch (IOException ignored) { - } - } - - public void reload() { - loadAllAreas(); - loadWhitelists(); - // 强制输出醒目的中文日志 - plugin.getLogger().info("========================================"); - plugin.getLogger().info("[防护] 已重载 " + areas.size() + " 个区域"); - plugin.getLogger().info("[防护] 已重载白名单"); - plugin.getLogger().info("========================================"); - } - - /** - * ★ 从数据库重新加载area_config配置(Web同步用) - */ - public void reloadAreaConfigFromDb() { - if (dbConnection == null) return; - try { - ResultSet cfgRs = dbConnection.createStatement().executeQuery("SELECT key, value FROM area_config"); - while (cfgRs.next()) { - String key = cfgRs.getString("key"); - String value = cfgRs.getString("value"); - switch (key) { - case "create_price_per_sqm": /* 已在area_config表管理 */ break; - case "max_lands_per_player": break; - case "default_height": break; - case "peace_mode_max_duration": break; - } - } - cfgRs.close(); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 从DB重载配置失败: " + e.getMessage()); - } - } - -// ==================== 消息格式化 ==================== - - /** - * 格式化区域消息 - * 1. & → §(颜色符号转换) - * 2.
和 \n → 换行 - * 3. 内容 → §l内容§r(加粗) - * 4. 自动添加 [区域防护] 前缀 - */ - private String formatAreaMsg(String raw) { - return formatAreaMsg(raw, null, null); - } - - /** - * 格式化区域消息(支持变量替换) - * 支持括号: () [] {} 【】 <> 《》 - */ - private String formatAreaMsg(String raw, String playerName, String landName) { - if (raw == null || raw.isEmpty()) return ""; - - String msg = raw; - - // ★ 变量替换:支持多种括号格式(DOTALL确保跨行匹配) - // 支持 () [] {} 【】 <> 《》 六种括号 - if (playerName != null) { - java.util.regex.Pattern varPattern = java.util.regex.Pattern.compile( - "[((\\[【<《{]玩家[))\\]】>》}]", - java.util.regex.Pattern.DOTALL); - msg = varPattern.matcher(msg).replaceAll( - java.util.regex.Matcher.quoteReplacement(playerName)); - } - if (landName != null) { - // 同时支持"领地"和"领地名"两种写法 - java.util.regex.Pattern varPattern = java.util.regex.Pattern.compile( - "[((\\[【<《{]领地名?[))\\]】>》}]", - java.util.regex.Pattern.DOTALL); - msg = varPattern.matcher(msg).replaceAll( - java.util.regex.Matcher.quoteReplacement(landName)); - } - - // HTML实体解码(必须在&颜色代码之前,防止&a被误转为§amp;) - msg = decodeHtmlEntities(msg); - - // & → § - for (int i = 0; i <= 9; i++) { - msg = msg.replace("&" + i, "§" + i); - } - for (char c : "abcdefklnor".toCharArray()) { - msg = msg.replace("&" + c, "§" + c); - } - - // 换行 - msg = msg.replace("
", "\n"); - msg = msg.replace("
", "\n"); - msg = msg.replace("
", "\n"); - msg = msg.replace("\\n", "\n"); - - //

和 颜色标签预处理 — 必须在HTML标签转换之前 - msg = processColorTags(msg); - - // HTML → Minecraft格式(栈式解析,支持嵌套) - msg = convertHtmlTags(msg); - - // 前缀 - msg = "§c§l【区域防护】§r " + msg; - - return msg; - } - - /** - * HTML实体解码 - * 支持: & < > " ' ' '       - */ - private String decodeHtmlEntities(String msg) { - msg = msg.replace("&", "&"); - msg = msg.replace("<", "<"); - msg = msg.replace(">", ">"); - msg = msg.replace(""", "\""); - msg = msg.replace("'", "'"); - msg = msg.replace("'", "'"); - msg = msg.replace("'", "'"); - msg = msg.replace(" ", " "); - msg = msg.replace(" ", " "); - msg = msg.replace(" ", " "); - return msg; - } - - /** - * 处理HTML颜色标签:

和 - * 转换为Minecraft颜色代码 - */ - private String processColorTags(String msg) { - //

text

→ §color text§r - java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile( - "(.*?)

", - java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.DOTALL); - java.util.regex.Matcher pMatcher = pPattern.matcher(msg); - StringBuilder sb = new StringBuilder(); - while (pMatcher.find()) { - String color = pMatcher.group(1); - String text = pMatcher.group(2); - String mcColor = convertHtmlColorToMc(color); - pMatcher.appendReplacement(sb, mcColor + text + "§r"); - } - pMatcher.appendTail(sb); - msg = sb.toString(); - - // text → §color text§r - java.util.regex.Pattern sPattern = java.util.regex.Pattern.compile( - "(.*?)", - java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.DOTALL); - java.util.regex.Matcher sMatcher = sPattern.matcher(msg); - sb = new StringBuilder(); - while (sMatcher.find()) { - String color = sMatcher.group(1); - String text = sMatcher.group(2); - String mcColor = convertHtmlColorToMc(color); - sMatcher.appendReplacement(sb, mcColor + text + "§r"); - } - sMatcher.appendTail(sb); - msg = sb.toString(); - - // 清理残留的

和标签 - msg = msg.replaceAll("

(.*?)

", "$1"); - msg = msg.replaceAll("(.*?)", "$1"); - - return msg; - } - - /** - * HTML颜色值→Minecraft颜色代码 - * 支持: #RRGGBB, 颜色名(red/green/blue等), rgb() - */ - private String convertHtmlColorToMc(String color) { - if (color == null || color.isEmpty()) return "§f"; - String lower = color.toLowerCase().trim(); - - // rgb()函数 - if (lower.startsWith("rgb(")) { - try { - String content = color.substring(color.indexOf('(') + 1, color.lastIndexOf(')')); - String[] parts = content.split(","); - if (parts.length >= 3) { - int r = Integer.parseInt(parts[0].trim()); - int g = Integer.parseInt(parts[1].trim()); - int b = Integer.parseInt(parts[2].trim()); - return convertRgbToMc(r, g, b); - } - } catch (Exception ignored) {} - return "§f"; - } - - // 十六进制 - if (lower.startsWith("#")) { - String hex = lower.substring(1); - try { - if (hex.length() == 3) { - hex = "" + hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2); - } - int r = Integer.parseInt(hex.substring(0, 2), 16); - int g = Integer.parseInt(hex.substring(2, 4), 16); - int b = Integer.parseInt(hex.substring(4, 6), 16); - return convertRgbToMc(r, g, b); - } catch (Exception ignored) {} - return "§f"; - } - - // 颜色名映射 - switch (lower) { - case "red": return "§c"; - case "green": case "lime": return "§a"; - case "blue": return "§9"; - case "yellow": return "§e"; - case "white": return "§f"; - case "black": return "§0"; - case "gray": case "grey": return "§7"; - case "dark_gray": case "dark_grey": return "§8"; - case "orange": case "gold": return "§6"; - case "aqua": case "cyan": return "§b"; - case "pink": case "light_purple": return "§d"; - case "purple": case "dark_purple": return "§5"; - case "dark_red": return "§4"; - case "dark_green": return "§2"; - case "dark_blue": case "navy": return "§1"; - case "dark_aqua": case "dark_cyan": return "§3"; - default: return "§f"; - } - } - - /** - * RGB值→最近的Minecraft颜色代码 - */ - private String convertRgbToMc(int r, int g, int b) { - // MC 16色匹配(取色差最小的) - int[][] mcColors = { - {0, 0, 0}, // §0 black - {0, 0, 170}, // §1 dark_blue - {0, 170, 0}, // §2 dark_green - {0, 170, 170}, // §3 dark_aqua - {170, 0, 0}, // §4 dark_red - {170, 0, 170}, // §5 dark_purple - {255, 170, 0}, // §6 gold - {170, 170, 170}, // §7 gray - {85, 85, 85}, // §8 dark_gray - {85, 85, 255}, // §9 blue - {85, 255, 85}, // §a green - {85, 255, 255}, // §b aqua - {255, 85, 85}, // §c red - {255, 85, 255}, // §d light_purple - {255, 255, 85}, // §e yellow - {255, 255, 255}, // §f white - }; - char[] codes = "0123456789abcdef".toCharArray(); - int bestIdx = 15; - int bestDist = Integer.MAX_VALUE; - for (int i = 0; i < mcColors.length; i++) { - int dr = r - mcColors[i][0]; - int dg = g - mcColors[i][1]; - int db = b - mcColors[i][2]; - int dist = dr * dr + dg * dg + db * db; - if (dist < bestDist) { - bestDist = dist; - bestIdx = i; - } - } - return "§" + codes[bestIdx]; - } - - /** - * 栈式HTML标签解析 - * 处理 text rest 这样的嵌套标签 - * 内层关闭时重新应用外层格式 - * ★ 支持颜色追踪:processColorTags设置的§c等颜色码在标签关闭§r后自动恢复 - */ - private String convertHtmlTags(String msg) { - StringBuilder sb = new StringBuilder(); - java.util.Deque stack = - new ArrayDeque<>(); - String activeColor = null; // 追踪processColorTags设置的颜色 - int i = 0; - - while (i < msg.length()) { - // ★ 追踪MC颜色码(§0-§f):当processColorTags插入颜色码时记录 - if (msg.charAt(i) == '§' && i + 1 < msg.length()) { - char next = Character.toLowerCase(msg.charAt(i + 1)); - if ("0123456789abcdef".indexOf(next) >= 0) { - activeColor = "" + msg.charAt(i) + next; - } - sb.append(msg.charAt(i)); - sb.append(msg.charAt(i + 1)); - i += 2; - continue; - } - - if (msg.charAt(i) == '<' - && i + 1 < msg.length()) { - if (msg.charAt(i + 1) == '/') { - // 关闭标签 - int end = msg.indexOf('>', i); - if (end < 0) { - sb.append(msg.charAt(i)); - i++; - continue; - } - String tag = msg.substring(i + 2, end) - .trim().toLowerCase(); - char code = htmlToMc(tag); - if (code != 0 && !stack.isEmpty() - && stack.peek() == code) { - stack.pop(); - // ★ 重置后先恢复颜色,再重新应用外层格式 - sb.append("§r"); - if (activeColor != null) { - sb.append(activeColor); - } - for (char c : stack) { - sb.append("§").append(c); - } - } - i = end + 1; - } else { - // 开放标签 - int end = msg.indexOf('>', i); - if (end < 0) { - sb.append(msg.charAt(i)); - i++; - continue; - } - String tag = msg.substring(i + 1, end) - .trim().toLowerCase(); - char code = htmlToMc(tag); - if (code != 0) { - stack.push(code); - sb.append("§").append(code); - } - i = end + 1; - } - } else { - sb.append(msg.charAt(i)); - i++; - } - } - return sb.toString(); - } - - private char htmlToMc(String tag) { - switch (tag) { - case "b": case "strong": case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": return 'l'; - case "i": case "em": case "var": return 'o'; - case "u": case "ins": return 'n'; - case "s": case "del": case "strike": return 'm'; - case "center": case "p": case "span": case "div": case "font": return 0; // 颜色标签已在processColorTags处理,此处跳过 - default: return 0; - } - } - -// =============== 加载 ==================== - - public void loadAllAreas() { - areas.clear(); - - // ★ 优先从数据库加载 - loadAreasFromDb(); - - // ★ 自动迁移:确保 allow_visitor_teleport 列存在(2026-07-08 新增) - try { - dbConnection.createStatement().execute("ALTER TABLE area_lands ADD COLUMN allow_visitor_teleport INTEGER DEFAULT 0"); - plugin.getLogger().info("[防护] 已添加 allow_visitor_teleport 列"); - } catch (Exception ignored) { /* 列已存在则忽略 */ } - - // ★ 自动迁移:如果DB有数据但txt文件仍存在,迁移txt中的白名单数据到DB后删除txt - if (!areas.isEmpty()) { - migrateTxtFilesToDb(); - } - - // ★ 兼容:如果没有从DB加载到数据,回退到txt(未迁移场景)并自动导入DB - if (areas.isEmpty()) { - File[] files = rootDir.listFiles( - (File d, String n) -> n.endsWith(".txt")); - if (files != null) { - for (File f : files) { - try { - String name = f.getName().replace(".txt", ""); - AreaConfig ac = parseArea(name, f); - areas.put(name, ac); - // ★ 自动写入数据库 - saveAreaToDb(ac); - plugin.getLogger().info("[防护] 自动迁移txt→DB: " + name); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 加载失败: " + f.getName()); - } - } - // ★ 迁移完成后删除所有txt文件 - if (!areas.isEmpty()) { - for (File f : files) { - if (f.delete()) { - plugin.getLogger().info("[防护] 已删除迁移后的txt: " + f.getName()); - } - } - plugin.getLogger().info("[防护] txt迁移完成,共导入 " + areas.size() + " 个领地"); - } - } - } - } - - /** - * 迁移txt文件到数据库(如果txt存在但DB已有数据,清理txt) - */ - private void migrateTxtFilesToDb() { - File[] files = rootDir.listFiles( - (File d, String n) -> n.endsWith(".txt")); - if (files == null || files.length == 0) return; - - plugin.getLogger().info("[防护] 检测到 " + files.length + " 个残留txt文件,开始清理..."); - for (File f : files) { - String name = f.getName().replace(".txt", ""); - // 如果txt对应的领地不在DB中,先导入 - if (!areas.containsKey(name)) { - try { - AreaConfig ac = parseArea(name, f); - areas.put(name, ac); - saveAreaToDb(ac); - plugin.getLogger().info("[防护] 迁移txt→DB: " + name); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 迁移失败: " + f.getName()); - } - } - // 删除txt - if (f.delete()) { - plugin.getLogger().info("[防护] 已删除: " + f.getName()); - } - } - } - - /** - * 从数据库加载所有领地配置 - */ - private void loadAreasFromDb() { - if (dbConnection == null) return; - try { - Statement stmt = dbConnection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM area_lands"); - int count = 0; - while (rs.next()) { - AreaConfig ac = new AreaConfig(); - ac.name = rs.getString("name"); - ac.owner = rs.getString("owner"); - ac.world = rs.getString("world"); - ac.x1 = rs.getInt("x1"); - ac.z1 = rs.getInt("z1"); - ac.x2 = rs.getInt("x2"); - ac.z2 = rs.getInt("z2"); - ac.yMin = rs.getInt("y_min"); - ac.yMax = rs.getInt("y_max"); - ac.confiscateItems = splitToList(rs.getString("confiscate_items")); - ac.denyUseItems = splitToList(rs.getString("deny_use_items")); - ac.giveEffects = parseEffectsString(rs.getString("give_effects")); - ac.clearEffects = splitToList(rs.getString("clear_effects")); - ac.clearAllBadEffects = rs.getInt("clear_all_bad") == 1; - ac.punishCommands = splitToList(rs.getString("punish_commands")); - ac.denyBlockPlace = rs.getInt("deny_block_place") == 1; - ac.denyBlockBreak = rs.getInt("deny_block_break") == 1; - ac.denyPVP = rs.getInt("deny_pvp") == 1; - ac.denyFallDamage = rs.getInt("deny_fall_damage") == 1; - ac.denyHunger = rs.getInt("deny_hunger") == 1; - ac.denyAllDamage = rs.getInt("deny_all_damage") == 1; - ac.allowDrop = rs.getInt("deny_drop") == 1; - ac.denyMount = rs.getInt("deny_mount") == 1; - ac.denyEnderPearl = rs.getInt("deny_ender_pearl") == 1; - ac.denyBow = rs.getInt("deny_bow") == 1; - ac.denyPotion = rs.getInt("deny_potion") == 1; - ac.denyExplosion = rs.getInt("deny_explosion") == 1; - ac.denyRaid = rs.getInt("deny_raid") == 1; - ac.denyFireSpread = rs.getInt("deny_fire_spread") == 1; - ac.denyAllEffects = rs.getInt("deny_all_effects") == 1; - ac.denyItemFrame = rs.getInt("deny_item_frame") == 1; - try { ac.denyEntityInteract = rs.getInt("deny_entity_interact") == 1; } catch (Exception ignored) {} - try { ac.denyMove = rs.getInt("deny_move") == 1; } catch (Exception ignored) {} - try { ac.allowPickup = rs.getInt("deny_pickup") == 1; } catch (Exception ignored) {} - try { ac.denyFire = rs.getInt("deny_fire") == 1; } catch (Exception ignored) {} - try { ac.denyThrownProjectiles = rs.getInt("deny_thrown_projectiles") == 1; } catch (Exception ignored) {} - try { ac.denyGlowing = rs.getInt("deny_glowing") == 1; } catch (Exception ignored) {} - try { ac.denyRedstoneInteraction = rs.getInt("deny_redstone_interaction") == 1; } catch (Exception ignored) {} - try { ac.denyDoorInteraction = rs.getInt("deny_door_interaction") == 1; } catch (Exception ignored) {} - try { ac.denyNoteblockJukebox = rs.getInt("deny_noteblock_jukebox") == 1; } catch (Exception ignored) {} - try { ac.denyLead = rs.getInt("deny_lead") == 1; } catch (Exception ignored) {} - try { ac.denyCropHarvest = rs.getInt("deny_crop_harvest") == 1; } catch (Exception ignored) {} - try { ac.denyFarmlandTrample = rs.getInt("deny_farmland_trample") == 1; } catch (Exception ignored) {} - try { ac.denyEnderTeleport = rs.getInt("deny_ender_teleport") == 1; } catch (Exception ignored) {} - try { ac.denyWoolShear = rs.getInt("deny_wool_shear") == 1; } catch (Exception ignored) {} - try { ac.denyAnimalFeeding = rs.getInt("deny_animal_feeding") == 1; } catch (Exception ignored) {} - try { ac.denyContainer = rs.getInt("deny_container") == 1; } catch (Exception ignored) {} - try { ac.denyMobAttack = rs.getInt("deny_mob_attack") == 1; } catch (Exception ignored) {} - try { ac.denySignEdit = rs.getInt("deny_sign_edit") == 1; } catch (Exception ignored) {} - try { ac.denySpawnEgg = rs.getInt("deny_spawn_egg") == 1; } catch (Exception ignored) {} - try { ac.denyWax = rs.getInt("deny_wax") == 1; } catch (Exception ignored) {} - try { ac.denyVillagerTrade = rs.getInt("deny_villager_trade") == 1; } catch (Exception ignored) {} - try { ac.badEffects = parseEffectsString(rs.getString("bad_effects")); } catch (Exception ignored) { ac.badEffects = new ArrayList<>(); } - try { ac.confiscateCount = rs.getInt("confiscate_count"); } catch (Exception ignored) {} - try { ac.returnOnLeave = rs.getInt("return_on_leave") == 1; } catch (Exception ignored) {} - try { ac.warpX = rs.getDouble("warp_x"); } catch (Exception ignored) {} - try { ac.warpY = rs.getDouble("warp_y"); } catch (Exception ignored) {} - try { ac.warpZ = rs.getDouble("warp_z"); } catch (Exception ignored) {} - try { ac.warpYaw = rs.getFloat("warp_yaw"); } catch (Exception ignored) {} - try { ac.warpPitch = rs.getFloat("warp_pitch"); } catch (Exception ignored) {} - try { ac.warpWorld = rs.getString("warp_world"); } catch (Exception ignored) {} - try { ac.isPublicBuilding = rs.getInt("is_public_building") == 1; } catch (Exception ignored) {} - try { ac.allowVisitorTeleport = rs.getInt("allow_visitor_teleport") == 1; } catch (Exception ignored) {} - ac.peaceMode = rs.getInt("peace_mode") == 1; - ac.peaceModeDuration = rs.getInt("peace_mode_duration") * 1000; // 转毫秒 - ac.peaceWhitelist = new HashSet<>(splitToList(rs.getString("peace_whitelist"))); - String gm = rs.getString("enforce_game_mode"); - ac.enforceGameMode = (gm != null && !gm.isEmpty()) ? gm : null; - ac.modeExempt = new HashSet<>(splitToList(rs.getString("mode_exempt"))); - ac.enterMsg = rs.getString("enter_msg"); - ac.leaveMsg = rs.getString("leave_msg"); - ac.confiscateMsg = rs.getString("confiscate_msg"); - ac.enableAnnounce = rs.getInt("enable_announce") == 1; - ac.announceTemplate = rs.getString("announce_template"); - ac.txtContent = rs.getString("txt_content"); - try { ac.createCost = rs.getInt("create_cost"); } catch (Exception ignored) {} - areas.put(ac.name, ac); - count++; - } - rs.close(); - stmt.close(); - if (count > 0) { - plugin.getLogger().info("[防护] 从数据库加载" + count + "个领地"); - } - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 从DB加载领地失败: " + e.getMessage()); - } - } - - /** - * 逗号分隔字符串或JSON数组 → List - * 兼容: "a,b,c" 或 ["a","b","c"] - */ - private List splitToList(String s) { - List result = new ArrayList<>(); - if (s == null || s.isEmpty()) return result; - String trimmed = s.trim(); - // ★ 支持JSON数组格式(PHP端存储格式),如 ["速度","急迫"] - // (PHP 端 JSON 存储时中文会被转义成 U+XXXX 形式,例如 "试" 对应 U+8BD5,此处展示的是 PHP 侧原样) - if (trimmed.startsWith("[") && trimmed.endsWith("]")) { - try { - String inner = trimmed.substring(1, trimmed.length() - 1); - java.util.regex.Matcher m = java.util.regex.Pattern - .compile("\"(?:\\\\.|[^\"\\\\])*\"") - .matcher(inner); - while (m.find()) { - String raw = m.group(); - String cleaned = unescapeJsonString(raw.substring(1, raw.length() - 1)); - cleaned = cleaned.replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - if (!cleaned.isEmpty()) result.add(cleaned); - } - if (!result.isEmpty()) return result; - } catch (Exception ignored) { - // 回退到逗号分隔 - } - } - for (String item : s.split(",")) { - String itemTrimmed = item.trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - if (!itemTrimmed.isEmpty()) result.add(itemTrimmed); - } - return result; - } - - /** - * 简单JSON字符串unescape(支持 \\ \" \/ \b \f \n \r \t 以及反斜杠u开头的Unicode四位十六进制转义) - */ - private String unescapeJsonString(String s) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '\\' && i + 1 < s.length()) { - char next = s.charAt(i + 1); - switch (next) { - case '"': case '\\': case '/': sb.append(next); i++; break; - case 'b': sb.append('\b'); i++; break; - case 'f': sb.append('\f'); i++; break; - case 'n': sb.append('\n'); i++; break; - case 'r': sb.append('\r'); i++; break; - case 't': sb.append('\t'); i++; break; - case 'u': - if (i + 5 < s.length()) { - try { - int code = Integer.parseInt(s.substring(i + 2, i + 6), 16); - sb.append((char) code); - i += 5; - } catch (NumberFormatException e) { - sb.append(c); - } - } else { - sb.append(c); - } - break; - default: - sb.append(next); - i++; - break; - } - } else { - sb.append(c); - } - } - return sb.toString(); - } - - /** - * 解析效果存储字符串 → List - * 格式: 管道格式 "效果名:等级:秒数|效果名:等级:秒数" - * 或 JSON格式 [["效果名",等级,秒数],...] - */ - private List parseEffectsString(String s) { - List result = new ArrayList<>(); - if (s == null || s.isEmpty()) return result; - - String trimmed = s.trim(); - // ★ 支持JSON格式(PHP端存储格式)[["效果名",等级,秒数],...] - if (trimmed.startsWith("[")) { - try { - String inner = trimmed.substring(1, trimmed.length() - 1); - java.util.regex.Matcher m = java.util.regex.Pattern - .compile("\\[\\s*\"((?:\\\\.|[^\"\\\\])*)\"\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\]") - .matcher(inner); - while (m.find()) { - String name = unescapeJsonString(m.group(1)) - .replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - if (!name.isEmpty()) { - result.add(new String[]{name, m.group(2), m.group(3)}); - } - } - if (!result.isEmpty()) return result; - } catch (Exception ignored) { - // JSON解析失败,回退到管道格式 - } - } - - // ★ 管道格式(Java端GUI存储格式): 效果名:等级:秒数|效果名:等级:秒数 - for (String part : s.split("\\|")) { - String[] pieces = part.split(":"); - if (pieces.length >= 3) { - String name = pieces[0].trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - result.add(new String[]{name, pieces[1], pieces[2]}); - } - } - return result; - } - - - private void initDatabase() { - try { - dbConnection = DriverManager.getConnection( - "jdbc:sqlite:" + newDbFile.getAbsolutePath()); - Statement stmt = dbConnection.createStatement(); - - // 效果记录表(原有) - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS player_effects (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "uuid TEXT NOT NULL," - + "player_name TEXT," - + "area_name TEXT NOT NULL," - + "effect_type TEXT NOT NULL," - + "effect_level INTEGER DEFAULT 0," - + "effect_duration INTEGER DEFAULT 999," - + "enter_time INTEGER NOT NULL)"); - stmt.executeUpdate( - "CREATE INDEX IF NOT EXISTS idx_uuid " - + "ON player_effects(uuid)"); - - // ★ 领地主表(替代txt文件存储) - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS area_lands (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "name TEXT UNIQUE NOT NULL," - + "owner TEXT NOT NULL DEFAULT ''," - + "world TEXT NOT NULL," - + "x1 INTEGER NOT NULL," - + "z1 INTEGER NOT NULL," - + "x2 INTEGER NOT NULL," - + "z2 INTEGER NOT NULL," - + "y_min INTEGER DEFAULT 0," - + "y_max INTEGER DEFAULT 255," - + "confiscate_items TEXT DEFAULT ''," - + "deny_use_items TEXT DEFAULT ''," - + "give_effects TEXT DEFAULT ''," - + "bad_effects TEXT DEFAULT ''," - + "clear_effects TEXT DEFAULT ''," - + "clear_all_bad INTEGER DEFAULT 0," - + "punish_commands TEXT DEFAULT ''," - + "deny_block_place INTEGER DEFAULT 0," - + "deny_block_break INTEGER DEFAULT 0," - + "deny_container INTEGER DEFAULT 0," - + "deny_pvp INTEGER DEFAULT 0," - + "deny_fall_damage INTEGER DEFAULT 0," - + "deny_hunger INTEGER DEFAULT 0," - + "deny_all_damage INTEGER DEFAULT 0," - + "deny_drop INTEGER DEFAULT 0," - + "deny_mount INTEGER DEFAULT 0," - + "deny_ender_pearl INTEGER DEFAULT 0," - + "deny_bow INTEGER DEFAULT 0," - + "deny_potion INTEGER DEFAULT 0," - + "deny_explosion INTEGER DEFAULT 0," - + "deny_raid INTEGER DEFAULT 0," - + "deny_fire_spread INTEGER DEFAULT 0," - + "deny_all_effects INTEGER DEFAULT 0," - + "deny_item_frame INTEGER DEFAULT 0," - + "deny_entity_interact INTEGER DEFAULT 0," - + "deny_move INTEGER DEFAULT 0," - + "deny_pickup INTEGER DEFAULT 0," - + "deny_fire INTEGER DEFAULT 0," - + "peace_mode INTEGER DEFAULT 0," - + "peace_mode_duration INTEGER DEFAULT 5," - + "peace_whitelist TEXT DEFAULT ''," - + "enforce_game_mode TEXT DEFAULT ''," - + "mode_exempt TEXT DEFAULT ''," - + "enter_msg TEXT DEFAULT ''," - + "leave_msg TEXT DEFAULT ''," - + "confiscate_msg TEXT DEFAULT ''," - + "enable_announce INTEGER DEFAULT 0," - + "announce_template TEXT DEFAULT ''," - + "txt_content TEXT DEFAULT ''," - + "created_at INTEGER NOT NULL)"); - - // ★ 领地权限表(管理员/用户/访客三级权限) - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS area_land_permissions (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "land_id INTEGER NOT NULL," - + "player_name TEXT NOT NULL," - + "role TEXT NOT NULL DEFAULT 'visitor'," - + "permissions TEXT DEFAULT ''," - + "granted_at INTEGER NOT NULL," - + "expires_at INTEGER DEFAULT 0," - + "UNIQUE(land_id, player_name))"); - stmt.executeUpdate( - "CREATE INDEX IF NOT EXISTS idx_land_id " - + "ON area_land_permissions(land_id)"); - - // ★ 权限商店表 - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS area_permission_shop (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "land_id INTEGER NOT NULL," - + "seller TEXT NOT NULL," - + "permission TEXT NOT NULL," - + "price INTEGER NOT NULL," - + "duration INTEGER NOT NULL DEFAULT 86400," - + "created_at INTEGER NOT NULL," - + "status TEXT DEFAULT 'active'," - + "buyer TEXT DEFAULT ''," - + "bought_at INTEGER DEFAULT 0)"); - - // ★ 全局配置表 - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS area_config (" - + "key TEXT PRIMARY KEY," - + "value TEXT NOT NULL)"); - - // 添加可能缺失的列(兼容旧数据库) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_move INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_pickup INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_fire INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN txt_content TEXT DEFAULT ''"); } catch (Exception ignored) {} - // ★ 新增权限列 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_thrown_projectiles INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_glowing INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_redstone_interaction INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_door_interaction INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_noteblock_jukebox INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_lead INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_crop_harvest INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_farmland_trample INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_ender_teleport INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wool_shear INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_animal_feeding INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 传送点列 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_x REAL DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_y REAL DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_z REAL DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_yaw REAL DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_pitch REAL DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_world TEXT DEFAULT ''"); } catch (Exception ignored) {} - // ★ 容器管理权限列(旧表缺失会导致INSERT OR REPLACE失败) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_container INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 玩家攻击生物权限列 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_mob_attack INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 告示牌编辑权限列 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_sign_edit INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 公共建筑标记 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN is_public_building INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 流体阻止 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_fluid INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 二级权限:实体交互(受一级放置/破坏总开关影响) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_entity_interact INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN create_cost INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 生物蛋权限列(上级权限:放置方块) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_spawn_egg INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 涂蜡/刮蜡权限列(上级权限:破坏方块/放置方块) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wax INTEGER DEFAULT 0"); } catch (Exception ignored) {} - // ★ 村民交易权限列(2026-10-10:领地主控制玩家能否与领地内村民交易) - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_villager_trade INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN bad_effects TEXT DEFAULT ''"); } catch (Exception ignored) {} - // ★ 违禁品管理面板(2026-10-11):没收计数 + 退出是否归还 - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN confiscate_count INTEGER DEFAULT 0"); } catch (Exception ignored) {} - try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN return_on_leave INTEGER DEFAULT 0"); } catch (Exception ignored) {} - - // ★ 全局配置默认值 - try { - ResultSet checkRs = stmt.executeQuery("SELECT COUNT(*) FROM area_config"); - int configCount = 0; - if (checkRs.next()) configCount = checkRs.getInt(1); - checkRs.close(); - if (configCount == 0) { - stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('create_price_per_sqm', '10')"); - stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_lands_per_player', '5')"); - stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_home_per_player', '5')"); - stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('default_height', '255')"); - stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('peace_mode_max_duration', '3600')"); - } - // ★ 退费比例配置(新键,已存在则忽略) - try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('refund_ratio_per_cent', '0')"); } catch (Exception ignored) {} - // ★ 面积限制默认值(新键,已存在则忽略) - try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_free_area_sqm', '3500')"); } catch (Exception ignored) {} - try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_land_side_blocks', '7000')"); } catch (Exception ignored) {} - // 读取全局配置 - ResultSet cfgRs = stmt.executeQuery("SELECT key, value FROM area_config"); - while (cfgRs.next()) { - String k = cfgRs.getString("key"); - String v = cfgRs.getString("value"); - try { - switch (k) { - case "create_price_per_sqm": globalCreatePricePerSqm = Integer.parseInt(v); break; - case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(v); break; - case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(v); break; - case "default_height": globalDefaultHeight = Integer.parseInt(v); break; - case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(v); break; - case "max_free_area_sqm": globalMaxFreeAreaSqm = Integer.parseInt(v); break; - case "max_land_side_blocks": globalMaxLandSideBlocks = Integer.parseInt(v); break; - } - } catch (NumberFormatException ignored) {} - } - cfgRs.close(); - } catch (Exception ignored) {} - - stmt.close(); - } catch (SQLException e) { - plugin.getLogger().severe( - "[防护] 数据库初始化失败: " - + e.getMessage()); - } - } - - -// ==================== txt→db 迁移 ==================== - - /** - * 将现有的txt文件区域配置迁移到数据库 - * 只在数据库中没有记录时执行(首次升级) - */ - private void migrateTxtToDb() { - try { - // 检查数据库是否已有领地数据 - Statement checkStmt = dbConnection.createStatement(); - ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM area_lands"); - int dbCount = 0; - if (rs.next()) dbCount = rs.getInt(1); - rs.close(); - checkStmt.close(); - - // 如果数据库已有数据,跳过迁移 - if (dbCount > 0) { - plugin.getLogger().info("[防护] 数据库已有" + dbCount + "个领地,跳过迁移"); - return; - } - - // 扫描txt文件 - File[] txtFiles = rootDir.listFiles( - (File d, String n) -> n.endsWith(".txt")); - if (txtFiles == null || txtFiles.length == 0) { - plugin.getLogger().info("[防护] 无txt文件需要迁移"); - return; - } - - int migrated = 0; - PreparedStatement insertStmt = dbConnection.prepareStatement( - "INSERT INTO area_lands (name, owner, world, x1, z1, x2, z2, y_min, y_max, " - + "confiscate_items, deny_use_items, give_effects, clear_effects, clear_all_bad, " - + "punish_commands, deny_block_place, deny_block_break, deny_pvp, deny_fall_damage, " - + "deny_hunger, deny_all_damage, deny_drop, deny_mount, deny_ender_pearl, " - + "deny_bow, deny_potion, deny_explosion, deny_raid, deny_fire_spread, " - + "deny_all_effects, deny_item_frame, deny_entity_interact, deny_move, deny_pickup, deny_fire, " - + "peace_mode, peace_mode_duration, " - + "peace_whitelist, enforce_game_mode, mode_exempt, enter_msg, leave_msg, " - + "confiscate_msg, enable_announce, announce_template, txt_content, created_at) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - - for (File f : txtFiles) { - try { - String name = f.getName().replace(".txt", ""); - // 读取原始txt内容 - String txtContent = new String( - java.nio.file.Files.readAllBytes(f.toPath()), - StandardCharsets.UTF_8); - - // 解析AreaConfig - AreaConfig ac = parseArea(name, f); - - // 写入数据库 - insertStmt.setString(1, name); - insertStmt.setString(2, ""); // owner(txt迁移无owner) - insertStmt.setString(3, ac.world); - insertStmt.setInt(4, ac.x1); - insertStmt.setInt(5, ac.z1); - insertStmt.setInt(6, ac.x2); - insertStmt.setInt(7, ac.z2); - insertStmt.setInt(8, ac.yMin); - insertStmt.setInt(9, ac.yMax); - insertStmt.setString(10, String.join(",", ac.confiscateItems)); - insertStmt.setString(11, String.join(",", ac.denyUseItems)); - insertStmt.setString(12, effectsToString(ac.giveEffects)); - insertStmt.setString(13, String.join(",", ac.clearEffects)); - insertStmt.setInt(14, ac.clearAllBadEffects ? 1 : 0); - insertStmt.setString(15, String.join("|", ac.punishCommands)); - insertStmt.setInt(16, ac.denyBlockPlace ? 1 : 0); - insertStmt.setInt(17, ac.denyBlockBreak ? 1 : 0); - insertStmt.setInt(18, ac.denyPVP ? 1 : 0); - insertStmt.setInt(19, ac.denyFallDamage ? 1 : 0); - insertStmt.setInt(20, ac.denyHunger ? 1 : 0); - insertStmt.setInt(21, ac.denyAllDamage ? 1 : 0); - insertStmt.setInt(22, ac.allowDrop ? 1 : 0); - insertStmt.setInt(23, ac.denyMount ? 1 : 0); - insertStmt.setInt(24, ac.denyEnderPearl ? 1 : 0); - insertStmt.setInt(25, ac.denyBow ? 1 : 0); - insertStmt.setInt(26, ac.denyPotion ? 1 : 0); - insertStmt.setInt(27, ac.denyExplosion ? 1 : 0); - insertStmt.setInt(28, ac.denyRaid ? 1 : 0); - insertStmt.setInt(29, ac.denyFireSpread ? 1 : 0); - insertStmt.setInt(30, ac.denyAllEffects ? 1 : 0); - insertStmt.setInt(31, ac.denyItemFrame ? 1 : 0); - insertStmt.setInt(32, ac.denyEntityInteract ? 1 : 0); - insertStmt.setInt(33, ac.denyMove ? 1 : 0); - insertStmt.setInt(34, ac.allowPickup ? 1 : 0); - insertStmt.setInt(35, ac.denyFire ? 1 : 0); - insertStmt.setInt(36, ac.peaceMode ? 1 : 0); - insertStmt.setInt(37, ac.peaceModeDuration / 1000); // 存秒 - insertStmt.setString(38, String.join(",", ac.peaceWhitelist)); - insertStmt.setString(39, ac.enforceGameMode != null ? ac.enforceGameMode : ""); - insertStmt.setString(40, String.join(",", ac.modeExempt)); - insertStmt.setString(41, ac.enterMsg); - insertStmt.setString(42, ac.leaveMsg); - insertStmt.setString(43, ac.confiscateMsg); - insertStmt.setInt(44, ac.enableAnnounce ? 1 : 0); - insertStmt.setString(45, ac.announceTemplate); - insertStmt.setString(46, txtContent); - insertStmt.setLong(47, System.currentTimeMillis() / 1000); - insertStmt.executeUpdate(); - migrated++; - plugin.getLogger().info("[防护] 迁移txt→db: " + name); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 迁移失败: " + f.getName() + " - " + e.getMessage()); - } - } - insertStmt.close(); - - if (migrated > 0) { - plugin.getLogger().info("[防护] ★ 迁移完成,共" + migrated + "个区域已写入数据库"); - // 创建迁移完成标记文件,防止重复迁移 - File marker = new File(rootDir, ".db_migrated"); - marker.createNewFile(); - // 创建owner待分配标记文件 - File ownerMarker = new File(rootDir, ".need_owner_assign"); - ownerMarker.createNewFile(); - } - } catch (Exception e) { - plugin.getLogger().warning("[防护] 迁移过程异常: " + e.getMessage()); - } - } - - /** - * 将效果列表转为存储字符串 - * 格式: 效果名:等级:秒数|效果名:等级:秒数 - */ - private String effectsToString(List effects) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < effects.size(); i++) { - String[] e = effects.get(i); - if (i > 0) sb.append("|"); - sb.append(e[0]).append(":").append(e[1]).append(":").append(e[2]); - } - return sb.toString(); - } - -// ==================== 效果名称中英文映射 ==================== - - /** - * 将中文效果名转为 Bukkit 能识别的英文名 - */ - private String resolveEffectName(String name) { - if (name == null) return null; - String n = name.trim(); - // 如果已经是英文或命名空间格式,直接用 - if (n.contains(":") - || n.equals(n.toUpperCase())) { - return n; - } - // 中文 → 英文映射 - switch (n) { - case "伤害吸收": return "ABSORPTION"; - case "生命提升": return "HEALTH_BOOST"; - case "抗性提升": return "DAMAGE_RESISTANCE"; - case "抗火": return "FIRE_RESISTANCE"; - case "夜视": return "NIGHT_VISION"; - case "力量": return "INCREASE_DAMAGE"; - case "速度": return "SPEED"; - case "急迫": return "FAST_DIGGING"; - case "跳跃提升": return "JUMP"; - case "再生": return "REGENERATION"; - case "生命恢复": return "REGENERATION"; - case "治疗": return "HEAL"; - case "瞬间治疗": return "HEAL"; - case "伤害药水": return "HARM"; - case "瞬间伤害": return "HARM"; - case "虚弱": return "WEAKNESS"; - case "中毒": return "POISON"; - case "缓慢": return "SLOW"; - case "挖掘疲劳": return "SLOW_DIGGING"; - case "失明": return "BLINDNESS"; - case "隐身": return "INVISIBILITY"; - case "发光": return "GLOWING"; - // 村庄英雄 - case "村庄英雄": return "HERO_OF_THE_VILLAGE"; - case "幸运": return "LUCK"; - case "不幸": return "UNLUCK"; - case "水下呼吸": return "WATER_BREATHING"; - case "水下速掘": return "DOLPHINS_GRACE"; - case "缓降": return "LEVITATION"; - case "漂浮": return "LEVITATION"; - case "饱和": return "SATURATION"; - // 无效效果 - case "伤害": return "HARM"; - case "治疗药水": return "HEAL"; - default: - // 尝试直接用英文匹配 - return n; - } - } - - /** - * 全量容错:把全角字符统一转半角, - * 去掉零宽字符/不可见字符, - * 让用户怎么写都能正确解析。 - */ - private String normalizeLine(String input) { - if (input == null || input.isEmpty()) return ""; - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - - // 跳过零宽字符和不可见字符 - if (c == UnicodeChars.ZERO_WIDTH_SPACE // 零宽空格 - || c == UnicodeChars.ZERO_WIDTH_NON_JOINER // 零宽非连接 - || c == UnicodeChars.ZERO_WIDTH_JOINER // 零宽连接 - || c == UnicodeChars.BOM // BOM - || c == UnicodeChars.NBSP // 不间断空格 - || c == UnicodeChars.LINE_SEPARATOR // 行分隔符 - || c == UnicodeChars.PARAGRAPH_SEPARATOR // 段分隔符 - || c == UnicodeChars.LTR_MARK // LTR标记 - || c == UnicodeChars.RTL_MARK // RTL标记 - || c == UnicodeChars.WORD_JOINER // 字连接符 - || (c >= UnicodeChars.SPACE_U2000 && c <= UnicodeChars.SPACE_U200A)) { // 各种空格 - continue; - } - - // 全角→半角映射 - if (c >= '!' && c <= '~') { - c = (char) (c - 0xFEE0); - } - - // 全角空格→半角空格 - if (c == UnicodeChars.IDEOGRAPHIC_SPACE) { - c = ' '; - } - - sb.append(c); - } - - return sb.toString(); - } - - - private AreaConfig parseArea(String name, File f) - throws IOException { - AreaConfig ac = new AreaConfig(); - ac.name = name; - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - boolean inBlockComment = false; - boolean inHtmlComment = false; - - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty()) continue; -// ===== 全量容错预处理:全角→半角,去零宽字符 ===== - line = normalizeLine(line); - if (line.isEmpty()) continue; -// ===== 暴力调试:输出所有包含关键词的行 ===== - if (line.indexOf("游戏") >= 0 - || line.indexOf("模式") >= 0 - || line.indexOf("生存") >= 0 - || line.indexOf("创造") >= 0 - || line.indexOf("排除") >= 0 - || line.indexOf("豁免") >= 0 - || line.indexOf("强制") >= 0 - || line.indexOf("GameMode") >= 0 - || line.indexOf("SURVIVAL") >= 0 - || line.indexOf("CREATIVE") >= 0) { - StringBuilder hex = new StringBuilder(); - for (int i = 0; i < line.length(); i++) { - hex.append(String.format("U+%04X ", (int) line.charAt(i))); - } - /* plugin.getLogger().info("[暴力调试] 行=[" + line - + "] 长度=" + line.length() - + " 字符码=" + hex.toString());*/ - } -// ===== 兜底扫描:确保和平白名单一定能解析 ===== - if (line.indexOf("白名单") >= 0 - || line.indexOf("排除名单") >= 0 - || line.indexOf("模式排除") >= 0 - || line.indexOf("模式豁免") >= 0) { - int ci = line.indexOf(':'); - if (ci < 0) ci = line.indexOf(':'); - if (ci < 0) ci = line.indexOf('='); - if (ci < 0) ci = line.indexOf(' '); - - // 和平白名单 - if (line.indexOf("和平") >= 0 - && line.indexOf("白名单") >= 0 - && ci >= 0) { - String rest = line.substring(ci + 1).trim(); - rest = rest.replaceAll("[,,;;|、/]", ","); - for (String s : rest.split(",")) { - String nm = s.trim(); - if (!nm.isEmpty()) ac.peaceWhitelist.add(nm); - } - plugin.getLogger().info("[解析兜底] 和平白名单=" - + ac.peaceWhitelist); - } - - // 排除名单 - if (line.indexOf("排除名单") >= 0 - || line.indexOf("模式排除") >= 0 - || line.indexOf("模式豁免") >= 0) { - if (ci >= 0) { - String rest = line.substring(ci + 1).trim(); - rest = rest.replaceAll("[,,;;|、/]", ","); - for (String s : rest.split(",")) { - String nm = s.trim(); - if (!nm.isEmpty()) ac.modeExempt.add(nm); - } - plugin.getLogger().info("[解析兜底] 排除名单=" - + ac.modeExempt); - } - } - } -// ===== 兜底结束 ===== - - if (inBlockComment) { - int endIdx = line.indexOf("*/"); - if (endIdx >= 0) { - inBlockComment = false; - line = line.substring(endIdx + 2).trim(); - if (line.isEmpty()) continue; - } else { - continue; - } - } - if (line.startsWith("/*")) { - int endIdx = line.indexOf("*/", 2); - if (endIdx >= 0) { - line = line.substring(endIdx + 2).trim(); - if (line.isEmpty()) continue; - } else { - inBlockComment = true; - continue; - } - } - if (inHtmlComment) { - int endIdx = line.indexOf("-->"); - if (endIdx >= 0) { - inHtmlComment = false; - line = line.substring(endIdx + 3).trim(); - if (line.isEmpty()) continue; - } else { - continue; - } - } - if (line.startsWith("", 4); - if (endIdx >= 0) { - line = line.substring(endIdx + 3).trim(); - if (line.isEmpty()) continue; - } else { - inHtmlComment = true; - continue; - } - } - if (line.startsWith("#") || line.startsWith("//")) { - continue; - } - int hashIdx = line.indexOf("#"); - if (hashIdx > 0) { - line = line.substring(0, hashIdx).trim(); - } - int slashIdx = line.indexOf("//"); - if (slashIdx > 0) { - line = line.substring(0, slashIdx).trim(); - } - if (line.isEmpty()) continue; - - if (line.startsWith("起点:")) { - String[] pp = line.substring("起点:".length()) - .trim().split(","); - if (pp.length == 3) { - ac.world = pp[0].trim(); - ac.x1 = Integer.parseInt(pp[1].trim()); - ac.z1 = Integer.parseInt(pp[2].trim()); - } - } else if (line.startsWith("终点:")) { - String[] pp = line.substring("终点:".length()) - .trim().split(","); - if (pp.length == 3) { - ac.world = pp[0].trim(); - ac.x2 = Integer.parseInt(pp[1].trim()); - ac.z2 = Integer.parseInt(pp[2].trim()); - } - } else if (line.startsWith("高度范围:")) { - String[] pp = line.substring("高度范围:".length()) - .trim().split("-"); - if (pp.length == 2) { - ac.yMin = Integer.parseInt(pp[0].trim()); - ac.yMax = Integer.parseInt(pp[1].trim()); - } - } else if (line.startsWith("没收物品:")) { - for (String s : line.substring("没收物品:".length()) - .trim().split("[,,]")) - ac.confiscateItems.add( - s.trim().toUpperCase()); - } else if (line.startsWith("禁止使用物品:")) { - for (String s : line.substring("禁止使用物品:".length()) - .trim().split("[,,]")) - ac.denyUseItems.add( - s.trim().toUpperCase()); - } else if (line.equals("禁止放置方块")) { - ac.denyBlockPlace = true; - } else if (line.equals("禁止破坏方块")) { - ac.denyBlockBreak = true; - } else if (line.equals("禁止PVP")) { - ac.denyPVP = true; - } else if (line.equals("禁止摔伤")) { - ac.denyFallDamage = true; - } else if (line.equals("禁止饥饿")) { - ac.denyHunger = true; - } else if (line.equals("禁止一切伤害")) { - ac.denyAllDamage = true; - } else if (line.equals("允许丢弃物品")) { - ac.allowDrop = true; - } else if (line.equals("禁止末影珍珠")) { - ac.denyEnderPearl = true; - } else if (line.equals("禁止使用弓箭")) { - ac.denyBow = true; - } else if (line.equals("禁止骑乘")) { - ac.denyMount = true; - } else if (line.equals("禁止袭击")) { - ac.denyRaid = true; - } else if (line.equals("禁止药水")) { - ac.denyPotion = true; - } else if (line.equals("禁止爆炸")) { - ac.denyExplosion = true; - } else if (line.equals("禁止燃烧") - || line.equals("禁止火焰蔓延")) { - ac.denyFireSpread = true; - } else if (line.equals("清除所有效果") - || line.equals("清理所有效果")) { - ac.clearAllBadEffects = true; - } else if (line.startsWith("清除") - || line.startsWith("清理")) { - String effName = line.substring(2).trim(); - if (effName.endsWith("效果")) - effName = effName.substring(0, - effName.length() - 2); - if (!effName.isEmpty()) - ac.clearEffects.add(effName); - } else if (line.contains("和平模式时间")) { - ac.peaceMode = true; // 自动开启和平模式 - int colonIdx = line.indexOf(':'); - if (colonIdx < 0) colonIdx = line.indexOf(':'); - if (colonIdx >= 0) { - try { - int seconds = Integer.parseInt(line.substring(colonIdx + 1).trim()); - // 最大1小时=3600秒,最小1秒 - seconds = Math.max(1, Math.min(3600, seconds)); - ac.peaceModeDuration = seconds * 1000; - plugin.getLogger().info("[解析] 和平模式时间=" + seconds + "秒"); - } catch (NumberFormatException ignored) {} - } - } else if (line.contains("和平模式") - && !line.contains("白名单") - && !line.contains("定时") - && !line.contains("开关") - && !line.contains("时间")) { - ac.peaceMode = true; - plugin.getLogger().info("[解析] 和平模式已识别"); - - } else if (line.startsWith("效果")) { - String rest = line.substring(2).trim(); - // 去掉前导冒号、空格 - while (rest.length() > 0 - && (rest.charAt(0) == ':' - || rest.charAt(0) == ':' - || rest.charAt(0) == ' ' - || rest.charAt(0) == '\t')) { - rest = rest.substring(1); - } - if (!rest.isEmpty()) { - String[] parts = rest.split("\\s+"); - // 第一个是效果名(可能是中文) - String effName = parts[0]; - int effLv = 1; - int effDur = 999; - if (parts.length >= 2) { - try { - effLv = Integer.parseInt(parts[1]); - } catch (NumberFormatException ignored) {} - } - if (parts.length >= 3) { - try { - effDur = Integer.parseInt(parts[2]); - } catch (NumberFormatException ignored) {} - } - - // ★ 用映射方法转为英文名 - String resolved = resolveEffectName(effName); - plugin.getLogger().info("[防护] 解析效果: " - + effName + " → " + resolved - + " 等级=" + effLv - + " 时长=" + effDur); - - ac.giveEffects.add(new String[]{ - resolved, - String.valueOf(effLv), - String.valueOf(effDur)}); - // ★ 加日志确认解析结果 - PotionEffectType checkType = - resolveEffectType(effName); - plugin.getLogger().info("[防护] 解析效果: " - + effName + " → type=" - + (checkType != null - ? checkType.getName() - : "NULL(失败)") - + " 等级=" + effLv - + " 时长=" + effDur); - - - } - if (!rest.isEmpty()) { - String[] parts = rest.split("\\s+"); - int effLv = 1; - int effDur = 999; - if (parts.length >= 2) { - try { - effLv = Integer.parseInt(parts[1]); - } catch (NumberFormatException ignored) { - } - } - if (parts.length >= 3) { - try { - effDur = Integer.parseInt(parts[2]); - } catch (NumberFormatException ignored) { - } - } - ac.giveEffects.add(new String[]{ - parts[0], - String.valueOf(effLv), - String.valueOf(effDur)}); - } - } else if (line.startsWith("通报批评:")) { - ac.announceTemplate = line.substring( - "通报批评:".length()).trim(); - ac.enableAnnounce = true; - } else if (line.startsWith("没收提示:")) { - ac.confiscateMsg = line.substring( - "没收提示:".length()).trim(); - } else if (line.startsWith("进入提示:")) { - String raw = line.substring("进入提示:".length()).trim(); - ac.enterMsg = raw; - - // ★ 调试:打印原文和Unicode - StringBuilder hex = new StringBuilder(); - for (int i = 0; i < raw.length(); i++) { - hex.append(String.format("U+%04X ", - (int) raw.charAt(i))); - } - plugin.getLogger().info( - "[防护-解析] ★★★进入提示原文=[" + raw - + "] 长度=" + raw.length() - + " Unicode=" + hex.toString()); - } else if (line.startsWith("离开提示:")) { - String raw = line.substring("离开提示:".length()).trim(); - ac.leaveMsg = raw; - - StringBuilder hex = new StringBuilder(); - for (int i = 0; i < raw.length(); i++) { - hex.append(String.format("U+%04X ", - (int) raw.charAt(i))); - } - plugin.getLogger().info( - "[防护-解析] ★★★离开提示原文=[" + raw - + "] 长度=" + raw.length() - + " Unicode=" + hex.toString()); - } else if (line.startsWith("惩罚命令:")) { - ac.punishCommands.add(line.substring( - "惩罚命令:".length()).trim()); - } else if (line.startsWith("强制游戏模式:") - || line.startsWith("游戏模式:")) { - String prefix = line.startsWith("强制游戏模式:") - ? "强制游戏模式:" : "游戏模式:"; - String modeStr = line.substring(prefix.length()) - .trim().toUpperCase(); - // 容错:支持多种写法 - if (modeStr.equals("CREATIVE") || modeStr.equals("创造") - || modeStr.equals("1")) { - ac.enforceGameMode = "CREATIVE"; - } else if (modeStr.equals("SURVIVAL") || modeStr.equals("生存") - || modeStr.equals("0")) { - ac.enforceGameMode = "SURVIVAL"; - } else if (modeStr.equals("ADVENTURE") || modeStr.equals("冒险") - || modeStr.equals("2")) { - ac.enforceGameMode = "ADVENTURE"; - } else if (modeStr.equals("SPECTATOR") || modeStr.equals("旁观") - || modeStr.equals("spectator") - || modeStr.equals("3")) { - ac.enforceGameMode = "SPECTATOR"; - } - } else if (line.contains("排除名单") - || line.contains("模式排除") - || line.contains("模式豁免")) { - int colonIdx = line.indexOf(':'); - if (colonIdx < 0) colonIdx = line.indexOf(':'); - if (colonIdx < 0) colonIdx = line.indexOf('='); - if (colonIdx >= 0) { - String rest = line.substring(colonIdx + 1).trim(); - String cleaned = rest.replaceAll("[,,;;|、/]", ","); - for (String s : cleaned.split(",")) { - String playerName = s.trim(); - if (!playerName.isEmpty()) { - ac.modeExempt.add(playerName); - } - } - } - - String rest = line.substring(/*idx*/ + 4).trim(); - while (rest.length() > 0) { - char c = rest.charAt(0); - if (c == ':' || c == ':' || c == '=' - || c == ' ' || c == '\t') { - rest = rest.substring(1); - } else { - break; - } - } - if (!rest.isEmpty()) { - String cleaned = rest.replaceAll("[,,;;|、/]", ","); - for (String s : cleaned.split(",")) { - String playername = s.trim(); - if (!name.isEmpty()) ac.modeExempt.add(name); - } - } - - while (rest.length() > 0) { - char c = rest.charAt(0); - if (c == ':' || c == ':' || c == '=' - || c == ' ' || c == '\t') { - rest = rest.substring(1); - } else { - break; - } - - } - if (!rest.isEmpty()) { - String cleaned = rest.replaceAll("[,,;;|、/]", ","); - for (String s : cleaned.split(",")) { - String wlName = s.trim(); - if (!wlName.isEmpty()) { - ac.peaceWhitelist.add(wlName); - } - } - } - - - - } else if (line.equals("禁止交互展示框") - || line.equals("禁止展示框")) { - ac.denyItemFrame = true; - - } - } - br.close(); - return ac; - } -// ===== 和平白名单操作 ===== - - // 添加生物名字到区域和平白名单 - public boolean addPeaceName(String areaName, String creatureName) { - AreaConfig ac = areas.get(areaName); - if (ac == null) return false; - ac.peaceWhitelist.add(creatureName); - savePeaceWhitelist(ac); - return true; - } - - // 从区域和平白名单移除生物名字 - public boolean removePeaceName(String areaName, String creatureName) { - AreaConfig ac = areas.get(areaName); - if (ac == null) return false; - ac.peaceWhitelist.remove(creatureName); - savePeaceWhitelist(ac); - return true; - } - -// ===== 模式排除名单操作 ===== - - - @EventHandler - public void onCreatureSpawn(CreatureSpawnEvent e) { - Entity entity = e.getEntity(); - if (!isHostile(entity.getType().name())) return; - - Location loc = entity.getLocation(); - AreaConfig ac = getArea( - loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), - loc.getBlockZ()); - - if (ac == null || !ac.peaceMode) return; - - protectedEntities.put( - entity.getUniqueId(), - System.currentTimeMillis() + ac.peaceModeDuration); - - /* plugin.getLogger().info("[和平] " - + entity.getType().name() - + " 获得5秒保护期" - + " 区域=" + ac.name - + " 白名单=" + ac.peaceWhitelist - + " 白名单数量=" + ac.peaceWhitelist.size());*/ - } - - - public void cleanupProtectedEntities() { - long now = System.currentTimeMillis(); - int cleaned = 0; - Iterator> it = - protectedEntities.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (now > entry.getValue()) { - it.remove(); - cleaned++; - } - } - if (cleaned > 0) { - /* plugin.getLogger().info("[和平模式] 清理过期保护: " - + cleaned + "个 剩余:" - + protectedEntities.size() + "个");*/ - } - } - - - public void banHostilesWithWhitelist(Player p, AreaConfig ac) { - long now = System.currentTimeMillis(); - List nearby = p.getNearbyEntities(48, 48, 48); - int scanned = 0; - int waiting = 0; - int saved = 0; - int banished = 0; - - for (Entity ent : nearby) { - if (!isHostile(ent.getType().name())) continue; - scanned++; - - UUID entUid = ent.getUniqueId(); - Long protectUntil = protectedEntities.get(entUid); - String customName = ent.getCustomName(); - - // 白名单命名生物:永久保留(含保护期内贴标的) - if (customName != null && !customName.isEmpty() && ac.peaceWhitelist.contains(customName)) { - if (protectUntil != null) { - protectedEntities.remove(entUid); // ★ 取消清理倒计时 - // ★ 保护期内贴标:立即确认 - if (protectUntil - now > 0) { - p.sendMessage("§a§l[和平模式] §f" + ent.getType().name() + " §a已被白名单保护,永久保留"); - } - } - saved++; - continue; - } - - // 有保护期记录 - if (protectUntil != null) { - long remain = protectUntil - now; - if (remain > 0) { - waiting++; - continue; - } - // 保护期到期 → 移除记录并检查白名单(最后一刻贴标也有效) - protectedEntities.remove(entUid); - if (customName != null && !customName.isEmpty() && ac.peaceWhitelist.contains(customName)) { - saved++; - continue; - } - banEntity(ent); - banished++; - continue; - } - - // 无保护期记录 → 未命名敌对生物,立即清理 - banEntity(ent); - banished++; - } - } - - /** - * 全图扫描清理和平模式下的未命名敌对生物 - * 由 Main 中的定时任务调用 - */ - public void scanAllLandsPeaceMode() { - long now = System.currentTimeMillis(); - int cleared = 0; - - // 遍历所有已加载的区域 - for (Map.Entry entry : areas.entrySet()) { - AreaConfig ac = entry.getValue(); - if (!ac.peaceMode) continue; - - // 对该区域边界内的实体扫描 - double centerX = (ac.x1 + ac.x2) / 2.0; - double centerZ = (ac.z1 + ac.z2) / 2.0; - World world = Bukkit.getWorld(ac.world); - if (world == null) continue; - - // ★ 根据领地实际大小计算扫描范围(取x和z范围的最大值,加边距) - int rangeX = Math.abs(ac.x2 - ac.x1) / 2 + 10; - int rangeZ = Math.abs(ac.z2 - ac.z1) / 2 + 10; - int range = Math.max(rangeX, rangeZ); - // 限制最大扫描范围避免性能问题 - range = Math.min(range, 200); - - // ★ 使用世界实际高度范围 - int minY = world.getMinHeight(); - int maxY = world.getMaxHeight(); - int heightRange = (maxY - minY) / 2; - - List nearby = new java.util.ArrayList<>(world.getNearbyEntities( - new Location(world, centerX, (minY + maxY) / 2.0, centerZ), range, heightRange, range)); - - for (Entity ent : nearby) { - if (!isHostile(ent.getType().name())) continue; - - // 检查是否在领地范围内 - AreaConfig landAc = getArea(world.getName(), - ent.getLocation().getBlockX(), - ent.getLocation().getBlockY(), - ent.getLocation().getBlockZ()); - if (landAc == null || !landAc.peaceMode) continue; - - // 白名单命名生物:跳过 - String cname = ent.getCustomName(); - if (cname != null && !cname.isEmpty() && landAc.peaceWhitelist.contains(cname)) continue; - - // 保护期内:跳过 - UUID uid = ent.getUniqueId(); - Long protectUntil = protectedEntities.get(uid); - if (protectUntil != null && (protectUntil - now) > 0) continue; - - // ★ 直接删除实体(传送可能卡住或重新刷新) - ent.remove(); - cleared++; - } - } - - if (cleared > 0) { - // plugin.getLogger().info("[和平模式扫描] 清理未命名敌对生物: " + cleared + " 个"); - } - } - - private void banEntity(Entity ent) { - Location voidLoc = ent.getLocation().clone(); - voidLoc.setY(ent.getWorld().getMinHeight() - 50); - if ("ENDERMAN".equals(ent.getType().name())) { - ent.remove(); - } else { - ent.teleport(voidLoc); - } - protectedEntities.remove(ent.getUniqueId()); - } - - private static final Set HOSTILE_TYPES = new HashSet<>(Arrays.asList( - "ZOMBIE", "ZOMBIE_VILLAGER", "HUSK", "DROWNED", - "SKELETON", "STRAY", "BOGGED", "WITHER_SKELETON", - "CREEPER", "SPIDER", "CAVE_SPIDER", "ENDERMITE", - "SILVERFISH", "BLAZE", "GHAST", "BIG_GHAST", - "MAGMA_CUBE", "SLIME", "WITCH", - "GUARDIAN", "ELDER_GUARDIAN", - "PILLAGER", "VINDICATOR", "EVOKER", "RAVAGER", - "ILLUSIONER", "VEX", - "PHANTOM", "SHULKER", "WARDEN", "BREEZE", - "PIGLIN", "PIGLIN_BRUTE", "ZOGLIN", - "ZOMBIFIED_PIGLIN", "ENDER_DRAGON", - "ENDERMAN", "WITHER" - )); - - private boolean isHostile(String typeName) { - return HOSTILE_TYPES.contains(typeName); - } - - // ==================== 白名单持久化 ==================== - - /** - * 保存白名单到文件 - * 路径:区域防护/whitelists/全局白名单.txt - * 路径:区域防护/whitelists/区域名.txt - */ - public void saveWhitelists() { - try { - // 1. 保存全局白名单 - PrintWriter pwGlobal = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(globalWhitelistFile), - StandardCharsets.UTF_8)); - for (String name : globalPlayerWhitelist) { - pwGlobal.println(name); - } - pwGlobal.close(); - - // 2. 删除旧的区域白名单文件(防止残留) - File[] oldFiles = whitelistDir.listFiles( - (d, n) -> n.endsWith(".txt") - && !n.equals("全局白名单.txt")); - if (oldFiles != null) { - for (File f : oldFiles) f.delete(); - } - - // 3. 保存各区域白名单 - for (Map.Entry> entry - : areaPlayerWhitelist.entrySet()) { - String name = entry.getKey(); - File areaFile = new File( - whitelistDir, name + ".txt"); - PrintWriter pwArea = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(areaFile), - StandardCharsets.UTF_8)); - for (String playerName : entry.getValue()) { - pwArea.println(playerName); - } - pwArea.close(); - } - - plugin.getLogger().info("[防护] 白名单已保存"); - } catch (Exception e) { - plugin.getLogger().warning( - "[防护] 保存白名单失败: " - + e.getMessage()); - } - } - - - - // ==================== 区域检测 ==================== - - public AreaConfig getArea(String worldName, - int x, int y, int z) { - for (AreaConfig ac : areas.values()) { - if (ac.world.equalsIgnoreCase(worldName) - && inRect(x, z, ac) - && y >= ac.yMin - && y <= ac.yMax) { - return ac; - } - } - return null; - } - - private boolean inRect(int x, int z, AreaConfig ac) { - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - return x >= minX && x <= maxX - && z >= minZ && z <= maxZ; - } - - /** - * 获取玩家在指定领地的权限级别 - */ - public PermissionLevel getPermissionLevel(Player player, AreaConfig ac) { - // 1. 全局管理员(ScoreboardTag) - if (isAreaAdmin(player)) { - return PermissionLevel.ADMIN; - } - - // 2. 领地所有者权限 - if (ac != null && ac.owner != null && !ac.owner.isEmpty()) { - if (samePlayer(player.getName(), ac.owner)) { - return PermissionLevel.OWNER; - } - } - - // 3. 领地角色(DB,仅限该领地;2026-10-11 精确匹配,一次查询判 admin/member 两态) - // admin → ADMIN(/protect setadmin 或领地所有者GUI设置) - // member(未过期) → MEMBER:已授权成员,不受领地全局默认权限限制 - // ★ 必须在白名单/VISITOR 检查之前:成员多半同时在区域白名单里, - // 若先命中白名单只会拿到 VISITOR,denyMove 等权限按访客回退领地默认, - // 导致"已授权成员被当成路人、跟随全局权限一起被限制"(2026-10-11 修复)。 - // ★ 注意:严格按 ac.name 限定,绝不跨领地生效(封堵越权) - if (ac != null) { - int landId = getLandIdFromDb(ac.name); - if (landId > 0) { - PermRow row = findPermRowExact(landId, player.getName()); - if (row != null) { - if ("admin".equalsIgnoreCase(row.role)) { - return PermissionLevel.ADMIN; - } - if ("member".equalsIgnoreCase(row.role) - && (row.expiresAt == 0 || row.expiresAt > System.currentTimeMillis())) { - return PermissionLevel.MEMBER; - } - } - } - } - - // 4. 访客权限(白名单 + 数据库购买权限) - if (isPlayerWhitelisted(player.getName(), ac)) { - return PermissionLevel.VISITOR; - } - - // 5. 数据库购买的访客权限(未过期) - if (hasValidVisitorPermission(player, ac)) { - return PermissionLevel.VISITOR; - } - - // 6. 无权限 - return null; - } - - /** - * 检查玩家在领地是否有足够权限执行操作 - * @param requiredLevel 所需的最低权限级别 - * @return true如果有足够权限 - */ - public boolean hasPermission(Player player, AreaConfig ac, PermissionLevel requiredLevel) { - PermissionLevel playerLevel = getPermissionLevel(player, ac); - if (playerLevel == null) return false; - - // ADMIN拥有所有权限 - if (playerLevel == PermissionLevel.ADMIN) return true; - - // OWNER可以执行VISITOR能执行的操作 - if (playerLevel == PermissionLevel.OWNER) { - return requiredLevel != PermissionLevel.ADMIN; - } - - // VISITOR只能执行VISITOR级别的操作 - return requiredLevel == PermissionLevel.VISITOR; - } - - /** - * 领地传送权限检查: - * - 领地主 / 插件全局管理员 / 领地管理员 → 无条件允许传送 - * - 其他玩家 → 必须已获得该领地的"传送"授权(area_land_permissions.permissions 中 allowTeleport=true) - */ - public boolean canTeleportToLand(Player p, AreaConfig ac) { - if (ac == null || p == null) return false; - if (isAreaAdmin(p)) return true; - if (samePlayer(p.getName(), ac.owner)) return true; - if (isLandAdmin(ac.name, p.getName())) return true; - // ★ 公共设施连控:公共设施启用时,所有访客自动获得传送权限(优先于 per-player 检查) - if (ac.isPublicBuilding) return true; - // ★ 访客传送权限:领地级别允许则放行(优先于per-player检查) - if (ac.allowVisitorTeleport) return true; - int landId = getLandIdFromDb(ac.name); - if (landId <= 0) return false; // 无法验证则拒绝 - Map perms = getPlayerPermMapExact(landId, p.getName()); - return perms.getOrDefault("allowTeleport", false); - } - - // ===== Per-Player 独立权限系统 ===== - - /** - * 获取玩家在指定领地的独立权限JSON - * 格式: {"denyMove":true,"denyPVP":false,...} - * 空字符串或null表示没有自定义权限(使用领地默认) - */ - public String getPlayerPermJson(int landId, String playerName) { - PermRow row = findPermRow(landId, playerName); - if (row == null || row.permissions == null) return ""; - return row.permissions; - } - - /** area_land_permissions 行(Java 端匹配用) */ - private static final class PermRow { - int id; - String playerName; - String role; - String permissions; - long expiresAt; - } - - /** - * 按 land_id 取出该领地全部权限行,在 Java 端做玩家名匹配: - * 精确匹配优先,其次 NFKC+ROOT 规范化匹配(normName)。 - * 替代 SQL COLLATE NOCASE —— 后者只折叠 ASCII,İ/ẞ/K 等变种字符不生效, - * 且命中多条大小写重复行时返回顺序不确定(跨平台易读到错误记录)。 - */ - private List findPermRows(int landId, String playerName) { - List exact = new ArrayList<>(); - if (dbConnection == null || landId <= 0 || playerName == null || playerName.isEmpty()) - return exact; - String want = normName(playerName); - List fuzzy = new ArrayList<>(); - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT id, player_name, role, permissions, expires_at " - + "FROM area_land_permissions WHERE land_id = ?"); - stmt.setInt(1, landId); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - PermRow r = new PermRow(); - r.id = rs.getInt("id"); - r.playerName = rs.getString("player_name"); - r.role = rs.getString("role"); - r.permissions = rs.getString("permissions"); - r.expiresAt = rs.getLong("expires_at"); - if (playerName.equals(r.playerName)) exact.add(r); - else if (want.equals(normName(r.playerName))) fuzzy.add(r); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - return exact; - } - exact.addAll(fuzzy); - return exact; - } - - /** 取匹配度最高的一行(精确匹配优先) */ - private PermRow findPermRow(int landId, String playerName) { - List rows = findPermRows(landId, playerName); - return rows.isEmpty() ? null : rows.get(0); - } - - /** - * ★ 身份/授权判定专用:仅精确匹配(区分大小写,2026-10-10)。 - * 本服离线模式:名字大小写不同 => UUID 不同 => 不同玩家。 - * 旧版 findPermRow 的 normName 模糊兜底会让 YoupaishidIfu 命中 youpaishidifu 的 - * admin/visitor 行,从而获得 ADMIN/VISITOR 豁免、绕过全部 deny* 限制。 - * normName 模糊行仅保留在写路径(定位旧行做 UPDATE/DELETE),两者不可混用。 - */ - private PermRow findPermRowExact(int landId, String playerName) { - if (dbConnection == null || landId <= 0 || playerName == null || playerName.isEmpty()) - return null; - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT id, player_name, role, permissions, expires_at " - + "FROM area_land_permissions WHERE land_id = ? AND player_name = ?"); - stmt.setInt(1, landId); - stmt.setString(2, playerName); - ResultSet rs = stmt.executeQuery(); - PermRow r = null; - if (rs.next()) { - r = new PermRow(); - r.id = rs.getInt("id"); - r.playerName = rs.getString("player_name"); - r.role = rs.getString("role"); - r.permissions = rs.getString("permissions"); - r.expiresAt = rs.getLong("expires_at"); - } - rs.close(); - stmt.close(); - return r; - } catch (SQLException e) { - return null; - } - } - - /** - * ★ per-player 权限读取(授权判定专用):精确匹配版(2026-10-10)。 - * getEffectiveDeny / canTeleportToLand 等授权路径必须用本方法, - * 防止大小写变种玩家读到他人 per-player 放行配置而绕过领地默认 deny。 - */ - public Map getPlayerPermMapExact(int landId, String playerName) { - Map map = new HashMap<>(); - PermRow row = findPermRowExact(landId, playerName); - if (row == null || row.permissions == null || row.permissions.isEmpty()) return map; - String clean = row.permissions.trim(); - if (clean.startsWith("{")) clean = clean.substring(1); - if (clean.endsWith("}")) clean = clean.substring(0, clean.length() - 1); - if (clean.isEmpty()) return map; - for (String pair : clean.split(",")) { - String[] kv = pair.split(":"); - if (kv.length == 2) { - String key = kv[0].trim().replace("\"", ""); - boolean val = Boolean.parseBoolean(kv[1].trim().replace("\"", "")); - map.put(key, val); - } - } - return map; - } - - /** - * 设置玩家在指定领地的独立权限JSON - */ - public void setPlayerPermJson(int landId, String playerName, String permJson) { - if (dbConnection == null) return; - String json = permJson != null ? permJson : ""; - try { - // ★ 先按“精确优先 + normName 规范化”定位已有行,再决定 UPDATE / INSERT: - // - 已有行:只更新 permissions,保留原 role(admin/member/visitor) - // - 无行:插入新记录,role 默认 visitor(非 member,防止误降级管理员) - // 注意:UNIQUE(land_id, player_name) 是 BINARY,直接 INSERT ON CONFLICT - // 在大小写/变种字符不一致时会新增重复行,导致后续读到错误记录。 - PermRow existing = findPermRow(landId, playerName); - if (existing != null) { - PreparedStatement stmt = dbConnection.prepareStatement( - "UPDATE area_land_permissions SET permissions = ? WHERE id = ?"); - stmt.setString(1, json); - stmt.setInt(2, existing.id); - stmt.executeUpdate(); - stmt.close(); - } else { - PreparedStatement stmt = dbConnection.prepareStatement( - "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " - + "VALUES (?, ?, 'visitor', ?, ?)"); - stmt.setInt(1, landId); - stmt.setString(2, playerName); - stmt.setString(3, json); - stmt.setLong(4, System.currentTimeMillis() / 1000); - stmt.executeUpdate(); - stmt.close(); - } - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 设置玩家权限失败: " + e.getMessage()); - } - } - - /** - * 获取玩家独立权限(解析JSON为Map) - */ - public Map getPlayerPermMap(int landId, String playerName) { - Map map = new HashMap<>(); - String json = getPlayerPermJson(landId, playerName); - if (json == null || json.isEmpty()) return map; - // 简单解析 {"key":true,"key2":false} - String clean = json.trim(); - if (clean.startsWith("{")) clean = clean.substring(1); - if (clean.endsWith("}")) clean = clean.substring(0, clean.length() - 1); - if (clean.isEmpty()) return map; - for (String pair : clean.split(",")) { - String[] kv = pair.split(":"); - if (kv.length == 2) { - String key = kv[0].trim().replace("\"", ""); - boolean val = Boolean.parseBoolean(kv[1].trim().replace("\"", "")); - map.put(key, val); - } - } - return map; - } - - /** - * 设置玩家独立权限中的单个权限项 - * @param permName 权限名(如 "denyMove") - * @param enabled true=启用限制(deny),false=取消限制(允许) - */ - public void setPlayerPerm(int landId, String playerName, String permName, boolean enabled) { - Map map = getPlayerPermMap(landId, playerName); - map.put(permName, enabled); - String json = mapToJson(map); - setPlayerPermJson(landId, playerName, json); - } - - /** - * 获取所有有独立权限的成员列表 - */ - public List> getLandMembersWithPerms(int landId) { - List> list = new ArrayList<>(); - if (dbConnection == null) return list; - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT player_name, role, permissions FROM area_land_permissions " - + "WHERE land_id = ?"); - stmt.setInt(1, landId); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - Map entry = new HashMap<>(); - entry.put("player", rs.getString("player_name")); - entry.put("role", rs.getString("role")); - entry.put("permissions", rs.getString("permissions")); - list.add(entry); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - // 忽略 - } - return list; - } - - /** - * 获取所有领地的成员权限数据(用于同步到PHP端) - */ - public List> getAllPermsForSync() { - List> list = new ArrayList<>(); - if (dbConnection == null) return list; - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT p.id, p.land_id, l.name AS land_name, p.player_name, p.role, " - + "p.permissions, p.granted_at, p.expires_at " - + "FROM area_land_permissions p " - + "JOIN area_lands l ON p.land_id = l.id " - + "ORDER BY p.land_id, p.player_name"); - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - Map entry = new HashMap<>(); - entry.put("land_id", rs.getInt("land_id")); - entry.put("land_name", rs.getString("land_name")); - entry.put("player_name", rs.getString("player_name")); - entry.put("role", rs.getString("role")); - entry.put("permissions", rs.getString("permissions")); - entry.put("granted_at", rs.getLong("granted_at")); - entry.put("expires_at", rs.getLong("expires_at")); - list.add(entry); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] getAllPermsForSync失败: " + e.getMessage()); - } - return list; - } - - /** - * 检查玩家是否是某领地的管理员(role='admin') - */ - public boolean isLandAdmin(String landName, String playerName) { - int landId = getLandIdFromDb(landName); - if (landId <= 0) return false; - // ★ 2026-10-10 精确匹配:normName 模糊会把大小写变种玩家判成本领地管理员而豁免全部 deny* - PermRow row = findPermRowExact(landId, playerName); - return row != null && "admin".equalsIgnoreCase(row.role); - } - - /** - * 设置/取消玩家的领地管理员身份 - */ - public void setLandAdmin(String landName, String playerName, boolean admin) { - int landId = getLandIdFromDb(landName); - if (landId <= 0) return; - if (dbConnection == null) return; - try { - String role = admin ? "admin" : "member"; - PermRow existing = findPermRow(landId, playerName); - if (existing != null) { - // 已有行:只改 role,保留 permissions - PreparedStatement stmt = dbConnection.prepareStatement( - "UPDATE area_land_permissions SET role = ? WHERE id = ?"); - stmt.setString(1, role); - stmt.setInt(2, existing.id); - stmt.executeUpdate(); - stmt.close(); - } else { - PreparedStatement stmt = dbConnection.prepareStatement( - "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " - + "VALUES (?, ?, ?, '', ?)"); - stmt.setInt(1, landId); - stmt.setString(2, playerName); - stmt.setString(3, role); - stmt.setLong(4, System.currentTimeMillis() / 1000); - stmt.executeUpdate(); - stmt.close(); - } - } catch (SQLException e) { - // 忽略 - } - } - - /** - * 拉黑时撤销该玩家在所有领地的管理员身份(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 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 - */ - public void insertLandPermission(String landName, String playerName, String role) { - int landId = getLandIdFromDb(landName); - if (landId <= 0 || dbConnection == null) return; - try { - // 已有任意大小写/变种形式的行则跳过,避免产生重复记录 - if (findPermRow(landId, playerName) != null) return; - PreparedStatement stmt = dbConnection.prepareStatement( - "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + - "VALUES (?, ?, ?, '', ?)"); - stmt.setInt(1, landId); - stmt.setString(2, playerName); - stmt.setString(3, role != null && !role.isEmpty() ? role : "visitor"); - stmt.setLong(4, System.currentTimeMillis() / 1000); - stmt.executeUpdate(); - stmt.close(); - } catch (SQLException e) { - // 忽略 - } - } - - /** - * ★ 核心方法:获取玩家的effective deny状态 - * 优先级:per-player权限 > 领地默认权限 - * @param permName 权限字段名(如 "denyMove") - * @return true=限制生效,false=允许 - */ - public boolean getEffectiveDeny(Player player, AreaConfig ac, String permName) { - // 如果ac为null(不在任何领地内),直接允许 - if (ac == null) return false; - - // ★ denyPVP / denyAllDamage 是全局安全权限,ADMIN/OWNER/VISITOR 也不例外 - // ★ denyWax(涂蜡/刮蜡)2026-09-29 起与其他 deny* 权限【对齐】,走下方四级判定: - // 领地主/插件管理员豁免 → per-player 授权 → 白名单访客豁免 → 普通访客回退领地默认。 - // 即"禁止涂蜡/刮蜡"只约束普通访客,已授权玩家/管理员/领地主不受影响(订正倒反天罡 bug)。 - boolean isSecurityPerm = "denyPVP".equals(permName) - || "denyAllDamage".equals(permName); - PermissionLevel level = isSecurityPerm ? null : getPermissionLevel(player, ac); - - // 1) ADMIN/OWNER 永远豁免 - if (level == PermissionLevel.ADMIN || level == PermissionLevel.OWNER) { - return false; - } - - // 2) per-player 显式权限【必须先于 VISITOR 豁免】: - // setPlayerPermJson 建行时 role 写死 'visitor'、expires_at 默认 0, - // 旧顺序会把"刚被关停权限的玩家"变成永久访客而直接豁免,关停形同虚设。 - int landId = getLandIdFromDb(ac.name); - if (landId > 0) { - // ★ 2026-10-10 精确匹配:防止大小写变种玩家读到他人 per-player 放行配置 - Map playerPerms = getPlayerPermMapExact(landId, player.getName()); - if (playerPerms.containsKey(permName)) { - return playerPerms.get(permName); - } - // ★ allowPickup/allowDrop 与 denyPickup/denyDrop 键名反转映射 - if ("denyPickup".equals(permName) && playerPerms.containsKey("allowPickup")) { - return !playerPerms.get("allowPickup"); - } - if ("denyDrop".equals(permName) && playerPerms.containsKey("allowDrop")) { - return !playerPerms.get("allowDrop"); - } - } - - // 2.5) 已授权成员(DB role='member')豁免领地默认权限 - // ★ 2026-10-11 修复严重bug:成员此前拿不到 ADMIN/OWNER/VISITOR 任一身份, - // getPermissionLevel 返回 null → 与纯路人一样回退领地默认 deny*, - // 被"跟随全局权限一起限制"(denyMove 传送、denyBlockBreak 拦截等全中)。 - // 成员是领地主显式授权的人,应豁免领地默认限制; - // per-player 显式配置仍在第 2 步优先命中,领地主可对单个成员单独收紧。 - if (level == PermissionLevel.MEMBER) { - return false; - } - - // 3) 访客豁免(该玩家没有任何 per-player 显式配置时才生效) - // ★ 2026-10-04 修复:denyMove 不再对访客豁免。 - // 原因:领地 GUI 的"移动"开关读的是 ac.denyMove(领地默认), - // 而访客在此处被无条件 return false,实际畅通无阻 —— 显示与行为脱节。 - // 访客若无 per-player 显式 denyMove(第 2 步未命中),就继续落到第 4 步, - // 按领地默认 denyMove 判定,与"移动 已禁用"的显示保持一致。 - // 其余 deny* 权限的访客豁免语义保持不变。 - if (level == PermissionLevel.VISITOR) { - if (!"denyMove".equals(permName)) { - return false; - } - } - - // 4) 回退到领地默认权限 - // ★ level==null(纯路人:无白名单、无权限行)本就走这里,行为正确,未改动。 - return getLandDefaultDeny(ac, permName); - } - - /** ★ 是否为刷怪蛋(生物蛋)物品 */ - private static boolean isSpawnEggItem(Material m) { - return m != null && m.name().endsWith("_SPAWN_EGG"); - } - - /** - * ★ 涂蜡/刮蜡权限是否生效: - * 2026-09-27 起改为【独立一级权限】:不再受"破坏方块/放置方块"上级开关约束。 - * 2026-09-29 订正:移出"安全权限"名单,与其他 deny* 权限【完全对齐】, - * 走 getEffectiveDeny 四级判定—— - * ① 领地主 / 插件管理员(含非OP领地管理员) 豁免; - * ② per-player 显式授权优先(已授权涂蜡刮蜡的普通玩家放行); - * ③ 白名单访客豁免; - * ④ 普通访客回退领地默认 denyWax("禁止涂蜡/刮蜡"只约束这一层)。 - * 此前按 denyPVP 处理导致 level 恒为 null、领地主和管理员也被拦(倒反天罡)。 - */ - private boolean isWaxDenied(Player player, AreaConfig ac) { - if (ac == null) return false; - return getEffectiveDeny(player, ac, "denyWax"); - } - - /** ★ 可涂蜡/刮蜡的方块(铜质方块、避雷针、告示牌) */ - private static boolean isWaxableBlock(Block b) { - if (b == null) return false; - String n = b.getType().name(); - if (n.contains("COPPER") && !n.contains("RAW_COPPER")) return true; - if (n.equals("LIGHTNING_ROD")) return true; - return n.endsWith("_SIGN"); - } - - /** - * 获取领地默认deny状态(按字段名映射) - */ - private boolean getLandDefaultDeny(AreaConfig ac, String permName) { - switch (permName) { - case "denyMove": return ac.denyMove; - case "denyBlockPlace": return ac.denyBlockPlace; - case "denyBlockBreak": return ac.denyBlockBreak; - case "denyContainer": return ac.denyContainer; - case "denyPVP": return ac.denyPVP; - case "denyFallDamage": return ac.denyFallDamage; - case "denyHunger": return ac.denyHunger; - case "denyAllDamage": return ac.denyAllDamage; - case "denyDrop": return !ac.allowDrop; - case "denyMount": return ac.denyMount; - case "denyEnderPearl": return ac.denyEnderPearl; - case "denyBow": return ac.denyBow; - case "denyPotion": return ac.denyPotion; - case "denyExplosion": return ac.denyExplosion; - case "denyRaid": return ac.denyRaid; - case "denyFireSpread": return ac.denyFireSpread; - case "denyAllEffects": return ac.denyAllEffects; - case "denyItemFrame": return ac.denyItemFrame; - case "denyEntityInteract": return ac.denyEntityInteract; - case "denyPickup": return !ac.allowPickup; - case "denyFire": return ac.denyFire; - case "denyThrownProjectiles": return ac.denyThrownProjectiles; - case "denyGlowing": return ac.denyGlowing; - case "denyRedstoneInteraction": return ac.denyRedstoneInteraction; - case "denyDoorInteraction": return ac.denyDoorInteraction; - case "denyNoteblockJukebox": return ac.denyNoteblockJukebox; - case "denyLead": return ac.denyLead; - case "denyCropHarvest": return ac.denyCropHarvest; - case "denyFarmlandTrample": return ac.denyFarmlandTrample; - case "denyEnderTeleport": return ac.denyEnderTeleport; - case "denyWoolShear": return ac.denyWoolShear; - case "denyAnimalFeeding": return ac.denyAnimalFeeding; - case "denyMobAttack": return ac.denyMobAttack; - case "denySignEdit": return ac.denySignEdit; - case "denySpawnEgg": return ac.denySpawnEgg; - case "denyVillagerTrade": return ac.denyVillagerTrade; - case "denyWax": return ac.denyWax; - case "peaceMode": return ac.peaceMode; - default: return false; - } - } - - /** - * Map转简单JSON - */ - private String mapToJson(Map map) { - if (map.isEmpty()) return ""; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry e : map.entrySet()) { - if (!first) sb.append(","); - sb.append("\"").append(e.getKey()).append("\":").append(e.getValue()); - first = false; - } - sb.append("}"); - return sb.toString(); - } - - /** - * 获取指定领地的成员列表 - */ - public Set getAreaMembers(String areaName) { - Set members = areaPlayerWhitelist.get(areaName); - return members != null ? new HashSet<>(members) : new HashSet<>(); - } - - // ===== 统一白名单检查(供所有事件处理器使用)===== - - /** - * 检查玩家是否在任意白名单中(全局白名单 + 区域白名单) - * 这是所有白名单判断的唯一入口 - */ - // ===== 统一玩家名规范化(修复大小写变种字符在 Windows/Linux 下比对结果不一致) ===== - - /** - * 统一玩家名规范化:NFKC 兼容归一 + Locale.ROOT 小写。 - * 规则固定,不受操作系统默认 Locale / JDK Unicode 版本影响, - * 保证同一玩家名在 Windows 开发环境与 Linux 面板服上归一结果完全相同。 - * 例:İ→i、ẞ→ß、K(Kelvin)→k、全角→半角。 - */ - public static String normName(String s) { - if (s == null || s.isEmpty()) return ""; - return java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFKC) - .toLowerCase(java.util.Locale.ROOT); - } - - /** - * 玩家身份比对(领地主 / 成员 / 上架人等“是不是同一个人”的判定): - * 精确匹配、区分大小写,禁止归一化。 - * - * 背景:本服为离线模式,名字大小写不同 => UUID 不同 => 是不同玩家。 - * 旧代码用 equalsIgnoreCase(相当于按小写归一化)比对领地主, - * 导致 YoUpaishidIfu 被判成领地主 youpaishidifu, - * 进而在 getPermissionLevel 里拿到 OWNER 豁免、直接跳过 denyMove 等限制(越权绕过)。 - * 与 8069070「取消归一化,按人头配置权限」保持一致。 - * - * 注意:normName 仍用于**定位数据库里那条权限行**(历史数据大小写不一致), - * 但**是否授予身份/豁免**必须走这里的精确比对,两者不可混用。 - */ - public static boolean samePlayer(String a, String b) { - return a != null && b != null && a.equals(b); - } - - /** - * 白名单查询:★ 精确匹配(2026-10-10 封堵大小写变种绕过)。 - * 离线模式大小写不同 => UUID 不同 => 不同玩家; - * 旧 normName 归一比对会让 YoupaishidIfu 命中 youpaishidifu 的白名单/访客身份, - * 从而豁免 denyBlockBreak 等全部限制。历史小写条目由 loadWhitelists 的 - * canonicalNameFromDb 规范名迁移兜底(用 DB 里记录的精确拼写回写)。 - */ - private static boolean whiteContains(Set set, String name) { - if (set == null || name == null || name.isEmpty()) return false; - return set.contains(name); - } - - private boolean isPlayerWhitelisted(String player, - AreaConfig ac) { - // 1. 全局白名单 - if (whiteContains(globalPlayerWhitelist, player)) - return true; - // 2. 区域白名单 - if (ac != null && whiteContains(areaPlayerWhitelist.get(ac.name), player)) - return true; - return false; - } - - /** - * 快速检查:仅全局白名单(不带 AreaConfig) - */ - private boolean isGlobalWhite(String player) { - return whiteContains(globalPlayerWhitelist, player); - } - - /** - * 快速检查:仅区域白名单 - */ - private boolean isAreaWhite(String player, String areaName) { - return whiteContains(areaPlayerWhitelist.get(areaName), player); - } - - /** - * 检查玩家是否为区域防护管理员 - * 与 Main.isAdmin() 使用相同的 ScoreboardTag 机制 - */ - public boolean isAreaAdmin(Player player) { - // ★ OP 即为全局领地管理员(封堵前仅认ScoreboardTag,导致非OP也能跨领地越权) - if (player.isOp()) return true; - - ConfigManager cfg = plugin.getConfigMgr(); - if (cfg == null) return false; - - String tag = cfg.areaProtectAdminTag; - if (tag == null || tag.isEmpty()) return false; - - // 使用ScoreboardTag验证(与Main.isAdmin()一致,仅作为OP之外的补充) - try { - return player.getScoreboardTags().contains(tag); - } catch (Exception e) { - return false; - } - } - - /** - * 检查CommandSender是否为区域防护管理员(支持控制台) - */ - public boolean isAreaAdmin(CommandSender sender) { - if (sender instanceof Player) { - return isAreaAdmin((Player) sender); - } - // 控制台视为管理员 - return true; - } - - /** - * 自动将无owner的txt迁移领地分配给第一个操作的管理员 - */ - private void assignUnownedLandsToAdmin(Player admin) { - File ownerMarker = new File(rootDir, ".need_owner_assign"); - if (!ownerMarker.exists()) return; - - String adminName = admin.getName(); - int assigned = 0; - - for (AreaConfig ac : areas.values()) { - if (ac.owner == null || ac.owner.isEmpty()) { - ac.owner = adminName; - saveAreaToDb(ac); - assigned++; - } - } - - // 删除标记文件 - ownerMarker.delete(); - - if (assigned > 0) { - admin.sendMessage("§a§l[防护] 已将 " + assigned + " 个无主领地分配给你"); - plugin.getLogger().info("[防护] txt迁移领地已分配给管理员: " + adminName + " 共" + assigned + "个"); - } - } - - private boolean isItemBlacklisted(String itemId, - AreaConfig ac) { - if (globalItemBlacklist.contains( - itemId.toUpperCase())) - return true; - Set areaList = - areaItemBlacklist.get(ac.name); - return areaList != null - && areaList.contains(itemId.toUpperCase()); - } - - // ==================== 没收 ==================== - - private int confiscateItem(Player p, String itemTypeId) { - Material mat = Material.matchMaterial(itemTypeId); - if (mat == null) return 0; - ItemStack[] contents = p.getInventory().getContents(); - int removed = 0; - for (int i = 0; i < contents.length; i++) { - if (contents[i] != null - && contents[i].getType() == mat) { - removed += contents[i].getAmount(); - contents[i] = null; - } - } - p.getInventory().setContents(contents); - return removed; - } - - // ==================== 边框显示 ==================== - private void showBorder(Player p, AreaConfig ac, boolean temp) { - cancelBorder(p); - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - int edgeLenX = maxX - minX + 1; - int edgeLenZ = maxZ - minZ + 1; - // 超大领地跳过3D边框粒子 - if (edgeLenX > 200 || edgeLenZ > 200) return; - int perimeter = 2 * (edgeLenX + edgeLenZ); - // 动态步长:保证每条边最多~25个粒子点 - final int step = Math.max(4, Math.min(10, perimeter / 100)); - // 动态层数:大领地减少层数(从6层降到2层) - final int dyStep = perimeter > 200 ? 10 : 5; - final int maxParticlesPerTick = 80; - - int taskId = Bukkit.getScheduler() - .runTaskTimer(plugin, new Runnable() { - @Override - public void run() { - if (!p.isOnline()) return; - Location loc = p.getLocation(); - int py = loc.getBlockY(); - int count = 0; - for (int dy = 5; dy <= 30 && count < maxParticlesPerTick; dy += dyStep) { - for (int x = minX; x <= maxX && count < maxParticlesPerTick; x += step) { - if (x == minX || x == maxX) { - p.spawnParticle(Particle.END_ROD, - x + 0.5, py + dy, minZ + 0.5, - 1, 0, 0, 0, 0); - p.spawnParticle(Particle.END_ROD, - x + 0.5, py + dy, maxZ + 0.5, - 1, 0, 0, 0, 0); - count += 2; - } - } - for (int z = minZ + step; z < maxZ && count < maxParticlesPerTick; z += step) { - p.spawnParticle(Particle.END_ROD, - minX + 0.5, py + dy, z + 0.5, - 1, 0, 0, 0, 0); - p.spawnParticle(Particle.END_ROD, - maxX + 0.5, py + dy, z + 0.5, - 1, 0, 0, 0, 0); - count += 2; - } - } - } - }, 0L, 20L).getTaskId(); - displayTaskIds.put(p.getUniqueId(), taskId); // taskId 是 int - if (temp) { - Bukkit.getScheduler().runTaskLater(plugin, - () -> cancelBorder(p), 300L); - } - } - - - private void cancelBorder(Player p) { - Integer taskId = displayTaskIds.remove(p.getUniqueId()); - if (taskId != null) { - Bukkit.getScheduler().cancelTask(taskId); - } - } - - public void onPlayerQuit(String playerName) { - playerCurrentArea.remove(playerName); - } - - public void clearAllPlayerAreas() { - playerCurrentArea.clear(); - } - private void saveModeExempt(AreaConfig ac) { - File file = new File(rootDir, ac.name + ".txt"); - if (!file.exists()) return; - - try { - List lines = new ArrayList<>(); - BufferedReader reader = new BufferedReader( - new FileReader(file, StandardCharsets.UTF_8)); - String line; - boolean found = false; - - while ((line = reader.readLine()) != null) { - String trimmed = line.trim(); - if (trimmed.contains("排除名单") - || trimmed.contains("模式排除") - || trimmed.contains("模式豁免")) { - StringBuilder sb = new StringBuilder(); - sb.append("排除名单:"); - boolean first = true; - for (String name : ac.modeExempt) { - if (!first) sb.append(","); - sb.append(name); - first = false; - } - lines.add(sb.toString()); - found = true; - } else { - lines.add(line); - } - } - reader.close(); - - if (!found && !ac.modeExempt.isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append("排除名单:"); - boolean first = true; - for (String name : ac.modeExempt) { - if (!first) sb.append(","); - sb.append(name); - first = false; - } - lines.add(sb.toString()); - } - - FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8); - for (String l : lines) { - writer.write(l + "\n"); - } - writer.close(); - } catch (IOException e) { - plugin.getLogger().warning( - "[防护] 保存模式排除名单失败: " + e.getMessage()); - } - } - - // ==================== 事件监听 ==================== - @EventHandler - public void onPlayerMove(PlayerMoveEvent event) { - Player p = event.getPlayer(); - UUID uid = p.getUniqueId(); - Location to = event.getTo(); - if (to == null) return; - - // ★ 未登录(冻结)玩家跳过领地移动判定: - // 登录系统冻结期间玩家本就无法移动,领地系统不应在此期间 - // 对其传送/施加效果;登录成功后由 onPlayerJoin 恢复区域效果。 - // (消除登录系统与领地系统在冻结态下的交叉干扰) - if (plugin.isFrozen(p)) return; - - Location from = event.getFrom(); - // X/Z debounce removed to allow denyMove teleport for small movements - - // 防抖 - Long lastTime = lastMoveProcess.get(uid); - long now = System.currentTimeMillis(); - if (lastTime != null - && now - lastTime < MOVE_DEBOUNCE_MS) { - return; - } - lastMoveProcess.put(uid, now); - - // 检测区域 - String oldArea = playerCurrentArea.get(uid); - String newArea = null; - String forcedMode = null; - - for (Map.Entry entry - : areas.entrySet()) { - AreaConfig ac = entry.getValue(); - if (!ac.world.equals(to.getWorld().getName())) - continue; - int px = to.getBlockX(); - int pz = to.getBlockZ(); - int py = to.getBlockY(); - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - if (px >= minX && px <= maxX - && pz >= minZ && pz <= maxZ - && py >= ac.yMin && py <= ac.yMax) { - newArea = entry.getKey(); - forcedMode = ac.enforceGameMode; - break; - } - } - - /* plugin.getLogger().info( - "[防护-移动] " + p.getName() - + " 旧=[" + oldArea - + "] 新=[" + newArea + "]" - + " areas.size=" + areas.size() - + " world=" + to.getWorld().getName());*/ - - // ===== 2. 区域没变 ===== - if (Objects.equals(oldArea, newArea)) { - if (newArea != null) { - AreaConfig ac = areas.get(newArea); - if (ac != null) { - // ★ denyMove检查:支持per-player独立权限 - // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免, - // 对无权限玩家则走领地默认 denyMove 配置 - if (getEffectiveDeny(p, ac, "denyMove")) { - Location safeLoc = findSafeExitLocation(p.getLocation(), ac); - if (safeLoc != null) { - p.teleport(safeLoc); - p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限,已被传送出去"); - } else { - p.teleport(p.getWorld().getSpawnLocation()); - p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限"); - } - return; - } else if (ac.denyMove) { - // 领地已开启移动限制但该玩家未被拦下 → 记录命中原因(10秒节流) - logDenyExempt(p, ac, "原地停留"); - } - - // ★ 每次移动都清除指定效果 - clearBadEffects(p, ac); - - // ★ 效果热更新:实时增删效果(2秒冷却) - hotUpdateRegionEffects(p, uid, newArea, ac); - - // ★ 每次移动都强制游戏模式 - if (ac.enforceGameMode != null - && !ac.enforceGameMode.isEmpty() - && !isExemptFromGameMode(p, newArea)) { - GameMode target = - GameMode.valueOf(ac.enforceGameMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - } - } - } - } - return; - } - - // 离开旧区域 - if (oldArea != null) { - /* plugin.getLogger().info( - "[防护-移动-离开] ★★★开始离开处理: - + oldArea);*/ - - AreaConfig oldAc = areas.get(oldArea); - - // 离开提示(默认消息:感谢(玩家)光临(领地)) - String leaveMsg = (oldAc != null && oldAc.leaveMsg != null && !oldAc.leaveMsg.isEmpty()) - ? oldAc.leaveMsg : "感谢(玩家)光临(领地)"; - if (oldAc != null) { - p.sendMessage(formatAreaMsg(leaveMsg, p.getName(), oldArea)); - } - - // ★ 离开领地时展示边框粒子(3秒) - if (oldAc != null) { - showBorderBrief(p, oldAc); - } - - // ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还 - handleConfiscateReturn(p, oldArea); - - // 收标 - List marked = - playerMarkedEffects.remove(uid); - - /* plugin.getLogger().info( - "[防护-移动-离开] ★★★收标: " - + (marked != null ? marked.size() : "null"));*/ - - if (marked != null) { - for (PotionEffectType t : marked) { - plugin.getLogger().info( - "[防护-移动-离开] 标记项: " + t.getName()); - } - } - - // 取消旧延时 - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - pendingClearArea.remove(uid); - pendingClearEffects.remove(uid); - plugin.getLogger().info( - "[防护-移动-离开] 取消旧延时"); - } - - // ★★★ 离开到野外(无新区域):立即清除领地给予的效果 ★★★ - if (newArea == null && oldAc != null - && oldAc.giveEffects != null && !oldAc.giveEffects.isEmpty()) { - plugin.getLogger().info( - "[防护-移动-离开野外] ★★★立即清除效果: 玩家=" + p.getName() - + " 区域=" + oldArea); - int cleared = 0; - for (String[] eff : oldAc.giveEffects) { - PotionEffectType type = resolveEffectType(eff[0]); - if (type != null) { - PotionEffect pe = p.getPotionEffect(type); - if (pe != null) { - p.removePotionEffect(type); - cleared++; - plugin.getLogger().info( - "[防护-移动-离开野外] ★清除: " + eff[0]); - } - } - } - removePlayerEffects(uid, oldArea); - plugin.getLogger().info( - "[防护-移动-离开野外] ★完成: 清除" - + cleared + "/" + oldAc.giveEffects.size() + "个效果"); - } - - // 创建延时清理(领地→领地跨区域差量清理) - if (marked != null && !marked.isEmpty() && newArea != null) { - String clearArea = oldArea; - UUID clearUid = uid; - List toClear = - new ArrayList<>(marked); - pendingClearArea.put(uid, clearArea); - pendingClearEffects.put(uid, toClear); - - BukkitTask task = new BukkitRunnable() { - @Override - public void run() { - pendingClearTask.remove(clearUid); - pendingClearArea.remove(clearUid); - - Player online = - Bukkit.getPlayer(clearUid); - - if (online == null || !online.isOnline()) { - removePlayerEffects( - clearUid, clearArea); - return; - } - - int cleared = 0; - for (PotionEffectType type : toClear) { - PotionEffect eff = - online.getPotionEffect(type); - if (eff != null) { - online.removePotionEffect(type); - cleared++; - } - } - removePlayerEffects(clearUid, clearArea); - plugin.getLogger().info( - "[防护-移动-清理] cleared " + cleared + "/" + toClear.size()); - } - }.runTaskLater(plugin, 100L); // 5秒 = 100 tick - pendingClearTask.put(uid, task); - } - } - - - // 进入新区域 - if (newArea != null) { - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - String oldP = pendingClearArea.remove(uid); - if (oldP != null) { - diffEffects(p, uid, oldP, newArea); - } - } - - AreaConfig newAc = areas.get(newArea); - if (newAc != null) { - // ★ denyMove检查:支持per-player独立权限 - // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免 - if (getEffectiveDeny(p, newAc, "denyMove")) { - Location safeLoc = findSafeExitLocation(event.getFrom(), newAc); - if (safeLoc != null) { - p.teleport(safeLoc); - p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限,已被传送出去"); - } else { - p.teleport(p.getWorld().getSpawnLocation()); - p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限"); - } - return; - } else if (newAc.denyMove) { - // 领地已开启移动限制但该玩家未被拦下 → 记录命中原因(10秒节流) - logDenyExempt(p, newAc, "进入区域"); - } - - // 进入提示(默认消息:欢迎(玩家)来到(领地)) - String enterMsg = (newAc.enterMsg != null && !newAc.enterMsg.isEmpty()) - ? newAc.enterMsg : "欢迎(玩家)来到(领地)"; - p.sendMessage(formatAreaMsg(enterMsg, p.getName(), newArea)); - // ★ 进入领地时展示边框粒子(3秒) - showBorderBrief(p, newAc); - // ★ 公共建筑设施:访客自动获得传送、免疫伤害、攻击敌对生物权限 - if (newAc.isPublicBuilding && !samePlayer(p.getName(), newAc.owner) && !isAreaAdmin(p)) { - p.sendMessage("§a§l[公共建筑] §f欢迎来到公共设施: §e" + newAc.name); - p.sendMessage("§7你在此区域享有: 传送、免疫伤害、攻击敌对生物权限"); - // 暂时移除伤害限制:给玩家5秒免疫效果 - p.addPotionEffect(new org.bukkit.potion.PotionEffect( - org.bukkit.potion.PotionEffectType.RESISTANCE, 100, 255, false, false, true)); - } - applyRegionEffects(p, uid, newArea, newAc); - // 清除效果 - clearBadEffects(p, newAc); - - } -// ===== 清除指定效果 ===== - if (newAc.clearEffects != null - && !newAc.clearEffects.isEmpty()) { - for (String effName : newAc.clearEffects) { - PotionEffectType type = - resolveEffectType(effName); - if (type != null && p.getPotionEffect(type) != null) { - p.removePotionEffect(type); - plugin.getLogger().info( - "[防护] ★清除效果: " + effName - + " 玩家=" + p.getName()); - } - } - } - -// ===== 清除负面效果(clearBadEffects 已在上方处理) ===== - // clearAllBadEffects 仅清除负面+中性效果,不清除正面效果 - // 此处不再单独处理,统一由上方 clearBadEffects() 负责 - - if (forcedMode != null - && !isExemptFromGameMode(p, newArea)) { - GameMode target = - GameMode.valueOf(forcedMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - } - } - } - - if (newArea != null) { - playerCurrentArea.put(uid, newArea); - } else { - playerCurrentArea.remove(uid); - } - } - - /** - * 诊断日志:领地已开启 denyMove,但该玩家判定为"允许移动"时, - * 打印命中的权限级别、白名单状态与权限行内容(每玩家10秒节流)。 - * 用于 Linux 面板服现场定位"关停权限后仍可移动"到底命中了哪条豁免分支。 - */ - private void logDenyExempt(Player p, AreaConfig ac, String scene) { - try { - long now = System.currentTimeMillis(); - Long last = denyExemptLogTs.get(p.getUniqueId()); - if (last != null && now - last < 10000L) return; - denyExemptLogTs.put(p.getUniqueId(), now); - - PermissionLevel lvl = getPermissionLevel(p, ac); - int landId = getLandIdFromDb(ac.name); - PermRow row = landId > 0 ? findPermRow(landId, p.getName()) : null; - String name = p.getName(); - plugin.getLogger().info("[防护][移动豁免诊断] 场景=" + scene - + " 领地=" + ac.name - + " 玩家=" + name - + " level=" + lvl - + " 全局管理员=" + isAreaAdmin(p) - + " 领地主=" + (ac.owner != null && samePlayer(name, ac.owner)) - + " 白名单=" + isPlayerWhitelisted(name, ac) - + " 访客行=" + (row == null ? "无" - : (row.playerName + "/" + row.role + "/exp=" + row.expiresAt + "/" + row.permissions)) - + " 领地默认denyMove=" + ac.denyMove); - } catch (Exception ignored) { - } - } - - /** - * 查找安全的领地外部传送位置 - */ - private Location findSafeExitLocation(Location loc, AreaConfig ac) { - int px = loc.getBlockX(), pz = loc.getBlockZ(); - int minX = Math.min(ac.x1, ac.x2), maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2), maxZ = Math.max(ac.z1, ac.z2); - - int distLeft = px - minX; - int distRight = maxX - px; - int distTop = pz - minZ; - int distBottom = maxZ - pz; - - int minDist = Math.min(Math.min(distLeft, distRight), Math.min(distTop, distBottom)); - - int exitX = px, exitZ = pz; - if (minDist == distLeft) exitX = minX - 3; - else if (minDist == distRight) exitX = maxX + 3; - else if (minDist == distTop) exitZ = minZ - 3; - else exitZ = maxZ + 3; - - // ★ 安全判定:头顶≥2格空气、脚下实体方块、距边界≥3格 - World world = loc.getWorld(); - if (world == null) return null; - for (int y = Math.min(ac.yMax, world.getMaxHeight() - 3); y >= Math.max(ac.yMin, world.getMinHeight() + 2); y--) { - Location check = new Location(world, exitX, y, exitZ); - Block feet = check.getBlock(); - Block head = world.getBlockAt(exitX, y + 1, exitZ); - Block head2 = world.getBlockAt(exitX, y + 2, exitZ); - // 脚下必须是实体方块 - if (!feet.getType().isSolid()) continue; - // 头顶2格必须是空气(可通过) - if (head.getType() != Material.AIR && head.getType() != Material.CAVE_AIR && head.getType() != Material.VOID_AIR) continue; - if (head2.getType() != Material.AIR && head2.getType() != Material.CAVE_AIR && head2.getType() != Material.VOID_AIR) continue; - // ★ 确保出口不在领地范围内(再往外推3格) - int finalX = exitX; - int finalZ = exitZ; - // 如果出口坐标还在领地内,继续往外推 - if (finalX >= minX && finalX <= maxX && finalZ >= minZ && finalZ <= maxZ) { - // 在minDist方向继续推 - if (minDist == distLeft) finalX = minX - 3; - else if (minDist == distRight) finalX = maxX + 3; - else if (minDist == distTop) finalZ = minZ - 3; - else finalZ = maxZ + 3; - } - return new Location(world, finalX + 0.5, y + 1, finalZ + 0.5); - } - // ★ 不满足条件,传送到世界出生点 - return world.getSpawnLocation().clone(); - } - -// ===== 清理袭击生物 ===== - - // 需要清理的袭击生物类型 - private static final Set RAID_MOBS = new HashSet<>(Arrays.asList( - "VEX", - "EVOKER", - "PILLAGER", - "VINDICATOR", - "WITCH", - "RAVAGER", - "PILLAGER" - )); - - @EventHandler - public void onEntitySpawn(EntitySpawnEvent event) { - Entity entity = event.getEntity(); - Location loc = entity.getLocation(); - - // 检查是否在防护区域内 - AreaConfig ac = getArea( - loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), - loc.getBlockZ()); - if (ac == null) return; - - // 禁止袭击:取消袭击事件 - if (ac.denyRaid) { - // 清理袭击生物 - String typeName = entity.getType().name(); - if (RAID_MOBS.contains(typeName)) { - event.setCancelled(true); - plugin.getLogger().info( - "[防护] ★清理袭击生物: " + typeName - + " 区域=" + ac.name); - } - } - - // 没收物品检测 - if (ac.confiscateItems != null - && !ac.confiscateItems.isEmpty()) { - if (entity instanceof Item) { - Item item = (Item) entity; - String itemType = item.getItemStack() - .getType().name(); - if (ac.confiscateItems.contains(itemType)) { - event.setCancelled(true); - plugin.getLogger().info( - "[防护] 没收违禁掉落物: " - + itemType); - } - } - } - } - - - /** - * 进入区域时发放效果 - * 流程:查DB → 有记录跳过写入 → 无记录写入DB - * → 给效果 → 贴标 - */ - private void applyRegionEffects(Player p, UUID uid, - String areaName, AreaConfig ac) { - /* plugin.getLogger().info( - "[防护-贴标] ★★★方法被调用! 玩家=" - + p.getName() + " 区域=" + areaName - + " ac=" + (ac != null) - + " 效果配置数=" - + (ac != null ? ac.giveEffects.size() : "null"));*/ - - if (ac == null) return; - // ★ 2026-10-04:负面配置拆出后,不能再只判 giveEffects 为空就返回 - boolean hasGive = ac.giveEffects != null && !ac.giveEffects.isEmpty(); - boolean hasBad = ac.badEffects != null && !ac.badEffects.isEmpty(); - if (!hasGive && !hasBad) return; - // 查DB:该玩家在此区域是否已有效果记录 - List dbEffects = - getPlayerEffectNames(uid, areaName); - - if (dbEffects.isEmpty()) { - // 无记录 → 写入DB - List allForDb = new ArrayList<>(ac.giveEffects); - if (ac.badEffects != null) allForDb.addAll(ac.badEffects); - for (String[] eff : allForDb) { - PotionEffectType t = - resolveEffectType(eff[0]); - if (t == null) continue; - saveSingleEffect(uid, p.getName(), - areaName, t.getName()); - } - plugin.getLogger().info( - "[防护] DB写入, 玩家=" + p.getName() - + " 区域=" + areaName); - } else { - plugin.getLogger().info( - "[防护] DB已有记录, 跳过写入, 玩家=" - + p.getName() - + " 区域=" + areaName); - } - - // 给效果 + 贴标 - // ★ 2026-10-04:负面配置已拆到 badEffects,两边都要给 - List allCfg = new ArrayList<>(ac.giveEffects); - if (ac.badEffects != null) allCfg.addAll(ac.badEffects); - List applied = new ArrayList<>(); - for (String[] eff : allCfg) { - try { - PotionEffectType type = - resolveEffectType(eff[0]); - if (type == null) continue; - // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 - int lv = clampEffectLevel(Integer.parseInt(eff[1]), eff[0], ac.owner) - 1; - int dur = Integer.parseInt(eff[2]) * 20; - - PotionEffect existing = - p.getPotionEffect(type); - if (existing != null - && existing.getAmplifier() >= lv) { - continue; - } - p.addPotionEffect( - new PotionEffect(type, dur, lv)); - applied.add(type); - } catch (Exception ignored) { - } - } - - // ★ 成就防作弊贴标:这批是插件给的效果,必须登记 - markPluginEffects(uid, applied); - - // 贴标 - // 新代码(合并,不覆盖): - List existingMarks = - playerMarkedEffects.get(uid); - if (existingMarks != null && !existingMarks.isEmpty()) { - // 已有标记,合并新增的 - for (PotionEffectType t : applied) { - if (!existingMarks.contains(t)) { - existingMarks.add(t); - } - } - /* plugin.getLogger().info( - "[防护-贴标] 合并标记: " - + existingMarks.size() - + " 个, 玩家=" + p.getName());*/ - } else { - // 无标记,新建 - playerMarkedEffects.put(uid, applied); - /* plugin.getLogger().info( - "[防护-贴标] 新建标记: " - + applied.size() - + " 个, 玩家=" + p.getName());*/ - } - } - - /** - * 效果热更新:玩家在领地内移动时,实时对比领地效果配置, - * 增删缺失/多余的效果(2秒冷却防止频繁计算) - */ - private void hotUpdateRegionEffects(Player p, UUID uid, - String areaName, AreaConfig ac) { - if (ac == null) return; - - // 冷却检查 - Long lastTime = lastEffectHotUpdate.get(uid); - long now = System.currentTimeMillis(); - if (lastTime != null && now - lastTime < EFFECT_HOT_UPDATE_INTERVAL_MS) { - return; - } - lastEffectHotUpdate.put(uid, now); - - // 领地无效果配置 → 跳过 - // ★ 2026-10-04:负面配置拆出后,不能只判 giveEffects - boolean hasGive = ac.giveEffects != null && !ac.giveEffects.isEmpty(); - boolean hasBad = ac.badEffects != null && !ac.badEffects.isEmpty(); - if (!hasGive && !hasBad) return; - - // 1. 构建领地当前期望的效果集合(增益 + 负面) - Map expectedEffects = new LinkedHashMap<>(); - List allForHot = new ArrayList<>(); - if (ac.giveEffects != null) allForHot.addAll(ac.giveEffects); - if (ac.badEffects != null) allForHot.addAll(ac.badEffects); - for (String[] eff : allForHot) { - PotionEffectType type = resolveEffectType(eff[0]); - if (type == null) continue; - try { - // ★ 药效等级封顶(2026-10-04 任务4),与首次给予同一口径 - int lv = clampEffectLevel(Integer.parseInt(eff[1]), eff[0], ac.owner) - 1; // MC amplifier从0开始 - int dur = Integer.parseInt(eff[2]) * 20; // 秒→tick - expectedEffects.put(type, new int[]{lv, dur}); - } catch (NumberFormatException ignored) {} - } - - // 2. 获取当前标记的效果(即本领地给予的效果) - List marked = playerMarkedEffects.get(uid); - Set currentMarked = (marked != null) - ? new HashSet<>(marked) : new HashSet<>(); - - // 3. 添加缺失的效果 - List newMarked = new ArrayList<>(currentMarked); - List hotApplied = new ArrayList<>(); - for (Map.Entry entry : expectedEffects.entrySet()) { - PotionEffectType type = entry.getKey(); - int lv = entry.getValue()[0]; - int dur = entry.getValue()[1]; - - if (currentMarked.contains(type)) { - // 已标记 → 检查等级是否需要更新 - PotionEffect existing = p.getPotionEffect(type); - if (existing != null && existing.getAmplifier() < lv) { - // 等级提升 → 重新给效果 - p.addPotionEffect(new PotionEffect(type, dur, lv)); - hotApplied.add(type); - plugin.getLogger().info("[防护-热更新] 等级提升: " - + type.getName() + " Lv" + (lv + 1) - + " 玩家=" + p.getName()); - } - continue; - } - - // 新效果 → 添加 - PotionEffect existing = p.getPotionEffect(type); - if (existing == null || existing.getAmplifier() < lv) { - p.addPotionEffect(new PotionEffect(type, dur, lv)); - newMarked.add(type); - hotApplied.add(type); - plugin.getLogger().info("[防护-热更新] 新增效果: " - + type.getName() + " Lv" + (lv + 1) + " " + dur / 20 + "s" - + " 玩家=" + p.getName()); - } - } - - // ★ 成就防作弊贴标:热更新补上的效果同样是插件给的 - markPluginEffects(uid, hotApplied); - - // 4. 移除多余的效果(领地不再给予的) - List toRemove = new ArrayList<>(); - for (PotionEffectType type : currentMarked) { - if (!expectedEffects.containsKey(type)) { - // 该效果不在领地配置中 → 移除 - PotionEffect existing = p.getPotionEffect(type); - if (existing != null) { - p.removePotionEffect(type); - toRemove.add(type); - plugin.getLogger().info("[防护-热更新] 移除效果: " - + type.getName() - + " 玩家=" + p.getName()); - } - } - } - - // 5. 更新标记 - if (!toRemove.isEmpty() || !newMarked.equals(currentMarked)) { - List updatedMarked = new ArrayList<>(newMarked); - updatedMarked.removeAll(toRemove); - if (updatedMarked.isEmpty()) { - playerMarkedEffects.remove(uid); - } else { - playerMarkedEffects.put(uid, updatedMarked); - } - } - } - - /** - * 离开区域时,延时5秒清理效果 - * 流程:收标 → 5秒后清效果 → 确认清空 → 清DB - */ - private void scheduleEffectClear(Player p, UUID uid, - String areaName) { - // 收标 - List marked = - playerAppliedEffects.remove(uid); - - plugin.getLogger().info( - "[防护] 收标 " + (marked != null ? marked.size() : 0) - + " 个, 玩家=" + p.getName() - + " 区域=" + areaName); - - // 启动5秒延时 - pendingClearArea.put(uid, areaName); - BukkitTask task = new BukkitRunnable() { - @Override - public void run() { - pendingClearTask.remove(uid); - pendingClearArea.remove(uid); - pendingClearEffects.remove(uid); - - // 清除效果 - if (marked != null) { - for (PotionEffectType type : marked) { - p.removePotionEffect(type); - } - } - - // ★ 成就防作弊:离开领地后插件不再持有这些效果,标记要一并撤掉, - // 否则玩家之后自己喝药水凑齐 34 种时会被误判成作弊。 - if (plugin.achievementEffectGuard != null) { - plugin.achievementEffectGuard.clearPluginEffects(uid, null); - } - - // 确认清空后清DB - removePlayerEffects(uid, areaName); - plugin.getLogger().info( - "[防护] 5秒已到, 清除 " - + (marked != null ? marked.size() : 0) - + " 个效果 + DB, 玩家=" - + p.getName() - + " 区域=" + areaName); - } - }.runTaskLater(plugin, 100L); // 5秒 = 100 tick - - pendingClearTask.put(uid, task); - } - public Set getAllAreaNames() { - return areas.keySet(); - } - - /** - * 5秒内进了新区域: - * 清除旧区域有、新区域没有的效果 - */ - private void diffEffects(Player p, UUID uid, - String oldAreaName, String newAreaName) { - AreaConfig oldAc = areas.get(oldAreaName); - AreaConfig newAc = areas.get(newAreaName); - if (oldAc == null || newAc == null) return; - - // ★ 从 pendingClearEffects 获取旧标记 - List oldMarked = - pendingClearEffects.remove(uid); - - if (oldMarked == null || oldMarked.isEmpty()) { - plugin.getLogger().info( - "[防护-跨区域] 无待清理效果"); - return; - } - - // 新区域的效果集合 - Set newTypes = new HashSet<>(); - if (newAc != null) { - if (newAc.giveEffects != null) { - for (String[] eff : newAc.giveEffects) { - PotionEffectType t = resolveEffectType(eff[0]); - if (t != null) newTypes.add(t); - } - } - // ★ 2026-10-04:负面配置拆到 badEffects,跨区域比对必须一起算 - if (newAc.badEffects != null) { - for (String[] eff : newAc.badEffects) { - PotionEffectType t = resolveEffectType(eff[0]); - if (t != null) newTypes.add(t); - } - } - } - - int cleared = 0; - for (PotionEffectType type : oldMarked) { - if (!newTypes.contains(type)) { - p.removePotionEffect(type); - cleared++; - plugin.getLogger().info( - "[防护-跨区域] 清除: " - + type.getName()); - } else { - plugin.getLogger().info( - "[防护-跨区域] 保留: " - + type.getName() - + " (新区域也有)"); - } - } - - removePlayerEffects(uid, oldAreaName); - plugin.getLogger().info( - "[防护-跨区域] 完成: 清除" - + cleared + "/" + oldMarked.size() - + " 个, " + oldAreaName - + " → " + newAreaName); - } - -// ===== DB 辅助方法(简化版)===== - - /** - * 查询玩家在某个区域的效果记录(返回英文名列表) - */ - private List getPlayerEffectNames( - UUID uid, String areaName) { - List list = new ArrayList<>(); - try { - PreparedStatement ps = dbConnection.prepareStatement( - "SELECT effect_type FROM player_effects " - + "WHERE uuid=? AND area_name=?"); - ps.setString(1, uid.toString()); - ps.setString(2, areaName); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - list.add(rs.getString("effect_type")); - } - rs.close(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 查询效果失败: " + e.getMessage()); - } - return list; - } - - /** - * 写入单条效果记录 - */ - private void saveSingleEffect(UUID uid, - String playerName, String areaName, - String effectType) { - try { - PreparedStatement ps = dbConnection.prepareStatement( - "INSERT INTO player_effects " - + "(uuid, player_name, area_name, " - + "effect_type, effect_level, " - + "effect_duration, enter_time) " - + "VALUES (?,?,?,?,?,?,?)"); - ps.setString(1, uid.toString()); - ps.setString(2, playerName); - ps.setString(3, areaName); - ps.setString(4, effectType); - ps.setInt(5, 0); - ps.setInt(6, 999); - ps.setLong(7, System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); - } catch (SQLException e) { - plugin.getLogger().warning( - "[防护] 写入效果失败: " + e.getMessage()); - } - } - - // ==================== 违禁品没收 ==================== - - /** - * 检查玩家是否豁免于没收 - * 全局白名单 / 区域白名单 / modeExempt / OP 都跳过 - */ - public boolean isExemptFromConfiscation( - Player p, String areaName) { - AreaConfig ac = areas.get(areaName); - // 管理员和领地所有者免检 - if (hasPermission(p, ac, PermissionLevel.OWNER)) return true; - // modeExempt名单也免检 - if (ac != null && ac.modeExempt.contains(p.getName())) - return true; - return false; - } - - - private String getChineseGameMode(GameMode mode) { - switch (mode) { - case SURVIVAL: return "生存模式"; - case CREATIVE: return "创造模式"; - case ADVENTURE: return "冒险模式"; - case SPECTATOR: return "旁观模式"; - default: return mode.name(); - } - } - - /** - * ★ 贴标:把「这批效果是插件给的」登记到成就防作弊守卫。 - * - *

2026-10-04 新增。原先只有 {@code playerMarkedEffects} 这份内部标记 - * (用于离场清理),现在额外同步一份到 {@link AchievementEffectGuard}, - * 后者据此撤销 potion 类原版成就(all_potions / all_effects)。

- */ - private void markPluginEffects(UUID uid, List types) { - if (plugin.achievementEffectGuard == null || uid == null || types == null || types.isEmpty()) return; - try { - plugin.achievementEffectGuard.markPluginEffects(uid, types); - } catch (Throwable ignored) { } - } - - /** - * 给予领地配置的全部效果(增益 giveEffects + 负面 badEffects) - * - *

2026-10-04:负面效果从 {@code giveEffects} 拆到 {@code badEffects}, - * 这里是两个列表的唯一合流点,改任何一处施加点都要记得带上 {@code badEffects}。

- * - * @return 实际施加成功(已解析出类型)的效果类型列表,供贴标使用 - */ - private List giveRegionEffects(Player p, AreaConfig ac) { - List given = new ArrayList<>(); - if (ac == null) return given; - if (ac.giveEffects != null) given.addAll(applyEffectList(p, ac, ac.giveEffects)); - if (ac.badEffects != null) given.addAll(applyEffectList(p, ac, ac.badEffects)); - return given; - } - - /** - * 施加一份效果列表([名字, 等级, 秒数]),统一走等级封顶 - */ - private List applyEffectList(Player p, AreaConfig ac, List list) { - List applied = new ArrayList<>(); - if (list == null) return applied; - for (String[] parts : list) { - if (parts == null || parts.length < 1 || parts[0] == null || parts[0].isEmpty()) continue; - String effName = parts[0]; - int level = 1; - int duration = 999; - try { level = Integer.parseInt(parts[1]); } - catch (Exception ignored) {} - try { duration = Integer.parseInt(parts[2]); } - catch (Exception ignored) {} - // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 - level = clampEffectLevel(level, effName, ac.owner); - PotionEffectType type = resolveEffectType(effName); - if (type != null) { - p.addPotionEffect(new PotionEffect( - type, duration * 20, level - 1)); - applied.add(type); - } - } - return applied; - } - - private void handleEnter(Player p, AreaConfig ac) { - UUID uid = p.getUniqueId(); - - // ★ 2026-10-04 关键顺序修正:负面配置拆到 badEffects 后, - // clearBadEffects 会把 badEffects 里配的负面一并清掉。 - // 所以「先给效果、再按配置清负面」——否则新增的负面效果子菜单等于白配。 - List given = giveRegionEffects(p, ac); - - // 清除负面效果(clearAllBadEffects / peaceMode / clearEffects / denyRaid) - clearBadEffects(p, ac); - - // ★ 贴标:这些效果是插件给的,不是玩家自己喝出来的。 - // 成就防作弊(撤销 nether/all_effects 等)完全依赖这份标记。 - markPluginEffects(uid, given); - - removePlayerEffects(uid, ac.name); - - // 白名单检查(最先执行) - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - - // 游戏模式切换(只对非白名单玩家) - if (ac.enforceGameMode != null) { - boolean exempt = isPlayerExemptFromModeChange(p, ac); - if (!exempt) { - GameMode target = GameMode.valueOf(ac.enforceGameMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - p.sendMessage("§e§l[区域防护] §f游戏模式已切换为: " - + getChineseGameMode(target)); - } - } - } - - // 和平模式/袭击检查 - if (ac.peaceMode) { - banHostilesWithWhitelist(p, ac); - } else if (ac.denyRaid) { - banRaidMobs(p, ac); - } - - handleConfiscate(p, ac); - } - - private boolean isPlayerExemptFromModeChange( - Player p, AreaConfig ac) { - // if (p.isOp()) return true; - // 1. 全局白名单(★ 精确匹配,2026-10-10) - if (whiteContains(globalPlayerWhitelist, p.getName())) { - return true; - } - // 2. 区域白名单 - Set areaWl = areaPlayerWhitelist.get(ac.name); - if (whiteContains(areaWl, p.getName())) { - return true; - } - if (ac.modeExempt.contains(p.getName())) { - return true; - } - return false; - } - - private void banRangedHostiles(Player p, AreaConfig ac) { - for (Entity ent : p.getNearbyEntities(48, 48, 48)) { - if (isRangedHostile(ent.getType().name())) { - Location voidLoc = ent.getLocation().clone(); - voidLoc.setY(ent.getWorld().getMinHeight() - 50); - ent.teleport(voidLoc); - } - } - } - - /** - * 按配置清除效果 - * denyRaid → 清除袭击相关 - * denyAllEffects → 清除所有负面+中性 - * denyAllEffects > denyRaid(全清优先) - */ - private void clearBadEffects(Player p, AreaConfig ac) { - // 和平模式 > 清除所有效果 > 禁止单个 - if (ac.peaceMode || ac.clearAllBadEffects) { - PotionEffectType[] allBad = { - PotionEffectType.SLOWNESS, - PotionEffectType.MINING_FATIGUE, - PotionEffectType.INSTANT_DAMAGE, - PotionEffectType.NAUSEA, - PotionEffectType.BLINDNESS, - PotionEffectType.HUNGER, - PotionEffectType.WEAKNESS, - PotionEffectType.POISON, - PotionEffectType.WITHER, - PotionEffectType.LEVITATION, - PotionEffectType.UNLUCK, - PotionEffectType.DARKNESS, - PotionEffectType.WIND_CHARGED, - PotionEffectType.WEAVING, - PotionEffectType.OOZING, - PotionEffectType.INFESTED, - PotionEffectType.BAD_OMEN, - PotionEffectType.TRIAL_OMEN, - PotionEffectType.RAID_OMEN - }; - for (PotionEffectType type : allBad) { - p.removePotionEffect(type); - } - return; - } - - // 禁止袭击 → 自动关联清除 - if (ac.denyRaid) { - p.removePotionEffect(PotionEffectType.BAD_OMEN); - p.removePotionEffect(PotionEffectType.RAID_OMEN); - } - - // 配置文件单个清除 - for (String effName : ac.clearEffects) { - PotionEffectType type = resolveEffectType(effName); - if (type != null) { - p.removePotionEffect(type); - } - } - } - - - private PotionEffectType resolveEffectType(String name) { - if (name == null || name.isEmpty()) return null; - - // 去除首尾空白、引号、反斜杠等残留转义字符(PHP JSON存储偶尔带入的\前缀) - String clean = name.trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - if (clean.isEmpty()) return null; - - // 去掉"效果"后缀 - if (clean.endsWith("效果")) { - clean = clean.substring(0, clean.length() - 2); - } - - // ★ 直接用 PotionEffectType 常量映射(不依赖 getByName,兼容所有 MC 版本) - // 中文名、英文ID(大写/小写/snake_case)全部在此处理 - switch (clean) { - // ========== 正面 ========== - // 速度 - case "迅捷": case "速度": - case "speed": case "SPEED": - return PotionEffectType.SPEED; - // 急迫 - case "急迫": - case "haste": case "HASTE": - return PotionEffectType.HASTE; - // 力量 - case "力量": - case "strength": case "STRENGTH": - case "INCREASE_DAMAGE": - return PotionEffectType.STRENGTH; - // 瞬间治疗 - case "瞬间治疗": case "治疗": - case "instant_health": case "INSTANT_HEALTH": - case "HEAL": - return PotionEffectType.INSTANT_HEALTH; - // 跳跃提升 - case "跳跃提升": - case "jump_boost": case "JUMP_BOOST": - case "JUMP": - return PotionEffectType.JUMP_BOOST; - // 生命恢复/再生/回复 - case "生命恢复": case "再生": case "回复": - case "regeneration": case "REGENERATION": - return PotionEffectType.REGENERATION; - // 抗性提升 - case "抗性提升": - case "resistance": case "RESISTANCE": - case "DAMAGE_RESISTANCE": - return PotionEffectType.RESISTANCE; - // 抗火 - case "抗火": - case "fire_resistance": case "FIRE_RESISTANCE": - return PotionEffectType.FIRE_RESISTANCE; - // 水下呼吸 - case "水下呼吸": - case "water_breathing": case "WATER_BREATHING": - return PotionEffectType.WATER_BREATHING; - // 隐身 - case "隐身": - case "invisibility": case "INVISIBILITY": - return PotionEffectType.INVISIBILITY; - // 夜视 - case "夜视": - case "night_vision": case "NIGHT_VISION": - return PotionEffectType.NIGHT_VISION; - // 发光 - case "发光": - case "glowing": case "GLOWING": - return PotionEffectType.GLOWING; - // 生命提升 - case "生命提升": - case "health_boost": case "HEALTH_BOOST": - return PotionEffectType.HEALTH_BOOST; - // 伤害吸收 - case "伤害吸收": case "吸收": - case "absorption": case "ABSORPTION": - return PotionEffectType.ABSORPTION; - // 饱和 - case "饱和": - case "saturation": case "SATURATION": - return PotionEffectType.SATURATION; - // 幸运 - case "幸运": - case "luck": case "LUCK": - return PotionEffectType.LUCK; - // 村庄英雄 - case "村庄英雄": - case "hero_of_the_village": case "HERO_OF_THE_VILLAGE": - return PotionEffectType.HERO_OF_THE_VILLAGE; - // 缓降 - case "缓降": - case "slow_falling": case "SLOW_FALLING": - return PotionEffectType.SLOW_FALLING; - // 潮涌能量 - case "潮涌能量": - case "conduit_power": case "CONDUIT_POWER": - return PotionEffectType.CONDUIT_POWER; - // 海豚的恩惠 - case "海豚的恩惠": - case "dolphins_grace": case "DOLPHINS_GRACE": - return PotionEffectType.DOLPHINS_GRACE; - // ========== 负面 ========== - // 缓慢 - case "缓慢": - case "slowness": case "SLOWNESS": - case "SLOW": - return PotionEffectType.SLOWNESS; - // 挖掘疲劳 - case "挖掘疲劳": - case "mining_fatigue": case "MINING_FATIGUE": - case "SLOW_DIGGING": - return PotionEffectType.MINING_FATIGUE; - // 瞬间伤害 - case "瞬间伤害": case "伤害药水": - case "instant_damage": case "INSTANT_DAMAGE": - case "HARM": - return PotionEffectType.INSTANT_DAMAGE; - // 反胃 - case "反胃": - case "nausea": case "NAUSEA": - return PotionEffectType.NAUSEA; - // 失明 - case "失明": - case "blindness": case "BLINDNESS": - return PotionEffectType.BLINDNESS; - // 饥饿 - case "饥饿": - case "hunger": case "HUNGER": - return PotionEffectType.HUNGER; - // 虚弱 - case "虚弱": - case "weakness": case "WEAKNESS": - return PotionEffectType.WEAKNESS; - // 中毒/致命中毒 - case "中毒": case "致命中毒": - case "poison": case "POISON": - case "poisonous": - return PotionEffectType.POISON; - // 凋零 - case "凋零": - case "wither": case "WITHER": - return PotionEffectType.WITHER; - // 飘浮/漂浮 - case "飘浮": case "漂浮": - case "levitation": case "LEVITATION": - return PotionEffectType.LEVITATION; - // 霉运/厄运 - case "霉运": case "厄运": case "不幸": - case "unluck": case "UNLUCK": - return PotionEffectType.UNLUCK; - // 黑暗 - case "黑暗": - case "darkness": case "DARKNESS": - return PotionEffectType.DARKNESS; - // 蓄风 - case "蓄风": - case "wind_charged": case "WIND_CHARGED": - return PotionEffectType.WIND_CHARGED; - // 盘丝 - case "盘丝": - case "weaving": case "WEAVING": - return PotionEffectType.WEAVING; - // 渗浆 - case "渗浆": - case "oozing": case "OOZING": - return PotionEffectType.OOZING; - // 寄生 - case "寄生": - case "infested": case "INFESTED": - return PotionEffectType.INFESTED; - // ========== 中性 ========== - // 不祥之兆 - case "不祥之兆": case "不祥征兆": - case "bad_omen": case "BAD_OMEN": - return PotionEffectType.BAD_OMEN; - // 袭击之兆 - case "袭击之兆": case "袭击征兆": - case "raid_omen": case "RAID_OMEN": - return PotionEffectType.RAID_OMEN; - // 试炼之兆 - case "试炼之兆": case "试炼征兆": - case "trial_omen": case "TRIAL_OMEN": - return PotionEffectType.TRIAL_OMEN; - default: - break; - } - - // ★ 终极兜底:用原版 getByName(兼容未来新增效果) - PotionEffectType fallback = PotionEffectType.getByName(clean.toLowerCase()); - if (fallback != null) return fallback; - - // 同一名称只警告一次,避免控制台刷屏 - if (UNKNOWN_EFFECT_WARNINGS.add(name)) { - plugin.getLogger().warning("[防护] 无法识别效果: " + name + " (clean=" + clean + "),已抑制后续相同警告"); - } - return null; - - } - - - private static final Map EFFECT_MAP - = new HashMap<>(); - - - private void clearRaidEffects(Player p) { - boolean cleared = false; - try { - if (p.hasPotionEffect(PotionEffectType.BAD_OMEN)) { - p.removePotionEffect(PotionEffectType.BAD_OMEN); - cleared = true; - } - } catch (Exception ignored) { - } - try { - if (p.hasPotionEffect(PotionEffectType.RAID_OMEN)) { - p.removePotionEffect(PotionEffectType.RAID_OMEN); - cleared = true; - } - } catch (Exception ignored) { - } - if (cleared) { - p.sendMessage("§c§l[区域防护] §f已清除袭击相关效果"); - } - } - - public Map> - getPendingClearEffects() { - return pendingClearEffects; - } - - public void onPlayerOffline(UUID uid, String playerName, - String areaName) { - // 清除该玩家在该区域的所有效果 - List given = getPlayerEffects(uid, areaName); - for (PotionEffectType type : given) { - // 只清除我们给予的效果 - Player p = Bukkit.getPlayer(uid); - if (p != null) { - p.removePotionEffect(type); - } - } - // 清除DB记录 - removePlayerEffects(uid, areaName); - plugin.getLogger().info("[防护] " + playerName - + " 下线,已清除" + given.size() - + "个效果并清理DB"); - } - - @EventHandler - public void onTeleport(PlayerTeleportEvent e) { - Location from = e.getFrom(); - Location to = e.getTo(); - if (to == null) return; - - if (from.getWorld() == to.getWorld() - && from.distance(to) < 5) return; - - Player p = e.getPlayer(); - UUID uid = p.getUniqueId(); - - // 标记传送中 - teleporting.add(uid); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - teleporting.remove(uid); - }, 5L); - - AreaConfig fromArea = getArea( - from.getWorld().getName(), - from.getBlockX(), from.getBlockY(), - from.getBlockZ()); - AreaConfig toArea = getArea( - to.getWorld().getName(), - to.getBlockX(), to.getBlockY(), - to.getBlockZ()); - - String fromName = fromArea != null - ? fromArea.name : null; - String toName = toArea != null - ? toArea.name : null; - - /* plugin.getLogger().info( - "[防护-传送] ★★★进入onTeleport: " - + p.getName() - + " [" + fromName + "] → [" + toName + "]" - + " teleporting=" + teleporting.contains(uid));*/ - - if (Objects.equals(fromName, toName)) { - /* plugin.getLogger().info( - "[防护-传送] 区域相同, 跳过");*/ - return; - } - - // ===== 离开旧区域 ===== - if (fromName != null) { - /* plugin.getLogger().info( - "[防护-传送-离开] ★★★开始离开处理: " - + fromName);*/ - - // 收标 - List marked = - playerMarkedEffects.remove(uid); - - /* plugin.getLogger().info( - "[防护-传送-离开] ★★★收标结果: " - + (marked != null ? marked.size() : "null"));*/ - - if (marked != null) { - for (PotionEffectType t : marked) { - /* plugin.getLogger().info( - "[防护-传送-离开] 标记项: " - + t.getName());*/ - } - } else { - /* plugin.getLogger().info( - "[防护-传送-离开] ★★标记为null! " - + "尝试其他key查找...");*/ - // 遍历查找可能的标记 - for (Map.Entry> entry - : playerMarkedEffects.entrySet()) { - if (entry.getKey().equals(uid)) { - /* plugin.getLogger().info( - "[防护-传送-离开] 找到标记! 效果数=" - + entry.getValue().size());*/ - } - } - /* plugin.getLogger().info( - "[防护-传送-离开] playerMarkedEffects大小=" - + playerMarkedEffects.size());*/ - } - - // 取消旧延时 - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - pendingClearArea.remove(uid); - pendingClearEffects.remove(uid); - } - - // ★★★ 传送到野外(无目标区域):立即清除领地效果 ★★★ - if (toName == null && fromArea != null - && fromArea.giveEffects != null && !fromArea.giveEffects.isEmpty()) { - plugin.getLogger().info( - "[防护-传送-离开野外] ★★★立即清除效果: 玩家=" + p.getName() - + " 区域=" + fromName); - int cleared = 0; - for (String[] eff : fromArea.giveEffects) { - PotionEffectType type = resolveEffectType(eff[0]); - if (type != null) { - PotionEffect pe = p.getPotionEffect(type); - if (pe != null) { - p.removePotionEffect(type); - cleared++; - plugin.getLogger().info( - "[防护-传送-离开野外] ★清除: " + eff[0]); - } - } - } - removePlayerEffects(uid, fromName); - plugin.getLogger().info( - "[防护-传送-离开野外] ★完成: 清除" - + cleared + "/" + fromArea.giveEffects.size() + "个效果"); - } - - // 创建5秒延时清理(传送跨领地差量清理) - if (marked != null && !marked.isEmpty() && toName != null) { - String clearArea = fromName; - UUID clearUid = uid; - List toClear = - new ArrayList<>(marked); - pendingClearArea.put(uid, clearArea); - pendingClearEffects.put(uid, toClear); - - BukkitTask task = new BukkitRunnable() { - @Override - public void run() { - pendingClearTask.remove(clearUid); - pendingClearArea.remove(clearUid); - - Player online = - Bukkit.getPlayer(clearUid); - - if (online == null - || !online.isOnline()) { - plugin.getLogger().info( - "[防护-传送-清理] 玩家不在线, 清DB"); - removePlayerEffects( - clearUid, clearArea); - return; - } - - int cleared = 0; - for (PotionEffectType type : toClear) { - PotionEffect eff = - online.getPotionEffect(type); - if (eff != null) { - online.removePotionEffect(type); - cleared++; - } - } - removePlayerEffects(clearUid, clearArea); - plugin.getLogger().info( - "[防护-传送-清理] cleared " - + cleared + "/" + toClear.size()); - } - }.runTaskLater(plugin, 100L); // 5秒 = 100 tick - pendingClearTask.put(uid, task); - - } - - // 离开提示(默认消息:感谢(玩家)光临(领地)) - String leaveMsg2 = (fromArea != null && fromArea.leaveMsg != null && !fromArea.leaveMsg.isEmpty()) - ? fromArea.leaveMsg : "感谢(玩家)光临(领地)"; - if (fromArea != null) { - p.sendMessage(formatAreaMsg(leaveMsg2, p.getName(), fromName)); - } - // ★ 离开领地时展示边框粒子(3秒) - if (fromArea != null) { - showBorderBrief(p, fromArea); - } - // ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还 - handleConfiscateReturn(p, fromName); - } - - // ===== 进入新区域 ===== - if (toName != null) { - /* plugin.getLogger().info( - "[防护-传送-进入] ★★★开始进入处理: " - + toName);*/ - - // 取消旧延时 - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - String oldP = pendingClearArea.remove(uid); - plugin.getLogger().info( - "[防护-传送-进入] 取消旧延时, 旧区域=" - + oldP); - if (oldP != null) { - diffEffects(p, uid, oldP, toName); - } - } - - // 进入提示(默认消息:欢迎(玩家)来到(领地)) - String enterMsg2 = (toArea != null && toArea.enterMsg != null && !toArea.enterMsg.isEmpty()) - ? toArea.enterMsg : "欢迎(玩家)来到(领地)"; - p.sendMessage(formatAreaMsg(enterMsg2, p.getName(), toName)); - - // ★ 进入领地时展示边框粒子(3秒) - if (toArea != null) { - showBorderBrief(p, toArea); - } - - // 发放效果 - if (toArea != null) { - /* plugin.getLogger().info( - "[防护-传送-进入] 调用applyRegionEffects");*/ - applyRegionEffects(p, uid, toName, toArea); -// 清除效果 - clearBadEffects(p, toArea); - - // ★ 没收违禁品(使用统一的handleConfiscate) - if (!isExemptFromConfiscation(p, toName)) { - handleConfiscate(p, toArea); - } - } - - // 强制模式 - if (toArea != null && toArea.enforceGameMode != null - && !toArea.enforceGameMode.isEmpty() - && !isExemptFromGameMode(p, toName)) { - GameMode target = GameMode.valueOf( - toArea.enforceGameMode); - if (p.getGameMode() != target) { - p.setGameMode(target); - } - } - } - - // 更新记录 - if (toName != null) { - playerCurrentArea.put(uid, toName); - } else { - playerCurrentArea.remove(uid); - } - } - - - public Map getPendingClearTasks() { - return pendingClearTask; - } - - public Map> - getPlayerMarkedEffects() { - return playerMarkedEffects; - } - - public void handleInside(Player p, AreaConfig ac) { - clearBadEffects(p, ac); - - if (ac.peaceMode) { - banHostilesWithWhitelist(p, ac); - } else if (ac.denyRaid) { - banRaidMobs(p, ac); - } - - // 所有者或管理员 → 跳过所有限制性规则(包括没收) - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - - handleConfiscate(p, ac); - } - - public void cleanupExpiredProtections() { - protectedEntities.entrySet().removeIf( - entry -> System.currentTimeMillis() > entry.getValue()); - } - - // 禁止袭击:只驱逐袭击怪物 - private void banRaidMobs(Player p, AreaConfig ac) { - for (Entity ent : p.getNearbyEntities(32, 32, 32)) { - String t = ent.getType().name(); - if (t.equals("PILLAGER") - || t.equals("VINDICATOR") - || t.equals("EVOKER") - || t.equals("RAVAGER") - || t.equals("VEX") - || t.equals("WITCH") - || t.equals("ILLUSIONER") - || t.equals("ZOMBIE_VILLAGER")) { - Location voidLoc = ent.getLocation().clone(); - voidLoc.setY(voidLoc.getWorld().getMinHeight() - 50); - ent.teleport(voidLoc); - } - } - } - - public Set getAreaNames() { - return areas.keySet(); - } - - // ==================== 领地边框粒子展示 ==================== - - /** - * 向玩家展示领地边框粒子特效(只展示给指定玩家) - * @param ac 领地配置 - * @param durationTicks 持续tick数(20tick=1秒) - */ - public void showBorderParticles(Player p, AreaConfig ac, int durationTicks) { - if (ac == null) return; - World world = Bukkit.getWorld(ac.world); - if (world == null) return; - - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - - // === 性能优化:动态密度控制 === - int edgeLenX = maxX - minX + 1; - int edgeLenZ = maxZ - minZ + 1; - int perimeter = 2 * (edgeLenX + edgeLenZ); - - // 超大领地直接跳过粒子(>200格边长) - if (edgeLenX > 200 || edgeLenZ > 200) { - return; - } - - // 动态步长:保证每条边最多~30个粒子点 - int step = Math.max(2, Math.min(6, perimeter / 120)); - // 每轮粒子总量上限60(不含四角) - int maxParticlesPerRound = 60; - - Particle.DustTransition dust = new Particle.DustTransition(org.bukkit.Color.fromRGB(0, 255, 255), org.bukkit.Color.fromRGB(255, 255, 0), 1.2f); - - // 每20tick刷新一次粒子(共durationTicks/20次) - int rounds = Math.max(1, durationTicks / 20); - for (int i = 0; i < rounds; i++) { - final int round = i; - final int fStep = step; - new org.bukkit.scheduler.BukkitRunnable() { - @Override - public void run() { - if (!p.isOnline()) { cancel(); return; } - - // 在玩家Y层展示四条边 - int playerY = p.getLocation().getBlockY(); - int y = Math.max(ac.yMin, Math.min(playerY, ac.yMax)); - - int count = 0; - // 四条边 - for (int x = minX; x <= maxX && count < maxParticlesPerRound; x += fStep) { - p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, x + 0.5, y, minZ + 0.5), 1, dust); - p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, x + 0.5, y, maxZ + 0.5), 1, dust); - count += 2; - } - for (int z = minZ; z <= maxZ && count < maxParticlesPerRound; z += fStep) { - p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, minX + 0.5, y, z + 0.5), 1, dust); - p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, maxX + 0.5, y, z + 0.5), 1, dust); - count += 2; - } - // 四个角高亮(各1个粒子,减少ProtocolLib包量) - p.spawnParticle(Particle.END_ROD, new Location(world, minX + 0.5, y, minZ + 0.5), 1); - p.spawnParticle(Particle.END_ROD, new Location(world, maxX + 0.5, y, minZ + 0.5), 1); - p.spawnParticle(Particle.END_ROD, new Location(world, minX + 0.5, y, maxZ + 0.5), 1); - p.spawnParticle(Particle.END_ROD, new Location(world, maxX + 0.5, y, maxZ + 0.5), 1); - } - }.runTaskLater(plugin, round * 20L); - } - } - - /** - * 短时间展示领地边框(进入/离开时,3秒) - */ - public void showBorderBrief(Player p, AreaConfig ac) { - showBorderParticles(p, ac, 60); // 3秒 - } - - /** 获取所有用户组名(tab补全用) */ - /** - * ★ 领地增益效果数量上限(2026-10-04 任务3)。 - * 普通玩家 5 个;用户组配置 max_effects 可覆盖(<=0=不限)。 - * @return >0=上限数量;Integer.MAX_VALUE=不限 - */ - public int getMaxGiveEffects(String ownerName) { - try { - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm != null) { - return ugm.getPlayerMaxEffects(ownerName, 5); - } - } catch (Exception ignored) {} - return 5; - } - - /** - * ★ 领地增益【药效等级上限】(2026-10-04 任务4)。 - * 普通玩家(无用户组)严格按原版最大药效强度 256 封顶; - * 用户组玩家按组配置 max_effect_level,组里配多少就是多少。 - * - * @param ownerName 领地所有者(上限跟随领地所有者所属用户组,与数量上限一致口径) - * @return 药效强度上限,恒在 1~256 之间 - */ - public int getMaxGiveEffectLevel(String ownerName) { - try { - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm != null) { - return ugm.getPlayerMaxEffectLevel(ownerName, - UserGroupManager.DEFAULT_MAX_EFFECT_LEVEL); - } - } catch (Exception ignored) {} - return UserGroupManager.DEFAULT_MAX_EFFECT_LEVEL; - } - - /** - * 把领地配置里的药效等级夹到该领地的合法上限内。 - * 所有写入/应用入口都必须过这一刀。 - * - * @param rawLevel 配置里写的原始药效强度 - * @param ownerName 领地所有者 - * @return 夹紧后的药效强度 - */ - public int clampGiveEffectLevel(int rawLevel, String ownerName) { - int cap = getMaxGiveEffectLevel(ownerName); - if (rawLevel < 1) return 1; - return rawLevel > cap ? cap : rawLevel; - } - - /** - * ★ 生存模式可获取的最大等级(2026-10-04 任务4 用户纠正)。 - * - *

口径:不是 /effect 命令的作弊上限(amplifier 255 = 强度 256), - * 而是生存模式下正常探索、合成、建造能拿到的最大等级。 - * 例:抗性提升靠信标最高 2 级;海豚的恩惠只有潮涌能量块一个来源,最高 1 级。

- * - *

取值来源:Minecraft 中文维基「状态效果」「信标」逐项核对, - * 效果清单与 paper-api 的 39 个 PotionEffectType 完全对齐。

- * - *

默认返回 1:未知效果一律按最低档处理,宁可少给也不超发。

- */ - private static int getSurvivalMaxLevel(String effectName) { - if (effectName == null) return 1; - String clean = effectName.trim() - .replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); - if (clean.endsWith("效果")) { - clean = clean.substring(0, clean.length() - 2); - } - if (clean.isEmpty()) return 1; - switch (clean) { - // ===== 信标类主效果:4级金字塔可选「提升至II」→ 2;无药水的固定 1 ===== - case "迅捷": case "速度": case "speed": case "SPEED": - return 2; // 迅捷药水(增强版) II;信标 I+II - case "急迫": case "haste": case "HASTE": - return 2; // 无药水,仅信标;4级金字塔可升 II - case "抗性提升": case "抗性": case "resistance": case "RESISTANCE": - return 2; // 抗性药水 II;信标 I+II - case "跳跃提升": case "跳跃": case "jump_boost": case "JUMP_BOOST": case "JUMP": - return 2; // 跳跃药水(增强版) II;信标 I+II - case "力量": case "strength": case "STRENGTH": case "INCREASE_DAMAGE": - return 2; // 力量药水 II;信标 I+II - // ===== 有药水且可升 II 的效果 ===== - case "瞬间治疗": case "治疗": case "instant_health": case "INSTANT_HEALTH": case "HEAL": - case "瞬间伤害": case "伤害": case "instant_damage": case "INSTANT_DAMAGE": case "HARM": - case "缓慢": case "slowness": case "SLOWNESS": - case "中毒": case "poison": case "POISON": - case "生命恢复": case "再生": case "回复": case "regeneration": case "REGENERATION": - return 2; // 各药水均可用发光石粉升至 II - case "挖掘疲劳": case "mining_fatigue": case "MINING_FATIGUE": - return 3; // 无药水;远古守卫者固定给 III(Wiki:natural effect level is III) - // ===== 生存只有单一来源、等级恒为 1 ===== - case "海豚的恩惠": case "海豚恩惠": case "dolphins_grace": case "DOLPHINS_GRACE": - return 1; // 仅潮涌能量块 / 海豚馈赠,无等级区分 - case "潮涌能量": case "conduit_power": case "CONDUIT_POWER": - return 1; - case "抗火": case "fire_resistance": case "FIRE_RESISTANCE": - return 1; // 抗火药水无升级配方 - case "水下呼吸": case "water_breathing": case "WATER_BREATHING": - return 1; // 水肺药水无升级配方 - case "隐身": case "invisibility": case "INVISIBILITY": - return 1; // 隐身药水无升级配方 - case "夜视": case "night_vision": case "NIGHT_VISION": - return 1; // 夜视药水无升级配方 - case "缓降": case "slow_falling": case "SLOW_FALLING": - return 1; // 缓降药水无升级配方 - case "虚弱": case "weakness": case "WEAKNESS": - return 1; // 虚弱药水无升级配方 - case "反胃": case "恶心": case "nausea": case "NAUSEA": - return 1; // 河豚等食物,固定 I - case "饥饿": case "hunger": case "HUNGER": - return 1; // 生肉/腐肉,固定 I - case "凋零": case "wither": case "WITHER": - return 1; // 凋灵之首施加,固定 I - case "失明": case "blindness": case "BLINDNESS": - return 1; - case "黑暗": case "darkness": case "DARKNESS": - return 1; // 监守者等施加,固定 I - case "发光": case "glowing": case "GLOWING": - return 1; // 光灵箭,固定 I - case "飘浮": case "悬浮": case "levitation": case "LEVITATION": - return 1; // 潜影弹,固定 I - case "不祥之兆": case "bad_omen": case "BAD_OMEN": - return 5; // 不祥之兆瓶 I–V(1.16 起上限 5),进村转袭击之兆时等级保留 - case "试炼之兆": case "trial_omen": case "TRIAL_OMEN": - case "袭击之兆": case "raid_omen": case "RAID_OMEN": - return 5; // 均由不祥之兆转化而来,等级沿用,故同为 5 - case "村庄英雄": case "hero_of_the_village": case "HERO_OF_THE_VILLAGE": - return 5; // JE 袭击全歼后按袭击等级给 I–V(Wiki:potency I–V) - case "蓄风": case "wind_charged": case "WIND_CHARGED": - case "盘丝": case "weaving": case "WEAVING": - case "渗浆": case "oozing": case "OOZING": - case "寄生": case "infested": case "INFESTED": - return 1; // 均为事件触发,无等级区分 - case "鹦鹉螺之息": case "breath_of_the_nautilus": case "BREATH_OF_THE_NAUTILUS": - return 1; - // ===== 生存无来源,仅命令/插件可得 ===== - case "生命提升": case "health_boost": case "HEALTH_BOOST": - return 1; // Wiki:Health Boost 仅能靠 /effect 获得 - case "伤害吸收": case "absorption": case "ABSORPTION": - return 4; // 附魔金苹果给 IV(金苹果/图腾只给 I/II),Wiki 1.9 起 IV - case "饱和": case "saturation": case "SATURATION": - case "幸运": case "luck": case "LUCK": - case "霉运": case "unluck": case "UNLUCK": - return 1; - default: - return 1; // 未知效果:宁可少给 - } - } - - /** - * ★ 单个效果的最终封顶:min(生存可获取上限, 该领地用户组配置的全局上限)。 - * - *

两层含义同时生效,缺一不可: - *

    - *
  1. 生存上限(硬顶):该效果在正常玩法里根本拿不到更高等级, - * 例如海豚的恩惠最多 1 级,配 255 也只给 1 级。
  2. - *
  3. 用户组上限(软顶):组里配多少就是多少, - * 但同样不得突破上面那条生存上限。
  4. - *

- * - * @param effectName 效果名(支持中文名与英文 ID) - * @param ownerName 领地所有者(决定用户组上限) - * @return 药效强度上限,恒 >=1 - */ - public int getEffectLevelCap(String effectName, String ownerName) { - int survival = getSurvivalMaxLevel(effectName); - int group = getMaxGiveEffectLevel(ownerName); - if (group < survival) return group < 1 ? 1 : group; - return survival; - } - - /** - * 按效果名封顶(各应用点统一走这里)。 - */ - public int clampEffectLevel(int rawLevel, String effectName, String ownerName) { - int cap = getEffectLevelCap(effectName, ownerName); - if (rawLevel < 1) return 1; - return rawLevel > cap ? cap : rawLevel; - } - - public List getUserGroupNames() { - List names = new ArrayList<>(); - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm != null) { - for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) { - names.add(cfg.name); - } - } - return names; - } - - /** - * 根据名称获取领地配置 - */ - public AreaConfig getLand(String name) { - return areas.get(name); - } - - /** - * 获取所有领地(只读) - */ - public Map getAllLands() { - return Collections.unmodifiableMap(areas); - } - - /** - * 检查领地是否有待删除请求 - */ - public boolean hasPendingDelete(String areaName) { - return pendingDeletes.containsKey(areaName); - } - - /** - * 获取指定玩家拥有的所有领地 - */ - public List getLandsByOwner(String ownerName) { - List result = new ArrayList<>(); - for (AreaConfig ac : areas.values()) { - if (samePlayer(ac.owner, ownerName)) { - result.add(ac); - } - } - return result; - } - - /** - * 删除领地 - */ - public void deleteLand(String name) { - areas.remove(name); - // 删除数据库记录 - try { - if (dbConnection != null && !dbConnection.isClosed()) { - PreparedStatement ps = dbConnection.prepareStatement( - "DELETE FROM area_lands WHERE name = ?"); - ps.setString(1, name); - ps.executeUpdate(); - ps.close(); - } - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 删除领地数据库失败: " + e.getMessage()); - } - } - - /** - * 获取领地成员列表(白名单) - */ - public Set getLandMembers(String landName) { - Set members = areaPlayerWhitelist.get(landName); - return members != null ? new HashSet<>(members) : new HashSet<>(); - } - - /** - * 添加领地成员(领地主自动跳过,不允许作为成员入库) - */ - public boolean addLandMember(String landName, String playerName) { - AreaConfig ac = getLand(landName); - if (ac != null && ac.owner != null && samePlayer(ac.owner, playerName)) { - return false; // 领地主不能作为成员添加 - } - Set members = areaPlayerWhitelist.computeIfAbsent( - landName, k -> ConcurrentHashMap.newKeySet()); - members.add(normName(playerName)); - saveWhitelists(); - return true; - } - - /** - * 移除领地成员 - */ - public void removeLandMember(String landName, String playerName) { - Set members = areaPlayerWhitelist.get(landName); - if (members != null) { - // ★ 精确 / 旧版默认Locale小写 / normName 三种格式全部尝试移除 - members.remove(playerName); - members.remove(playerName.toLowerCase()); - members.remove(normName(playerName)); - saveWhitelists(); - } - // ★ 同步删除 area_land_permissions 记录(确保 admin 身份一并清除) - removePlayerFromAreaWhitelist(landName, playerName); - } - - /** - * 获取玩家当前所在区域名 - */ - public String getPlayerArea(Player p) { - return playerCurrentArea.get(p.getUniqueId()); - } - - public Collection getPendingTasks() { - return pendingClearTask.values(); - } - - - // 判断是否为远程攻击敌对生物 - private boolean isRangedHostile(String typeName) { - switch (typeName) { - // ===== 骷髅系 ===== - case "SKELETON": // 骷髅(弓箭) - case "STRAY": // 流浪者(迟缓之箭) - case "BOGGED": // 虫蚀骷髅(毒箭) - case "WITHER_SKELETON": // 凋灵骷髅(近战但算灾厄) - - // ===== 灾厄村民 ===== - case "PILLAGER": // 掠夺者(弩) - case "ILLUSIONER": // 幻术师(弓箭+失明) - case "EVOKER": // 幻魔者(唤魔) - case "VINDICATOR": // 卫道士(斧头近战) - case "VEX": // 恼鬼(剑近战,但会穿墙飞行) - - // ===== 法术/投射 ===== - case "BLAZE": // 烈焰人(火球) - case "WITCH": // 女巫(药水) - - // ===== 飞行/爆炸 ===== - case "GHAST": // 恶魂(火球) - case "PHANTOM": // 幻翼(俯冲) - case "BIG_GHAST": // 大恶魂(1.21新增) - - // ===== 水下 ===== - case "GUARDIAN": // 守卫者(激光) - case "ELDER_GUARDIAN": // 远古守卫者(激光) - - // ===== 远程特殊 ===== - case "SHULKER": // 潜影贝(潜影弹) - case "BREEZE": // 旋风人(风弹) - - // ===== 蜘蛛系(远程毒液攻击) ===== - case "SPIDER": // 蜘蛛(白天远程吐丝) - case "CAVE_SPIDER": // 洞穴蜘蛛(毒) - - // ===== 1.21新增 ===== - case "WIND_CHARGED": // 蓄风怪(风弹) - case "WEAVING": // 盘丝怪(吐丝) - case "OOZING": // 渗浆怪(史莱姆) - case "INFESTED": // 寄生怪(蠹虫) - - // ===== 盔甲/变种(部分版本) ===== - case "ARMORED_SKELETON": // 盔甲骷髅 - case "ARCHER_SKELETON": // 弓箭骷髅(部分版本) - case "SNOW_GOLEM": // 雪傀儡(不是敌对但远程) - - // ===== 亡灵骑乘 ===== - case "HUSK": // 尸壳(近战但属于灾厄系) - case "DROWNED": // 溺尸(三叉戟远程) - case "ZOMBIE_VILLAGER": // 僵尸村民 - case "ZOMBIE_PIGMAN": // 僵尸猪灵 - case "ZOMBIFIED_PIGLIN": // 僵尸猪灵(1.16+) - case "PIGLIN_BRUTE": // 猪灵蛮兵 - - // ===== 猪灵(弓箭) ===== - case "PIGLIN": // 猪灵(金弩远程) - - // ===== 末影系 ===== - case "ENDER_DRAGON": // 末影龙(远程吐息) - case "WARDEN": // 监守者(远程音波) - - // ===== 其他 ===== - case "CREPER": // 苦力怕(爆炸不算远程但危险) - case "CREEPER": // 苦力怕 - - return true; - default: - return false; - } - } - - - - private void applyEffects(Player p, AreaConfig ac) { - // ★ 2026-10-04:负面效果从 giveEffects 拆到 badEffects,两边都要给 - List all = new ArrayList<>(ac.giveEffects); - if (ac.badEffects != null) all.addAll(ac.badEffects); - for (String[] parts : all) { - String effName = parts[0]; - int level = 1; - int duration = 999; - try { level = Integer.parseInt(parts[1]); } - catch (NumberFormatException ignored) {} - try { duration = Integer.parseInt(parts[2]); } - catch (NumberFormatException ignored) {} - // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 - level = clampEffectLevel(level, effName, ac.owner); - - PotionEffectType type = resolveEffectType(effName); - plugin.getLogger().info("[防护调试] 给予: " - + effName + " -> " + type); - if (type != null) { - p.addPotionEffect(new PotionEffect( - type, duration * 20, level - 1)); - plugin.getLogger().info("[防护调试] 已给予: " - + effName + " 等级" + level - + " 持续" + duration + "秒"); - } - } - } - - - @EventHandler - public void onBlockPlace(BlockPlaceEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getBlock().getX(), - e.getBlock().getY(), - e.getBlock().getZ()); - if (ac == null) return; - - Material placedMat = e.getBlock().getType(); - - // ★ 农作物种植权限分层:种子/农作物由denyCropHarvest独立控制 - if (isCropSeed(placedMat)) { - if (getEffectiveDeny(p, ac, "denyCropHarvest")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止种植农作物"); - } - return; // 农作物种子不走denyBlockPlace - } - - // 非农作物:正常denyBlockPlace检查 - if (getEffectiveDeny(p, ac, "denyBlockPlace")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置方块"); - } - // ★ 领地记录:记录成功放置的方块(事件未被取消才记录) - if (!e.isCancelled()) { - plugin.landRecordManager.recordBlockPlace(ac, p, e.getBlock()); - } - } - - @EventHandler - public void onEntityPlace(org.bukkit.event.entity.EntityPlaceEvent e) { - if (!(e.getPlayer() instanceof Player)) return; - Player p = e.getPlayer(); - Location loc = e.getEntity().getLocation(); - AreaConfig ac = getArea( - p.getWorld().getName(), - loc.getBlockX(), - loc.getBlockY(), - loc.getBlockZ()); - if (ac == null) return; - - // ★ 生物蛋权限:上级权限=放置方块(放置未开启时,单独开启生物蛋不生效) - if (isSpawnEggItem(p.getInventory().getItemInMainHand().getType()) - && getEffectiveDeny(p, ac, "denyBlockPlace") - && getEffectiveDeny(p, ac, "denySpawnEgg")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋"); - return; - } - - // 层级:denyBlockPlace (一级) → denyEntityInteract (二级) → denyItemFrame (三级) - if (getEffectiveDeny(p, ac, "denyBlockPlace")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置方块或实体"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置实体"); - return; - } - // 三级:展示框特定 - String typeName = e.getEntity().getType().name(); - if ((typeName.equals("ITEM_FRAME") || typeName.equals("GLOW_ITEM_FRAME")) - && getEffectiveDeny(p, ac, "denyItemFrame")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置展示框"); - } - } - - @EventHandler - public void onBlockBreak(BlockBreakEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getBlock().getX(), - e.getBlock().getY(), - e.getBlock().getZ()); - if (ac == null) return; - - Material blockMat = e.getBlock().getType(); - - // ★ 农作物权限分层:农作物由denyCropHarvest独立控制,不受denyBlockBreak影响 - if (isCropBlock(blockMat)) { - if (getEffectiveDeny(p, ac, "denyCropHarvest")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止收获农作物"); - } - return; // 农作物不走denyBlockBreak - } - - // 非农作物:正常denyBlockBreak检查 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏方块"); - } - // ★ 领地记录:记录成功破坏的方块(事件未被取消才记录) - if (!e.isCancelled()) { - plugin.landRecordManager.recordBlockBreak(ac, p, e.getBlock()); - } - } - - @EventHandler - public void onSignChange(SignChangeEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getBlock().getX(), - e.getBlock().getY(), - e.getBlock().getZ()); - if (ac == null) return; - - // ★ 记录告示牌放置/编辑(权限检查前记录,被取消也要记录尝试) - // 检测是编辑还是放置:如果该位置已有告示牌方块数据 - boolean isEdit = e.getBlock().getState() instanceof org.bukkit.block.Sign; - String[] lines = e.getLines(); - // 延迟记录(等事件处理完后,内容已写入) - final boolean editFinal = isEdit; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - plugin.landRecordManager.recordSignEdit(ac, p, e.getBlock(), - lines, editFinal); - }, 1L); - - if (p.hasPermission("sdf1.admin")) return; // 管理员跳过 - if (samePlayer(p.getName(), ac.owner)) return; // 领地主免检 - if (isAreaAdmin(p)) return; // 插件管理员免检 - // ★ 授权成员(白名单内玩家)也豁免denySignEdit - if (isPlayerWhitelisted(p.getName(), ac)) return; - - if (getEffectiveDeny(p, ac, "denySignEdit")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止编辑告示牌"); - } - } - - - @EventHandler - public void onDamage(EntityDamageEvent e) { - if (!(e.getEntity() instanceof Player)) return; - Player p = (Player) e.getEntity(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) return; - // 保护性规则:支持per-player独立权限 - if (getEffectiveDeny(p, ac, "denyAllDamage")) { e.setCancelled(true); return; } - if (getEffectiveDeny(p, ac, "denyFallDamage") - && e.getCause() == EntityDamageEvent.DamageCause.FALL) - e.setCancelled(true); - } - - - @EventHandler - public void onFood(FoodLevelChangeEvent e) { - if (!(e.getEntity() instanceof Player)) return; - Player p = (Player) e.getEntity(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac != null && getEffectiveDeny(p, ac, "denyHunger")) - e.setCancelled(true); - } - - - - @EventHandler - public void onDrop(PlayerDropItemEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) return; - // 使用getEffectiveDeny,OWNER/Admin自动豁免,visitor走领地默认 - if (getEffectiveDeny(p, ac, "denyDrop")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); - } - } - - // ★ 拾取限制:allowPickup=false时禁止拾取(denyPickup映射到allowPickup) - @EventHandler - public void onPlayerPickup(PlayerPickupItemEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) return; - // 使用getEffectiveDeny,OWNER/Admin自动豁免,visitor走领地默认 - if (getEffectiveDeny(p, ac, "denyPickup")) { - e.setCancelled(true); - // 节流:5~10秒随机间隔 - long now = System.currentTimeMillis(); - Long last = lastPickupMsgTime.get(p.getUniqueId()); - long cooldown = 5000 + new java.util.Random().nextLong(5000); // 5~10秒 - if (last == null || now - last > cooldown) { - lastPickupMsgTime.put(p.getUniqueId(), now); - p.sendMessage(PICKUP_MSGS[new java.util.Random().nextInt(PICKUP_MSGS.length)]); - } - } - } - - // ===== 涂蜡/刮蜡权限(denyWax)—— 独立处理器 ===== - // 为什么单独开一个方法: - // ① onInteract 开头有选区工具等分支会提前 return,合并写会被短路跳过; - // ② 用 e.getItem()/e.getHand() 取"实际使用的那只手",副手蜜脾/副手斧同样拦截; - // ③ 同时 DENY useInteractedBlock + useItemInHand,确保原版"刮蜡/涂蜡"流程(1.20.5+ 在 - // ServerPlayerGameMode.useItemOn 中先查 useItemInHand 再继续 item.useOn)被彻底切断; - // ④ HIGHEST 优先级,避免被其它监听器的默认处理覆盖。 - @EventHandler(priority = EventPriority.HIGHEST) - public void onWaxInteract(PlayerInteractEvent e) { - Action act = e.getAction(); - if (act != Action.RIGHT_CLICK_BLOCK && act != Action.RIGHT_CLICK_AIR) return; - // 副手事件会与主手重复触发一次,只处理主手 - if (e.getHand() == org.bukkit.inventory.EquipmentSlot.OFF_HAND) return; - - Player p = e.getPlayer(); - ItemStack used = e.getItem(); - if (used == null || used.getType().isAir()) { - used = p.getInventory().getItemInMainHand(); - } - Material mat = used.getType(); - boolean waxOn = mat == Material.HONEYCOMB; - boolean waxOff = mat.name().endsWith("_AXE"); // 木/石/铜/铁/金/钻/下界合金斧全部匹配 - if (!waxOn && !waxOff) return; - - Block waxBlock = e.getClickedBlock(); - if (waxBlock == null) { - try { waxBlock = p.getTargetBlockExact(6); } catch (Exception ignored) {} - } - if (waxBlock == null || !isWaxableBlock(waxBlock)) return; - - AreaConfig waxAc = getArea(waxBlock.getWorld().getName(), - waxBlock.getX(), waxBlock.getY(), waxBlock.getZ()); - if (waxAc == null) return; // 不在任何领地内 → 不干预 - - if (isWaxDenied(p, waxAc)) { - e.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY); - e.setUseItemInHand(org.bukkit.event.Event.Result.DENY); - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止" + (waxOn ? "涂蜡" : "刮蜡")); - return; - } - - // ★ 诊断日志:领地已开启 denyWax 却放行时,把放行原因打进控制台, - // 方便定位"为什么没拦住"(所有者/管理员/白名单访客豁免是最常见原因)。 - if (waxAc.denyWax) { - plugin.getLogger().info("[防护] 放行" + (waxOn ? "涂蜡" : "刮蜡") - + ": 玩家=" + p.getName() + " 领地=" + waxAc.name - + " 原因=" + waxAllowReason(p, waxAc)); - } - } - - /** 诊断用:denyWax 已开却放行的原因(2026-09-29 对齐后放行路径与普通 deny* 一致) */ - private String waxAllowReason(Player p, AreaConfig ac) { - try { - PermissionLevel lvl = getPermissionLevel(p, ac); - if (lvl == PermissionLevel.ADMIN) return "插件/领地管理员豁免"; - if (lvl == PermissionLevel.OWNER) return "领地主豁免"; - if (lvl == PermissionLevel.MEMBER) return "已授权成员豁免"; - int landId = getLandIdFromDb(ac.name); - if (landId > 0) { - Map perms = getPlayerPermMap(landId, p.getName()); - if (perms != null && perms.containsKey("denyWax") - && !Boolean.TRUE.equals(perms.get("denyWax"))) { - return "该玩家被单独放行(denyWax=false)"; - } - } - if (lvl == PermissionLevel.VISITOR) return "白名单访客豁免"; - } catch (Exception ignored) {} - return "未知(请反馈该日志)"; - } - - // 替换整个 onInteract 方法 - @EventHandler - public void onInteract(PlayerInteractEvent e) { - Player p = e.getPlayer(); - ItemStack hand = p.getInventory().getItemInMainHand(); - if (hand == null) return; - - // ===== 选区工具 ===== - if (hand.getType() == Material.BLAZE_ROD) { - ItemMeta meta = hand.getItemMeta(); - if (meta != null && meta.getDisplayName() != null - && meta.getDisplayName().contains("区域选择")) { - Action action = e.getAction(); - boolean isLeft = (action == Action.LEFT_CLICK_AIR - || action == Action.LEFT_CLICK_BLOCK); - boolean isRight = (action == Action.RIGHT_CLICK_AIR - || action == Action.RIGHT_CLICK_BLOCK); - if (isLeft || isRight) { - e.setCancelled(true); - // ★ SN 归属 + 状态双校验(2026-10-03 两次收紧): - // 只认「登记在本人名下 + 状态 active」的圈地工具; - // 注销 / 报失 / 非法绑定 / 补发了结 / 已销毁 一律不支持选区 - // (isAreaAdmin 豁免);无 SN、未在册同样拦截。 - if (!isAreaAdmin(p)) { - SnManager snm = plugin.getSnManager(); - String wandSn = (snm != null) ? snm.readSn(hand) : null; - Map wandRow = (wandSn != null) ? snm.getSn(wandSn) : null; - Object wandOwnerObj = (wandRow != null) ? wandRow.get("owner") : null; - String wandOwner = (wandOwnerObj == null) ? null : String.valueOf(wandOwnerObj); - boolean ownerOk = wandSn != null && wandRow != null - && wandOwner != null && !wandOwner.isEmpty() - && samePlayer(wandOwner, p.getName()); - if (!ownerOk) { - String why = (wandSn == null - ? "该工具没有绑定 SN,请用 §e/protect 工具 §f重新申领" - : (wandOwner == null || wandOwner.isEmpty() - ? "该工具的 SN 未在登记簿中,请用 §e/protect 工具 §f重新申领" - : "这是 §e" + wandOwner + " §f的圈地工具")); - p.sendMessage("§c§l[防护] §f请使用你自己的圈地工具选区——" + why); - return; - } - Object wandStatusObj = wandRow.get("status"); - String wandStatus = (wandStatusObj == null) ? null : String.valueOf(wandStatusObj); - if (!SnManager.ST_ACTIVE.equals(wandStatus)) { - p.sendMessage("§c§l[防护] §f该圈地工具的登记状态为 §e" - + snm.statusCn(wandStatus) - + "§f,不支持选区——请用 §e/protect 工具 §f重新申领"); - return; - } - } - Block block = e.getClickedBlock(); - if (block == null) { - try { block = p.getTargetBlockExact(5); } - catch (Exception ignored) {} - } - if (block == null) return; - UUID uid = p.getUniqueId(); - if (isLeft) { - pos1.put(uid, block.getLocation()); - p.sendMessage("§a§l[防护] §f位置1: " - + block.getX() + ", " - + block.getY() + ", " - + block.getZ()); - } else { - Location newLoc = block.getLocation(); - Location oldLoc = pos2.get(uid); - boolean changed = oldLoc == null - || oldLoc.getBlockX() != newLoc.getBlockX() - || oldLoc.getBlockY() != newLoc.getBlockY() - || oldLoc.getBlockZ() != newLoc.getBlockZ(); - pos2.put(uid, newLoc); - if (changed) { - p.sendMessage("§a§l[防护] §f位置2: " - + block.getX() + ", " - + block.getY() + ", " - + block.getZ()); - // ★ 两个位置都选好后,显示面积+价格预览 + 一键创建超链接 - if (pos1.containsKey(uid)) { - Location lp1 = pos1.get(uid); - int w = Math.abs(newLoc.getBlockX() - lp1.getBlockX()) + 1; - int l = Math.abs(newLoc.getBlockZ() - lp1.getBlockZ()) + 1; - int a = w * l; - UserGroupManager ug = plugin.getUserGroup(); - int pricePerSqm = (ug != null) ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; - int totalCost = a * pricePerSqm; - String src = (pricePerSqm != globalCreatePricePerSqm) ? " §7(用户组优惠价)" : ""; - BondManager bnd = plugin.getBonds(); - int bal = (bnd != null) ? bnd.getBonds(p.getName()) : 0; - p.sendMessage("§e§l[防护] §f选区完成!面积: §a" + a + "㎡§7(" + w + "×" + l + ") 单价: §f" + pricePerSqm + "/㎡" + src + " 预估: §e" + totalCost + "§7债券 余额: §a" + bal); - // ★ 一键创建超链接:自动用玩家名,冲突则加数字(重名不区分大小写) - String autoName = p.getName(); - if (areaNameExists(autoName)) { - for (int i = 0; i <= 99; i++) { - String candidate = autoName + i; - if (!areaNameExists(candidate)) { autoName = candidate; break; } - } - } - String finalAutoName = autoName; - p.sendMessage(Component.empty() - .append(Component.text("§a§l[✅ 点击一键创建]")) - .hoverEvent(HoverEvent.showText(Component.text("§e以 §f" + finalAutoName + " §e为名创建领地\n§7费用: " + totalCost + " 债券"))) - .clickEvent(ClickEvent.runCommand("/protect confirm_create " + finalAutoName)) - ); - p.sendMessage(Component.text("§7或输入 §f/protect 创建 <自定义名> §7指定领地名")); - } - } - } - } - return; - } - } - - // ===== 涂蜡/刮蜡权限(denyWax)===== - // ★ 已拆成独立处理器 onWaxInteract(见本类 onInteract 方法之前): - // ① 本方法开头有多处提前 return(选区工具分支等),合并写会被短路跳过; - // ② 独立处理器改用 e.getItem()/e.getHand() 取"实际使用的手",副手蜜脾/斧同样拦截。 - - // ===== 生物蛋权限(denySpawnEgg)===== - // 上级权限=放置方块:放置未开启时,单独开启本项不生效 - if (isSpawnEggItem(hand.getType()) - && (e.getAction() == Action.RIGHT_CLICK_BLOCK || e.getAction() == Action.RIGHT_CLICK_AIR)) { - Block eggBlock = e.getClickedBlock(); - Location eggLoc = (eggBlock != null) ? eggBlock.getLocation() : p.getLocation(); - AreaConfig eggAc = getArea(p.getWorld().getName(), - eggLoc.getBlockX(), eggLoc.getBlockY(), eggLoc.getBlockZ()); - if (eggAc != null - && getEffectiveDeny(p, eggAc, "denyBlockPlace") - && getEffectiveDeny(p, eggAc, "denySpawnEgg")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋"); - return; - } - } - - // ===== 实体类物品放置:层级管控 ===== - // Level 1: denyBlockPlace(一级总开关)→ 禁止一切放置 - // Level 2: denyEntityInteract(二级实体交互)→ 禁止实体交互 - // Level 3: denyItemFrame(三级展示框)→ 仅限展示框具体控制 - Material mat = hand.getType(); - boolean isControlledEntity = - mat == Material.MINECART - || mat == Material.TNT_MINECART - || mat == Material.CHEST_MINECART - || mat == Material.HOPPER_MINECART - || mat == Material.OAK_BOAT - || mat == Material.SPRUCE_BOAT - || mat == Material.BIRCH_BOAT - || mat == Material.JUNGLE_BOAT - || mat == Material.ACACIA_BOAT - || mat == Material.DARK_OAK_BOAT - || mat == Material.MANGROVE_BOAT - || mat == Material.CHERRY_BOAT - || mat == Material.BAMBOO_RAFT - || mat == Material.ARMOR_STAND - || mat == Material.ITEM_FRAME - || mat == Material.GLOW_ITEM_FRAME; - if (isControlledEntity) { - // ★ 使用被点击方块定位,而非玩家脚下位置 - Block clicked = e.getClickedBlock(); - Location checkLoc = (clicked != null) ? clicked.getLocation() : p.getLocation(); - AreaConfig ac = getArea( - p.getWorld().getName(), - checkLoc.getBlockX(), - checkLoc.getBlockY(), - checkLoc.getBlockZ()); - if (ac != null) { - // Level 1: 方块放置总开关 - if (getEffectiveDeny(p, ac, "denyBlockPlace")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置方块或实体"); - return; - } - // Level 2: 实体交互 - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置实体"); - return; - } - // Level 3: 展示框单独控制 - boolean isFrame = (mat == Material.ITEM_FRAME - || mat == Material.GLOW_ITEM_FRAME); - if (isFrame && getEffectiveDeny(p, ac, "denyItemFrame")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止放置展示框"); - return; - } - } - // 实体物品不再往下走区域规则(防止误触发末影珍珠等检查) - return; - } - - - // ===== 容器/UI方块交互:以【被点击方块】定位领地 ===== - // 玩家站在领地外右键领地内容器/UI方块同样受控(旧逻辑用玩家脚下定位, - // 站领地外 ac==null 直接放行);双箱被点侧在领地外时补查相邻同材质半边 - if (e.getAction() == Action.RIGHT_CLICK_BLOCK) { - Block clicked = e.getClickedBlock(); - if (clicked != null && isUIBlock(clicked.getType())) { - // ★ 补刀:被点击方块定位 + 四向相邻同材质半边(双箱被点侧在领地外) - // + 上下方向(潜影盒叠放/箱子上下不合并但漏斗矿车场景仍按方块算) - // 任一侧命中领地且 denyContainer 即拦截。 - java.util.List cands = new java.util.ArrayList<>(); - AreaConfig cac = getArea(clicked.getWorld().getName(), - clicked.getX(), clicked.getY(), clicked.getZ()); - if (cac != null) cands.add(cac); - if (clicked.getType() == Material.CHEST - || clicked.getType() == Material.TRAPPED_CHEST) { - org.bukkit.block.BlockFace[] around = { - org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST, - org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH}; - for (org.bukkit.block.BlockFace bf : around) { - Block nb = clicked.getRelative(bf); - if (nb.getType() == clicked.getType()) { - AreaConfig na = getArea(nb.getWorld().getName(), - nb.getX(), nb.getY(), nb.getZ()); - if (na != null && !cands.contains(na)) cands.add(na); - } - } - } - // ★ 公共设施连控:公共设施启用时访客自动获得容器交互权限 - for (AreaConfig cand : cands) { - if (cand.isPublicBuilding) continue; - if (getEffectiveDeny(p, cand, "denyContainer")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); - return; - } - } - } - } - - // ===== 区域规则检查 ===== - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) return; - - // 末影珍珠禁止(支持per-player权限) - if (getEffectiveDeny(p, ac, "denyEnderPearl") - && hand.getType() == Material.ENDER_PEARL) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); - return; - } - - // 弓箭禁止(支持per-player权限) - if (getEffectiveDeny(p, ac, "denyBow") - && (hand.getType() == Material.BOW - || hand.getType() == Material.CROSSBOW)) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); - return; - } - - // 所有者+管理员跳过禁止使用物品检查 - if (hasPermission(p, ac, PermissionLevel.OWNER)) { - // 管理员和所有者不受限制 - } else { - // 禁止使用物品(访客级别检查) - String typeId = hand.getType().name(); - if (ac.denyUseItems.contains(typeId)) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止使用此物品"); - return; - } - } - - // ★ 农作物收获:右键检测(南瓜/西瓜用剪刀收获) - if (ac.denyCropHarvest && e.getAction() == Action.RIGHT_CLICK_BLOCK) { - Block clicked = e.getClickedBlock(); - if (clicked != null) { - Material clickedMat = clicked.getType(); - if (clickedMat == Material.PUMPKIN || clickedMat == Material.MELON) { - if (getEffectiveDeny(p, ac, "denyCropHarvest")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止收获农作物"); - return; - } - } - } - } - - // ★ 容器交互扩展:检测非Container类型UI方块(工作台、砂轮、制图台、告示牌等) - if (ac.denyContainer && e.getAction() == Action.RIGHT_CLICK_BLOCK) { - Block clicked = e.getClickedBlock(); - if (clicked != null) { - Material clickedMat = clicked.getType(); - if (isUIBlock(clickedMat)) { - // ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(工作台/砂轮等UI方块同样放开) - if (ac.isPublicBuilding) return; - if (getEffectiveDeny(p, ac, "denyContainer")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); - return; - } - } - } - } - - // 没收检查 - handleConfiscate(p, ac); - } - - - @EventHandler - public void onExplosion(EntityExplodeEvent e) { - Iterator it = e.blockList().iterator(); - while (it.hasNext()) { - Block b = it.next(); - AreaConfig ac = getArea( - b.getWorld().getName(), - b.getX(), b.getY(), b.getZ()); - if (ac != null && ac.denyExplosion) - it.remove(); - } - } - - // ===== 火床/烈焰弹/三叉戟闪电也拦截 ===== - @EventHandler - public void onBlockExplode(org.bukkit.event.block.BlockExplodeEvent e) { - for (Iterator it = e.blockList().iterator(); it.hasNext();) { - Block b = it.next(); - AreaConfig ac = getArea(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()); - if (ac != null && ac.denyExplosion) it.remove(); - } - } - - @EventHandler - public void onEntityChangeBlock(org.bukkit.event.entity.EntityChangeBlockEvent e) { - if (e.getBlock().getType() == Material.FIRE || e.getBlock().getType() == Material.LAVA) { - AreaConfig ac = getArea(e.getBlock().getWorld().getName(), - e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ()); - if (ac != null && ac.denyFire) e.setCancelled(true); - } - // ★ 末影人行为控制:禁止末影人抱走方块 - if (e.getEntityType().name().equals("ENDERMAN")) { - AreaConfig ac = getArea(e.getBlock().getWorld().getName(), - e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ()); - if (ac != null && ac.denyEnderTeleport) { - e.setCancelled(true); - } - } - } - - // ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 ===== - @EventHandler - public void onIgniteBlock(BlockIgniteEvent e) { - Player p = e.getPlayer(); - Block tgtBlock = e.getBlock(); - - // 非玩家触发(如火蔓延、TNT引爆)-> 不拦截,由BlockBurnEvent处理 - if (p == null) return; - - AreaConfig srcArea = getArea(p.getWorld().getName(), - p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); - AreaConfig tgtArea = getArea(tgtBlock.getWorld().getName(), - tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); - - // 使用统一豁免 - boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); - boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); - if (srcDenied || tgtDenied) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); - } - } - - // ===== 火蔓延防漏网(非玩家触发) ===== - @EventHandler - public void onFireSpreadNonPlayer(BlockIgniteEvent e) { - if (e.getPlayer() != null) return; - Block tgtBlock = e.getBlock(); - AreaConfig ac = getArea(tgtBlock.getWorld().getName(), - tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); - if (ac != null && ac.denyFireSpread) { - e.setCancelled(true); - } - } - - @EventHandler - public void onInventoryOpen(InventoryOpenEvent e) { - if (!(e.getPlayer() instanceof Player)) return; - Player p = (Player) e.getPlayer(); - - InventoryHolder holder = e.getInventory().getHolder(); - java.util.List locs = new java.util.ArrayList<>(); - - // ★ 双箱特殊处理:DoubleChest 的 holder 不是 BlockState, - // 必须同时收集左右两侧位置——任一侧在领地内即受 denyContainer 管控。 - // 旧实现只反射取 left 且 getLeft() 返回 Inventory 无 getBlock(), - // 反射异常被吞后 loc=null 直接放行,导致大箱子完全绕过领地判定。 - if (e.getInventory() instanceof org.bukkit.inventory.DoubleChestInventory) { - org.bukkit.inventory.DoubleChestInventory dci = - (org.bukkit.inventory.DoubleChestInventory) e.getInventory(); - addChestSideLocation(locs, dci.getLeftSide()); - addChestSideLocation(locs, dci.getRightSide()); - } else if (holder instanceof org.bukkit.block.DoubleChest) { - org.bukkit.block.DoubleChest dc = (org.bukkit.block.DoubleChest) holder; - addChestSideLocation(locs, dc.getLeftSide()); - addChestSideLocation(locs, dc.getRightSide()); - } - // ★ 检测所有容器类型:箱子、熔炉、高炉、潜影盒、末影箱、讲台、铁砧、信标、漏斗、发射器、投掷器、酿造台、工作台等 - else if (holder instanceof BlockState) { - BlockState bs = (BlockState) holder; - locs.add(bs.getLocation()); - } else if (holder instanceof Container) { - // Container接口覆盖所有方块容器 - if (holder instanceof org.bukkit.block.Block) { - org.bukkit.block.Block block = (org.bukkit.block.Block) holder; - locs.add(block.getLocation()); - } - } - - // ★ Issue 9: 矿车容器(漏斗矿车、运输矿车) - if (holder != null && locs.isEmpty()) { - String className = holder.getClass().getSimpleName(); - if (className.contains("Minecart")) { - // 矿车容器使用实体位置 - if (holder instanceof org.bukkit.entity.Entity) { - org.bukkit.entity.Entity entity = (org.bukkit.entity.Entity) holder; - locs.add(entity.getLocation()); - } - } - } - - // ★ 补刀:holder 无法解析出位置(部分容器/插件自定义 holder)时, - // 回退用玩家视线命中的方块再定位一次,避免直接放行。 - if (locs.isEmpty()) { - try { - Block eye = p.getTargetBlockExact(6); - if (eye != null && isUIBlock(eye.getType())) { - locs.add(eye.getLocation()); - } - } catch (Throwable ignored) {} - } - - if (locs.isEmpty()) return; - - // ★ 跨边界大箱子:任一侧在领地内即判定;先取第一个命中领地做打开记录 - AreaConfig ac = null; - Location hitLoc = null; - for (Location loc : locs) { - if (loc == null || loc.getWorld() == null) continue; - AreaConfig a = getArea(loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (a != null) { ac = a; hitLoc = loc; break; } - } - if (ac == null || hitLoc == null) return; - - // ★ 领地记录(所有容器打开都记录,含领地主/管理员/公共设施) - String containerType; - if (hitLoc.getBlock().getType() != org.bukkit.Material.AIR) { - containerType = hitLoc.getBlock().getType().name(); - } else { - containerType = holder.getClass().getSimpleName(); - } - plugin.landRecordManager.recordContainerOpen(ac, p, hitLoc, containerType, e.getInventory()); - - // ★ 逐侧判定:任一侧所在领地禁止容器访问即拦截(跨边界大箱子不再漏网) - // ★ 补刀:某侧查不到领地时,回查四向同材质半边(被开侧在领地外、 - // 另一半在领地内)与视线命中方块,避免站领地外开领地内箱子漏网。 - for (Location loc : locs) { - if (loc == null || loc.getWorld() == null) continue; - AreaConfig a = getArea(loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (a == null) { - Block blk = loc.getBlock(); - if (blk.getType() == Material.CHEST || blk.getType() == Material.TRAPPED_CHEST) { - org.bukkit.block.BlockFace[] around = { - org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST, - org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH}; - for (org.bukkit.block.BlockFace bf : around) { - Block nb = blk.getRelative(bf); - if (nb.getType() != blk.getType()) continue; - a = getArea(nb.getWorld().getName(), - nb.getX(), nb.getY(), nb.getZ()); - if (a != null) break; - } - } - } - if (a == null) continue; - if (hasPermission(p, a, PermissionLevel.OWNER)) continue; - - // ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查) - if (a.isPublicBuilding) continue; - - // ★ 容器管理权限:denyContainer控制访客能否访问容器 - // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免, - // 对无权限玩家则走领地默认 denyContainer 配置 - if (getEffectiveDeny(p, a, "denyContainer")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); - return; - } - } - } - - // ★ 双箱侧边定位:从半边(Inventory 或 InventoryHolder)的 BlockState holder 取其方块位置 - private void addChestSideLocation(java.util.List locs, Object side) { - if (side == null) return; - Object sh = side; - if (side instanceof org.bukkit.inventory.Inventory) { - sh = ((org.bukkit.inventory.Inventory) side).getHolder(); - } - // ★ 补刀:半边 holder 仍可能是 DoubleChest(跨边界双箱的其中一侧),递归展开 - if (sh instanceof org.bukkit.block.DoubleChest) { - org.bukkit.block.DoubleChest dc2 = (org.bukkit.block.DoubleChest) sh; - addChestSideLocation(locs, dc2.getLeftSide()); - addChestSideLocation(locs, dc2.getRightSide()); - return; - } - if (sh instanceof BlockState) { - Location l = ((BlockState) sh).getLocation(); - if (l != null) locs.add(l); - } else if (sh instanceof org.bukkit.block.Block) { - locs.add(((org.bukkit.block.Block) sh).getLocation()); - } else if (sh instanceof Container) { - // ★ 补刀:非 BlockState 的 Container(如矿车容器被包装)取其位置 - Location l = ((Container) sh).getLocation(); - if (l != null) locs.add(l); - } - } - - /** - * ★ 白名单遗留小写条目 → DB 精确拼写迁移(2026-10-10 精确匹配改造配套)。 - * area_land_permissions.player_name 保存授予时的精确拼写(权威), - * 用它把旧版小写落盘的白名单条目还原为规范名;多种拼写并存(多义)时放弃迁移。 - * @param landName 领地名;null 表示查全表(全局白名单用) - * @return 规范名;无匹配/无法确定时返回 null - */ - private String canonicalNameFromDb(String landName, String entry) { - if (dbConnection == null || entry == null || entry.isEmpty()) return null; - String want = normName(entry); - String found = null; - try { - PreparedStatement stmt; - if (landName == null) { - stmt = dbConnection.prepareStatement( - "SELECT DISTINCT player_name FROM area_land_permissions"); - } else { - int landId = getLandIdFromDb(landName); - if (landId <= 0) return null; - stmt = dbConnection.prepareStatement( - "SELECT DISTINCT player_name FROM area_land_permissions WHERE land_id = ?"); - stmt.setInt(1, landId); - } - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - String n = rs.getString("player_name"); - if (n == null || n.isEmpty() || !want.equals(normName(n))) continue; - if (n.equals(entry)) { found = entry; break; } // 已是规范拼写 - if (found != null && !found.equals(n)) { found = null; break; } // 多义,放弃 - found = n; - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - return null; - } - return found; - } - - public void loadWhitelists() { - globalPlayerWhitelist.clear(); - areaPlayerWhitelist.clear(); - boolean whitelistMigrated = false; - - if (!whitelistDir.exists()) { - whitelistDir.mkdirs(); - return; - } - - try { - // 加载全局白名单 - if (globalWhitelistFile.exists()) { - List lines = - java.nio.file.Files.readAllLines( - globalWhitelistFile.toPath(), - StandardCharsets.UTF_8); - for (String line : lines) { - String name = line.trim(); - if (!name.isEmpty() - && !name.startsWith("#")) { - // ★ 遗留小写条目 → DB 精确拼写迁移(2026-10-10 精确匹配改造配套) - String fixed = canonicalNameFromDb(null, name); - if (fixed != null && !fixed.equals(name)) { - whitelistMigrated = true; - name = fixed; - } - globalPlayerWhitelist.add(name); - } - } - } - - // 加载各区域白名单 - File[] files = whitelistDir.listFiles( - (File d, String n) -> n.endsWith(".txt") - && !n.equals("全局白名单.txt")); - if (files != null) { - for (File f : files) { - String areaName = f.getName() - .replace(".txt", ""); - Set set = - ConcurrentHashMap.newKeySet(); - List lines = - java.nio.file.Files.readAllLines( - f.toPath(), - StandardCharsets.UTF_8); - for (String line : lines) { - String name = line.trim(); - if (!name.isEmpty() - && !name.startsWith("#")) { - // ★ 精确存储原始拼写(2026-10-10):离线模式大小写不同=不同玩家。 - // 旧版统一小写落盘会让真实大小写拼写的玩家无法精确命中; - // 遗留小写条目用 area_land_permissions 里的精确拼写做规范名迁移。 - String fixed = canonicalNameFromDb(areaName, name); - if (fixed != null && !fixed.equals(name)) { - whitelistMigrated = true; - name = fixed; - } - set.add(name); - } - } - if (!set.isEmpty()) { - areaPlayerWhitelist.put( - areaName, set); - } - } - } - - if (whitelistMigrated) { - saveWhitelists(); - plugin.getLogger().info("[防护] 白名单遗留小写条目已按DB精确拼写迁移回写"); - } - plugin.getLogger().info( - "[防护] 白名单加载完成,全局: " - + globalPlayerWhitelist.size() - + ",区域数: " - + areaPlayerWhitelist.size()); - } catch (Exception e) { - plugin.getLogger().warning( - "[防护] 加载白名单失败: " - + e.getMessage()); - } - } - - - @EventHandler - public void onPotionSplash(PotionSplashEvent e) { - if (!(e.getEntity().getShooter() instanceof Player)) return; - Player shooter = (Player) e.getEntity().getShooter(); - AreaConfig ac = getArea( - shooter.getWorld().getName(), - shooter.getLocation().getBlockX(), - shooter.getLocation().getBlockY(), - shooter.getLocation().getBlockZ()); - if (ac != null && getEffectiveDeny(shooter, ac, "denyPotion")) { - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止使用药水"); - } - } - - // ===== ★ 投掷物检测(三叉戟、雪球、风蛋、箭)===== - @EventHandler - public void onProjectileLaunch(ProjectileLaunchEvent e) { - if (!(e.getEntity().getShooter() instanceof Player)) return; - Player shooter = (Player) e.getEntity().getShooter(); - Entity proj = e.getEntity(); - AreaConfig ac = getArea( - shooter.getWorld().getName(), - shooter.getLocation().getBlockX(), - shooter.getLocation().getBlockY(), - shooter.getLocation().getBlockZ()); - if (ac == null) return; - if (!getEffectiveDeny(shooter, ac, "denyThrownProjectiles")) return; - // 检查投掷物类型:箭、三叉戟、雪球、风蛋、龙息弹等 - String typeName = proj.getType().name(); - if (typeName.contains("ARROW") || typeName.contains("TRIDENT") - || typeName.contains("SNOWBALL") || typeName.contains("WIND_CHARGE") - || typeName.contains("DRAGON_FIREBALL") || typeName.contains("FIREBALL") - || typeName.contains("SHULKER_BULLET") || typeName.contains("LLAMA_SPIT")) { - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止在此区域投掷物品"); - } - } - - // ★ 门禁交互检测(已移除按钮/压力板,按钮/压力板由onRedstoneInteract处理) - - // ★ 门禁交互检测(门/栅栏门) - @EventHandler - public void onDoorOpen(PlayerInteractEvent e) { - if (e.getClickedBlock() == null) return; - if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; - Player p = e.getPlayer(); - Material mat = e.getClickedBlock().getType(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getClickedBlock().getX(), - e.getClickedBlock().getY(), - e.getClickedBlock().getZ()); - if (ac == null) return; - if (!getEffectiveDeny(p, ac, "denyDoorInteraction")) return; - String name = mat.name(); - if (name.contains("DOOR") || name.contains("GATE") || name.contains("FENCE_GATE")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止操作门/栅栏门"); - } - } - - // ★ 红石交互检测(按钮、压力板、中继器、比较器、拉杆、阳光传感器) - @EventHandler - public void onRedstoneInteract(PlayerInteractEvent e) { - if (e.getClickedBlock() == null) return; - Player p = e.getPlayer(); - Material mat = e.getClickedBlock().getType(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getClickedBlock().getX(), - e.getClickedBlock().getY(), - e.getClickedBlock().getZ()); - if (ac == null) return; - // 权限检查:如果允许操作,记录状态变化并返回 - if (!getEffectiveDeny(p, ac, "denyRedstoneInteraction")) { - // 记录红石元器件状态变化 - if (isRedstoneComponent(mat)) { - plugin.landRecordManager.recordRedstoneStateChange(ac, p, e.getClickedBlock()); - } - return; - } - // 权限禁止:检查是否为红石元器件并取消 - if (isRedstoneComponent(mat)) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止操作红石元件"); - } - } - - /** 判断材质是否为红石元器件(压力板、按钮、拉杆、中继器、比较器等) */ - private boolean isRedstoneComponent(Material mat) { - String name = mat.name(); - return mat == Material.REPEATER || mat == Material.COMPARATOR - || mat == Material.LEVER - || mat == Material.STONE_BUTTON || mat == Material.OAK_BUTTON - || mat == Material.SPRUCE_BUTTON || mat == Material.BIRCH_BUTTON - || mat == Material.JUNGLE_BUTTON || mat == Material.ACACIA_BUTTON - || mat == Material.DARK_OAK_BUTTON || mat == Material.MANGROVE_BUTTON - || mat == Material.CHERRY_BUTTON || mat == Material.BAMBOO_BUTTON - || mat == Material.CRIMSON_BUTTON || mat == Material.WARPED_BUTTON - || mat == Material.POLISHED_BLACKSTONE_BUTTON - || mat == Material.STONE_PRESSURE_PLATE || mat == Material.OAK_PRESSURE_PLATE - || mat == Material.SPRUCE_PRESSURE_PLATE || mat == Material.BIRCH_PRESSURE_PLATE - || mat == Material.JUNGLE_PRESSURE_PLATE || mat == Material.ACACIA_PRESSURE_PLATE - || mat == Material.DARK_OAK_PRESSURE_PLATE || mat == Material.MANGROVE_PRESSURE_PLATE - || mat == Material.CHERRY_PRESSURE_PLATE || mat == Material.BAMBOO_PRESSURE_PLATE - || mat == Material.CRIMSON_PRESSURE_PLATE || mat == Material.WARPED_PRESSURE_PLATE - || mat == Material.POLISHED_BLACKSTONE_PRESSURE_PLATE || mat == Material.LIGHT_WEIGHTED_PRESSURE_PLATE - || mat == Material.HEAVY_WEIGHTED_PRESSURE_PLATE - || mat == Material.DAYLIGHT_DETECTOR - || name.contains("REPEATER") || name.contains("COMPARATOR") - || name.contains("BUTTON") || name.contains("PRESSURE_PLATE") - || name.contains("DAYLIGHT"); - } - - // ★ 音频交互检测(音符盒、唱片机) - @EventHandler - public void onNoteblockInteract(PlayerInteractEvent e) { - if (e.getClickedBlock() == null) return; - Player p = e.getPlayer(); - Material mat = e.getClickedBlock().getType(); - AreaConfig ac = getArea( - p.getWorld().getName(), - e.getClickedBlock().getX(), - e.getClickedBlock().getY(), - e.getClickedBlock().getZ()); - if (ac == null) return; - if (!getEffectiveDeny(p, ac, "denyNoteblockJukebox")) return; - if (mat == Material.NOTE_BLOCK || mat.name().contains("JUKEBOX") - || mat.name().contains("CHERRY_JUKEBOX")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止操作音符盒/唱片机"); - } - } - - // ★ 农作物自然生长不受 denyCropHarvest 影响。 - // denyCropHarvest 只拦截"种植/收获"交互(BlockPlace / BlockBreak / 右键瓜果), - // 此处曾误取消 BlockGrowEvent,导致领地一旦开启"禁用农作物收获"庄稼就完全不长。 - - // ★ 耕地破坏(玩家在耕地上跳跃/掉落时触发) - @EventHandler - public void onFarmlandTrample(PlayerInteractEvent e) { - if (e.getAction() != Action.PHYSICAL) return; - Block block = e.getClickedBlock(); - if (block == null || block.getType() != Material.FARMLAND) return; - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - block.getX(), block.getY(), block.getZ()); - if (ac != null && ac.denyFarmlandTrample) { - e.setCancelled(true); - } - } - - // ★ 末影人行为控制(禁止末影人传送进入领地、在领地内传送、自然进入领地) - @EventHandler - public void onEnderTeleport(EntityTeleportEvent e) { - if (!e.getEntityType().name().equals("ENDERMAN")) return; - Location from = e.getFrom(); - Location to = e.getTo(); - // 检查起点是否在禁止末影人传送的领地内 - if (from != null) { - AreaConfig fromAc = getArea(from.getWorld().getName(), - from.getBlockX(), from.getBlockY(), from.getBlockZ()); - if (fromAc != null && fromAc.denyEnderTeleport) { - e.setCancelled(true); - return; - } - } - // 检查终点是否在禁止末影人传送的领地内 - if (to != null) { - AreaConfig toAc = getArea(to.getWorld().getName(), - to.getBlockX(), to.getBlockY(), to.getBlockZ()); - if (toAc != null && toAc.denyEnderTeleport) { - e.setCancelled(true); - return; - } - } - // 如果起点和终点都不在禁止传送的领地内,不干预(让MC自身逻辑处理) - // 注意:如果起点在允许传送的领地内,终点在禁止传送的领地内,已在上面处理 - // 如果起点在禁止传送的领地内,终点在允许传送的领地内,已在上面处理 - } - - // ★ 末影人行为控制(禁止末影人AI寻路进入领地) - @EventHandler - public void onEndermanPathfind(EntityPathfindEvent e) { - if (!e.getEntity().getType().name().equals("ENDERMAN")) return; - Location loc = e.getLoc(); - if (loc == null) return; - AreaConfig ac = getArea(loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (ac != null && ac.denyEnderTeleport) { - e.setCancelled(true); - } - } - - // ★ 末影人生成控制(自然生成时禁止进入领地) - @EventHandler - public void onEndermanSpawn(CreatureSpawnEvent e) { - if (!e.getEntityType().name().equals("ENDERMAN")) return; - if (e.getSpawnReason() != CreatureSpawnEvent.SpawnReason.NATURAL) return; - Location loc = e.getLocation(); - AreaConfig ac = getArea(loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (ac != null && ac.denyEnderTeleport) { - e.setCancelled(true); - } - } - - // ★ 判断方块是否为农作物 - private boolean isCropBlock(Material mat) { - String name = mat.name(); - return name.contains("WHEAT") || name.contains("CARROT") - || name.contains("POTATO") || name.contains("BEETROOT") - || name.contains("MELON") || name.contains("PUMPKIN") - || name.contains("COCOA") || name.contains("CROP") - || name.contains("SWEET_BERRY") || name.contains("BAMBOO") - || name.contains("SUGAR_CANE") || name.contains("NETHER_WART") - || name.contains("KELP") || name.contains("SEA_PICKLE") - || name.contains("CHORUS") || name.contains("TWISTING_VINES") - || name.contains("WEEPING_VINES") || name.contains("PITCHER") - || name.contains("TORCHFLOWER") - || mat == Material.MELON_STEM - || mat == Material.ATTACHED_MELON_STEM - || mat == Material.PUMPKIN_STEM - || mat == Material.ATTACHED_PUMPKIN_STEM; - } - - // ★ 判断方块是否为UI交互方块(非Container类型,需要单独检测) - private boolean isUIBlock(Material mat) { - return mat == Material.CRAFTING_TABLE || mat == Material.STONECUTTER - || mat == Material.GRINDSTONE || mat == Material.ANVIL - || mat == Material.CHIPPED_ANVIL || mat == Material.DAMAGED_ANVIL - || mat == Material.CARTOGRAPHY_TABLE || mat == Material.LOOM - || mat == Material.SMITHING_TABLE || mat == Material.ENCHANTING_TABLE - || mat == Material.BREWING_STAND - || mat.name().contains("SIGN") || mat.name().contains("BANNER") - || mat == Material.BARREL || mat.name().contains("SHULKER_BOX") - // ★ 补刀:漏掉的容器/UI方块(此前这些方块在领地外右键不受 denyContainer 管控) - || mat == Material.CRAFTER || mat.name().contains("BOOKSHELF") - || mat == Material.COMPOSTER || mat.name().contains("CAULDRON") - || mat == Material.ENDER_CHEST || mat.name().contains("CHEST") - || mat.name().contains("FURNACE") || mat.name().contains("BLAST_FURNACE") - || mat.name().contains("SMOKER") || mat == Material.HOPPER - || mat.name().contains("DISPENSER") || mat.name().contains("DROPPER") - || mat == Material.BEACON || mat.name().contains("LECTERN") - // ★ 展示架/展示柜(1.21.5+新方块):SHELF是MC 1.21.5 Spring Drop加入的 - || mat.name().contains("SHELF") || mat.name().contains("DISPLAY") - || mat.name().contains("SHOWCASE") || mat.name().contains("DECORATED_POT"); - } - - // ★ 判断物品是否为可种植的农作物种子(用于BlockPlaceEvent分层权限) - private boolean isCropSeed(Material mat) { - String name = mat.name(); - return name.contains("SEEDS") || name.contains("SAPLING") - || name.contains("STEM") || name.contains("PICKLE") - || mat == Material.BAMBOO || mat == Material.SUGAR_CANE - || mat == Material.CACTUS || mat == Material.COCOA_BEANS - || mat == Material.NETHER_WART || mat == Material.KELP - || mat == Material.TWISTING_VINES || mat == Material.WEEPING_VINES - || mat == Material.CHORUS_FLOWER || mat == Material.SWEET_BERRY_BUSH - || mat == Material.CARROT || mat == Material.POTATO - || mat == Material.BEETROOT_SEEDS || mat == Material.WHEAT_SEEDS - || mat == Material.PITCHER_POD || mat == Material.TORCHFLOWER_SEEDS; - } - - // ★ 检查是否为动物食物(包括玩家食物+动物专用食物) - private boolean isAnimalFood(Material mat) { - if (mat.isEdible()) return true; // 玩家食物(面包、胡萝卜等) - // 动物专用食物(玩家不可食用) - return mat == Material.WHEAT || mat == Material.BEETROOT_SEEDS - || mat == Material.MELON_SEEDS || mat == Material.PUMPKIN_SEEDS - || mat == Material.COCOA_BEANS || mat == Material.BONE_MEAL - || mat == Material.NAME_TAG || mat == Material.SADDLE - || mat.name().contains("SEEDS"); // 各种种子 - } - - // ★ 采集羊毛检测 - @EventHandler - public void onShearEntity(PlayerShearEntityEvent e) { - Player p = e.getPlayer(); - // 检查羊的位置 - Location loc = e.getEntity().getLocation(); - AreaConfig ac = getArea( - loc.getWorld().getName(), - loc.getBlockX(), - loc.getBlockY(), - loc.getBlockZ()); - if (ac == null) return; - if (!getEffectiveDeny(p, ac, "denyWoolShear")) return; - String entityType = e.getEntity().getType().name(); - if (entityType.contains("SHEEP") || entityType.contains("MOoshroom")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止在此区域剪毛"); - } - } - - // ★ 投喂动物检测 - @EventHandler - public void onEntityInteract(PlayerInteractEntityEvent e) { - if (!(e.getPlayer() instanceof Player)) return; - Player p = e.getPlayer(); - Entity entity = e.getRightClicked(); - - // ★ 蜜脾涂蜡实体(铜傀儡等):受涂蜡权限管控(与其他 deny* 对齐,仅约束普通访客) - ItemStack waxHand = p.getInventory().getItemInMainHand(); - if (waxHand != null && waxHand.getType() == Material.HONEYCOMB) { - Location waxLoc = entity.getLocation(); - AreaConfig waxAc = getArea(waxLoc.getWorld().getName(), - waxLoc.getBlockX(), waxLoc.getBlockY(), waxLoc.getBlockZ()); - if (isWaxDenied(p, waxAc)) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止涂蜡"); - return; - } - } - // ★ 村民交易权限(denyVillagerTrade):右键村民直接拦截,交易界面不会打开(2026-10-10) - if (entity instanceof org.bukkit.entity.Villager) { - Location vLoc = entity.getLocation(); - AreaConfig vAc = getArea(vLoc.getWorld().getName(), - vLoc.getBlockX(), vLoc.getBlockY(), vLoc.getBlockZ()); - if (vAc != null && getEffectiveDeny(p, vAc, "denyVillagerTrade")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止与此领地的村民交易"); - return; - } - } - // 只检测动物类 - String typeName = entity.getType().name(); - if (!(entity instanceof Animals || typeName.contains("IRON_GOLEM") - || typeName.contains("WOLF") || typeName.contains("CAT") - || typeName.contains("PARROT") || typeName.contains("FROG"))) { - return; - } - Location loc = entity.getLocation(); - AreaConfig ac = getArea( - loc.getWorld().getName(), - loc.getBlockX(), - loc.getBlockY(), - loc.getBlockZ()); - if (ac == null) return; - if (!getEffectiveDeny(p, ac, "denyAnimalFeeding")) return; - // 检查玩家手持的物品是否是食物(isEdible()只检测玩家食物,需额外检测动物食物) - ItemStack hand = p.getInventory().getItemInMainHand(); - if (hand != null && hand.getType() != Material.AIR && isAnimalFood(hand.getType())) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止在此区域投喂动物"); - } - } - - // ★ 村民交易兜底拦截(2026-10-10):交易界面已打开后(进服前打开 / 权限中途切换), - // 点击交易结果槽仍受 denyVillagerTrade 管控;正常路径由 onEntityInteract 右键拦截。 - @EventHandler - public void onVillagerTradeClick(org.bukkit.event.inventory.InventoryClickEvent e) { - if (!(e.getWhoClicked() instanceof Player)) return; - org.bukkit.inventory.Inventory topInv = e.getView().getTopInventory(); - if (topInv == null - || !(topInv.getHolder() instanceof org.bukkit.entity.Villager)) return; - Player p = (Player) e.getWhoClicked(); - org.bukkit.entity.Entity holder = (org.bukkit.entity.Entity) topInv.getHolder(); - Location loc = holder.getLocation(); - AreaConfig ac = getArea(loc.getWorld().getName(), - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); - if (ac != null && getEffectiveDeny(p, ac, "denyVillagerTrade")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止与此领地的村民交易"); - } - } - - // ★ 拴绳使用检测 - @EventHandler - public void onLeadUse(PlayerLeashEntityEvent e) { - if (!(e.getPlayer() instanceof Player)) return; - Player p = e.getPlayer(); - Entity entity = e.getEntity(); - Location loc = entity.getLocation(); - AreaConfig ac = getArea( - loc.getWorld().getName(), - loc.getBlockX(), - loc.getBlockY(), - loc.getBlockZ()); - if (ac == null) return; - if (!getEffectiveDeny(p, ac, "denyLead")) return; - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止在此区域使用拴绳"); - } - - /** - * 解析区域名:先精准匹配,再模糊匹配 - * @param input 用户输入的区域名 - * @return 匹配到的区域名,无匹配返回null - */ - public String resolveAreaName(String input) { - if (input == null || input.isEmpty()) return null; - if (input.equalsIgnoreCase("global")) return "global"; - - // 第一步:精准匹配 - for (String name : areas.keySet()) { - if (name.equalsIgnoreCase(input)) { - return name; - } - } - - // 第二步:包含匹配(用户只打了部分名字) - for (String name : areas.keySet()) { - if (name.contains(input) || input.contains(name)) { - return name; - } - } - - // 第三步:编辑距离模糊匹配(著称→主城) - String bestMatch = null; - int bestDistance = Integer.MAX_VALUE; - for (String name : areas.keySet()) { - int dist = levenshteinDistance( - input.toLowerCase(), name.toLowerCase()); - if (dist < bestDistance) { - bestDistance = dist; - bestMatch = name; - } - } - - // 容错阈值:编辑距离 <= 名字长度的一半 - if (bestMatch != null) { - int threshold = Math.max(1, bestMatch.length() / 2); - if (bestDistance <= threshold) { - return bestMatch; - } - } - - return null; - } - - /** - * ★ 严格解析领地名(写操作/权限敏感操作专用) - * 只做"完全一致"匹配:先区分大小写,再忽略大小写; - * 不做包含匹配,不做编辑距离模糊匹配。 - * 目的:防止攻击者用大小写变体、部分字符串或特殊字符 - * 把输入"归一化"成他人领地名后越权操作。 - */ - public String resolveAreaNameStrict(String input) { - if (input == null || input.isEmpty()) return null; - if (input.equalsIgnoreCase("global")) return "global"; - if (areas.containsKey(input)) return input; - for (String name : areas.keySet()) { - if (name.equalsIgnoreCase(input)) return name; - } - return null; - } - - /** - * ★ 大小写不敏感的领地重名检查(创建/改名防重) - */ - public boolean areaNameExists(String name) { - if (name == null || name.isEmpty()) return false; - for (String k : areas.keySet()) { - if (k.equalsIgnoreCase(name)) return true; - } - return false; - } - - /** - * ★ 领地名格式校验(创建/改名) - * 只允许中英文、数字、下划线、连字符,1-20 字符。 - * 拒绝空格、颜色代码、斜杠、点号等特殊字符——领地名会被 - * 直接当作白名单/配置文件名落盘,特殊字符会造成文件互相 - * 覆盖或路径穿越(Windows 下文件名不区分大小写,A/a 会互覆盖)。 - * @return null=合法,非 null=错误提示 - */ - public static String validateAreaName(String name) { - if (name == null || name.trim().isEmpty()) { - return "名字不能为空"; - } - String n = name.trim(); - if (n.length() > 20) return "名字不能超过20个字符"; - if (!n.matches("^[A-Za-z0-9一-龥_-]{1,20}$")) { - return "名字只能包含中英文、数字、下划线和连字符"; - } - if (n.equalsIgnoreCase("global")) { - return "global 是保留名,请换一个"; - } - return null; - } - - /** - * ★ 写操作权限闸门:领地所有者 / 领地管理员 / 插件管理员 / 控制台 - * 凡是"解析出领地名后直接改数据"的子命令,都必须先过这道闸。 - */ - private boolean requireLandOwnership(CommandSender sender, String areaName) { - if (areaName == null || areaName.equalsIgnoreCase("global")) { - return requireGlobalAdmin(sender, "全服级(global)操作"); - } - AreaConfig ac = areas.get(areaName); - if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); - return false; - } - if (!(sender instanceof Player)) return true; // 控制台=管理员 - if (!hasPermission((Player) sender, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限: " + areaName); - return false; - } - return true; - } - - /** - * ★ 全服级(global)写操作闸门:仅插件管理员 / 控制台 - */ - private boolean requireGlobalAdmin(CommandSender sender, String what) { - if (isAreaAdmin(sender)) return true; - sender.sendMessage("§c" + what + " 仅管理员可用"); - return false; - } - - /** - * 判断字符串是否是区域名(精准或模糊) - */ - private boolean isAreaName(String name) { - if (name.equalsIgnoreCase("global")) return false; - return resolveAreaName(name) != null; - } - - /** - * 获取模糊匹配的实际区域名(用于提示用户) - */ - private String getResolvedName(String input) { - return resolveAreaName(input); - } - - /** - * 编辑距离算法(莱文斯坦距离) - */ - private int levenshteinDistance(String s1, String s2) { - int[][] dp = new int[s1.length() + 1][s2.length() + 1]; - for (int i = 0; i <= s1.length(); i++) dp[i][0] = i; - for (int j = 0; j <= s2.length(); j++) dp[0][j] = j; - for (int i = 1; i <= s1.length(); i++) { - for (int j = 1; j <= s2.length(); j++) { - if (s1.charAt(i - 1) == s2.charAt(j - 1)) { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], - Math.min(dp[i - 1][j], dp[i][j - 1])); - } - } - } - return dp[s1.length()][s2.length()]; - } - - /** - * 智能重排参数:找出子命令位置,重排为标准顺序 - * 标准顺序:/protect 子命令 参数1 参数2 ... - */ - private String[] smartReorderArgs(String[] original) { - if (original.length <= 1) return original; - - Set knownSubs = new HashSet<>(Arrays.asList( - "add", "remove", "additem", "removeitem", - "addname", "removename", "listname", - "addwhite", "removewhite", "listwhite", - "list", "listitem", "创建", "create", "删除", "delete", "重载", "reload", - "工具", "wand", "expand", "contraction", - "on", "off", "tempon", "modeexempt", - "settp", "setwarp", "tp", "warp", - "setowner", "setadmin", "unsetadmin", "addvisitor", "removevisitor", "listvisitors", "transfer", "transfeland", - "info", "setowner", "addvisitor", "removevisitor", - "listvisitors", "transfer", "shop", - "menu", "菜单", "sdf1debug", "testclear", "cli", - "取消删除", "canceldelete", "canceladminchange", - "removemember", "addmember", - "config", "配置", - "public", "公共", "listpublic", "公共列表", "tpb", "传送公共", - "group", "用户组", "groupadd", "groupdel", "grouplist", "groupset", "groupedit", "groupdelconfig", "groupmembers", - "confirm_create", "accept", "deny" - )); - - String first = original[0].toLowerCase(); - if (knownSubs.contains(first)) { - return original; - } - - int subIndex = -1; - for (int i = 0; i < original.length; i++) { - if (knownSubs.contains(original[i].toLowerCase())) { - subIndex = i; - break; - } - } - - if (subIndex < 0) { - return original; - } - - String[] reordered = new String[original.length]; - reordered[0] = original[subIndex]; - int idx = 1; - for (int i = 0; i < original.length; i++) { - if (i == subIndex) continue; - reordered[idx] = original[i]; - idx++; - } - - return reordered; - } - - // ==================== 管理命令 ==================== - public boolean handleCommand(CommandSender sender, - String[] args) { - args = smartReorderArgs(args); - if (args.length == 0) { - // ★ 读取用户偏好决定打开GUI还是CLI - if (sender instanceof Player) { - Player p = (Player) sender; - int uiMode = 1; - try { - uiMode = plugin.getDb() - .getUiMode(p.getName()); - } catch (Exception ignored) {} - if (uiMode == 0) { - // GUI模式 → 打开领地管理GUI - if (plugin.areaGUIManager != null) { - plugin.areaGUIManager.openMainMenu(p); - } else { - p.sendMessage("§cGUI管理器未初始化,显示文本帮助"); - showHelp(sender); - } - return true; - } - } - // CLI模式 → 显示交互式菜单 - if (sender instanceof Player) { - Player p = (Player) sender; - if (plugin.areaCLIManager != null) { - plugin.areaCLIManager.showMainMenu(p); - return true; - } - } - showHelp(sender); - return true; - } - String sub = args[0].toLowerCase(); - - // ★ 管理员操作时,自动分配无owner的领地 - if (sender instanceof Player && isAreaAdmin(sender)) { - assignUnownedLandsToAdmin((Player) sender); - } -// ===== 诊断命令 ===== - if (sub.equals("sdf1debug")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - UUID uid = p.getUniqueId(); - - sender.sendMessage("§e===== 调试信息 ====="); - sender.sendMessage("§7当前区域: " - + playerCurrentArea.get(uid)); - sender.sendMessage("§7标记效果数: " - + (playerMarkedEffects.containsKey(uid) - ? playerMarkedEffects.get(uid).size() - : "无标记")); - sender.sendMessage("§7待清理任务: " - + pendingClearTask.containsKey(uid)); - sender.sendMessage("§7区域数: " + areas.size()); - - if (playerMarkedEffects.containsKey(uid)) { - List marked = - playerMarkedEffects.get(uid); - for (PotionEffectType type : marked) { - sender.sendMessage("§7 标记: " - + type.getName() - + " 身上有=" - + (p.getPotionEffect(type) != null)); - } - } - return true; - } - -// ===== 强制清理命令(测试用)===== - if (sub.equals("testclear")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - UUID uid = p.getUniqueId(); - - List marked = - playerMarkedEffects.remove(uid); - if (marked != null) { - for (PotionEffectType type : marked) { - p.removePotionEffect(type); - } - sender.sendMessage("§a已强制清理 " - + marked.size() + " 个效果"); - } else { - sender.sendMessage("§c无标记效果可清理"); - } - - // 取消延时任务 - if (pendingClearTask.containsKey(uid)) { - pendingClearTask.get(uid).cancel(); - pendingClearTask.remove(uid); - pendingClearArea.remove(uid); - pendingClearEffects.remove(uid); - sender.sendMessage("§a已取消延时任务"); - } - - removePlayerEffects(uid, - playerCurrentArea.getOrDefault(uid, "")); - sender.sendMessage("§aDB已清理"); - return true; - } - - // ===== 工具 ===== - if (sub.equals("工具") || sub.equals("wand")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - // ★ 5秒冷却 - long now = System.currentTimeMillis(); - Long lastGive = wandCooldownMap.get(p.getUniqueId()); - if (lastGive != null && now - lastGive < 5000) { - long remaining = (5000 - (now - lastGive)) / 1000 + 1; - p.sendMessage("§c§l[防护] §f请等待 " + remaining + " 秒后再获取"); - return true; - } - // ★ 检查是否已拥有 - for (ItemStack item : p.getInventory().getContents()) { - if (item != null && item.getType() == WAND - && item.hasItemMeta() && item.getItemMeta() != null - && item.getItemMeta().hasDisplayName() - && item.getItemMeta().getDisplayName().contains("区域选择工具")) { - p.sendMessage("§e§l[防护] §f你已拥有圈地工具"); - wandCooldownMap.put(p.getUniqueId(), now); - return true; - } - } - // ★ SN 管控:1人同种类仅限1个 - if (plugin.getSnManager() != null - && plugin.getSnManager().isBlocked( - p, SnManager.TYPE_WAND)) { - long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName()); - if (cdMin > 0) { - p.sendMessage("§c§l[防护] §f注销冷静期未结束,还剩约 §e" + cdMin + " §f分钟,期间无法申领区域选择工具"); - } else { - p.sendMessage("§c§l[防护] §f你已有区域选择工具的有效登记," - + "旧SN未注销前不能重新申领"); - } - wandCooldownMap.put(p.getUniqueId(), now); - return true; - } - ItemStack tool = new ItemStack(WAND); - ItemMeta meta = tool.getItemMeta(); - meta.setDisplayName("§a§l区域选择工具"); - meta.setLore(Arrays.asList( - "§7左键: 位置1", "§7右键: 位置2")); - tool.setItemMeta(meta); - if (plugin.getSnManager() != null) { - String sn = plugin.getSnManager().applySn( - p, SnManager.TYPE_WAND, "区域选择工具"); - if (sn == null) { - wandCooldownMap.put(p.getUniqueId(), now); - return true; - } - plugin.getSnManager().writeSn( - tool, sn, SnManager.TYPE_WAND); - } - p.getInventory().addItem(tool); - p.sendMessage("§a§l[防护] §f已获取选择工具"); - wandCooldownMap.put(p.getUniqueId(), now); - return true; - } - - // ===== GUI菜单 ===== - if (sub.equals("menu") || sub.equals("菜单")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (plugin.areaGUIManager != null) { - plugin.areaGUIManager.openMainMenu(p); - } else { - p.sendMessage("§cGUI管理器未初始化"); - } - return true; - } - - // ===== 回声碎片(领地记录查询工具)===== - if (sub.equals("echoshard") || sub.equals("回声碎片")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (plugin.landRecordManager != null) { - plugin.landRecordManager.giveEchoShard(p); - } else { - p.sendMessage("§c领地记录管理器未初始化"); - } - return true; - } - - // ===== UI模式偏好 ===== - if (sub.equals("uimode") || sub.equals("偏好")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - int current = plugin.getDb() - .getUiMode(p.getName()); - p.sendMessage("§e当前模式: §f" - + (current == 0 ? "GUI" : "CLI")); - p.sendMessage("§7用法: /protect uimode "); - return true; - } - String mode = args[1].toLowerCase(); - if (mode.equals("gui") || mode.equals("0")) { - plugin.getDb().setUiMode(p.getName(), 0); - p.sendMessage("§a已切换为 §6GUI §a模式"); - // ★ 立即打开GUI - if (plugin.areaGUIManager != null) { - plugin.areaGUIManager.openMainMenu(p); - } - } else if (mode.equals("cli") || mode.equals("1")) { - plugin.getDb().setUiMode(p.getName(), 1); - p.sendMessage("§a已切换为 §6CLI §a模式"); - // ★ 立即显示CLI菜单 - if (plugin.areaCLIManager != null) { - plugin.areaCLIManager.showMainMenu(p); - } - } else { - p.sendMessage("§c用法: /protect uimode "); - } - return true; - } - - // ===== CLI交互式菜单 ===== - if (sub.equals("cli")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (plugin.areaCLIManager == null) { - p.sendMessage("§cCLI管理器未初始化"); - return true; - } - String action = args.length >= 2 ? args[1] : "menu"; - switch (action) { - case "menu": - plugin.areaCLIManager.showMainMenu(p); - break; - case "lands": - int page = 1; - if (args.length >= 3) { - try { page = Integer.parseInt(args[2]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showLandList(p, page); - break; - case "manage": - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli manage <领地名> [页码]"); - break; - } - int permPage = 1; - if (args.length >= 4) { - try { permPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showLandManage(p, args[2], permPage); - break; - case "members": - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli members <领地名> [页码]"); - break; - } - int memberPage = 1; - if (args.length >= 4) { - try { memberPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showMemberList(p, args[2], memberPage); - break; - case "visitorperm": - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli visitorperm <领地名> [页码]"); - break; - } - int vPermPage = 1; - if (args.length >= 4) { - try { vPermPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showVisitorPerm(p, args[2], vPermPage); - break; - case "toggle": - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli toggle <领地名> <权限key> [页码]"); - break; - } - int togglePage = 1; - if (args.length >= 5) { - try { togglePage = Integer.parseInt(args[4]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.togglePerm(p, args[2], args[3], togglePage); - // ★ 切换后刷新到访客授权页面(停留当前页面,不退出) - plugin.areaCLIManager.refreshCurrentPage(p); - break; - case "config": - // ★ CLI全局配置页面 - plugin.areaCLIManager.showConfigPage(p); - break; - case "memberperm": - // ★ 成员独立权限列表 - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli memberperm <领地名> [页码]"); - break; - } - int mpPage = 1; - if (args.length >= 4) { - try { mpPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showMemberPermList(p, args[2], mpPage); - break; - case "playerperm": - // ★ 某成员的独立权限编辑 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli playerperm <领地名> <玩家> [页码]"); - break; - } - int ppPage = 1; - if (args.length >= 5) { - try { ppPage = Integer.parseInt(args[4]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showPlayerPerm(p, args[2], args[3], ppPage); - break; - case "toggleplayerperm": - // ★ 切换某成员的独立权限 - if (args.length < 5) { - p.sendMessage("§c用法: /protect cli toggleplayerperm <领地名> <玩家> <权限key> [页码]"); - break; - } - int tpPage = 1; - if (args.length >= 6) { - try { tpPage = Integer.parseInt(args[5]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.togglePlayerPerm(p, args[2], args[3], args[4], tpPage); - break; - case "toggleadmin": - // ★ 切换玩家的管理员身份(领地主专属) - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli toggleadmin <领地名> <玩家> [页码]"); - break; - } - int taPage = 1; - if (args.length >= 5) { - try { taPage = Integer.parseInt(args[4]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.toggleAdmin(p, args[2], args[3], taPage); - break; - case "clearplayerperm": - // ★ 清除某成员所有自定义权限 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli clearplayerperm <领地名> <玩家>"); - break; - } - plugin.areaCLIManager.clearPlayerPerm(p, args[2], args[3]); - break; - case "effectsmgmt": - // ★ 效果管理菜单 - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli effectsmgmt <领地名> [子页]"); - break; - } - int effPage = 1; - if (args.length >= 4) { - try { effPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showEffectsManagement(p, args[2], effPage); - break; - case "effectsclearall": - // ★ 切换「全清负面效果」 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsclearall <领地名> on/off"); - break; - } - plugin.areaCLIManager.toggleClearAllBadEffects(p, args[2], args[3]); - // 刷新:回到效果管理(subPage 1) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 1); - break; - case "effectsdenyall": - // ★ 切换「禁止所有效果」 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsdenyall <领地名> on/off"); - break; - } - plugin.areaCLIManager.toggleDenyAllEffects(p, args[2], args[3]); - // 刷新:回到效果管理(subPage 1) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 1); - break; - case "effectsclearremove": - // ★ 移除指定清除效果 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsclearremove <领地名> <序号>"); - break; - } - int clrIdx = 1; - try { clrIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} - plugin.areaCLIManager.removeClearEffect(p, args[2], clrIdx); - // 刷新:回到单清效果列表(subPage 2) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 2); - break; - case "effectsclearadd": - // ★ 添加清除效果 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsclearadd <领地名> <效果名>"); - break; - } - plugin.areaCLIManager.addClearEffect(p, args[2], args[3]); - // 刷新:回到单清效果列表(subPage 2) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 2); - break; - case "effectsaddremove": - // ★ 移除指定增益效果 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsaddremove <领地名> <序号>"); - break; - } - int giveIdx = 1; - try { giveIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} - plugin.areaCLIManager.removeGiveEffect(p, args[2], giveIdx); - // 刷新:回到增益效果列表(subPage 3) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 3); - break; - case "effectsaddadd": - // ★ 添加增益效果: 效果名 [等级] [秒数] - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsaddadd <领地名> <效果名> [等级] [秒数]"); - break; - } - String effName = args[3]; - String effLevel = "1"; - String effDuration = "999"; - if (args.length >= 5) effLevel = args[4]; - if (args.length >= 6) effDuration = args[5]; - plugin.areaCLIManager.addGiveEffect(p, args[2], effName, effLevel, effDuration); - // 刷新:回到增益效果列表(subPage 3) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 3); - break; - case "effectsaddedit": - // ★ 编辑增益效果等级或时长: 序号 level|duration - if (args.length < 5) { - p.sendMessage("§c用法: /protect cli effectsaddedit <领地名> <序号> "); - break; - } - plugin.areaCLIManager.startEditGiveEffect(p, args[2], args[3], args[4]); - break; - case "effectsbadremove": - // ★ 移除指定负面效果(2026-10-04 从增益拆出) - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsbadremove <领地名> <序号>"); - break; - } - int badIdx = 1; - try { badIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} - plugin.areaCLIManager.removeBadEffect(p, args[2], badIdx); - // 刷新:回到负面效果列表(subPage 6) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 6); - break; - case "effectsbadadd": - // ★ 添加负面效果: 效果名 [等级] [秒数] - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli effectsbadadd <领地名> <效果名> [等级] [秒数]"); - break; - } - String badEffName = args[3]; - String badEffLevel = "1"; - String badEffDuration = "999"; - if (args.length >= 5) badEffLevel = args[4]; - if (args.length >= 6) badEffDuration = args[5]; - plugin.areaCLIManager.addBadEffect(p, args[2], badEffName, badEffLevel, badEffDuration); - // 刷新:回到负面效果列表(subPage 6) - plugin.areaCLIManager.showEffectsManagement(p, args[2], 6); - break; - case "effectsbadedit": - // ★ 编辑负面效果等级或时长: 序号 level|duration - if (args.length < 5) { - p.sendMessage("§c用法: /protect cli effectsbadedit <领地主> <序号> "); - break; - } - plugin.areaCLIManager.startEditBadEffect(p, args[2], args[3], args[4]); - break; - case "announcement": - // ★ 领地公告管理 - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli announcement <领地名> [子页]"); - break; - } - int annPage = 1; - if (args.length >= 4) { - try { annPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} - } - plugin.areaCLIManager.showAnnouncementManagement(p, args[2], annPage); - break; - case "confiscate": - // ★ 违禁品管理面板(2026-10-11) - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli confiscate <领地名>"); - break; - } - plugin.areaCLIManager.showConfiscateManagement(p, args[2]); - break; - case "confiscateret": - // ★ 切换「退出领地是否归还没收物」 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli confiscateret <领地名> on/off"); - break; - } - plugin.areaCLIManager.toggleReturnOnLeave(p, args[2], args[3]); - plugin.areaCLIManager.showConfiscateManagement(p, args[2]); - break; - case "confiscateadd": - // ★ 添加禁带物品(交互式输入) - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli confiscateadd <领地名>"); - break; - } - plugin.areaCLIManager.startAddConfiscateItem(p, args[2]); - break; - case "confiscateremove": - // ★ 移除指定禁带物品 - if (args.length < 4) { - p.sendMessage("§c用法: /protect cli confiscateremove <领地名> <序号>"); - break; - } - plugin.areaCLIManager.removeConfiscateItem(p, args[2], args[3]); - plugin.areaCLIManager.showConfiscateManagement(p, args[2]); - break; - case "rename": - // ★ 领地改名(交互式输入) - if (args.length < 3) { - p.sendMessage("§c用法: /protect cli rename <领地名>"); - break; - } - { - String rnLandName = args[2]; - AreaConfig rnAc = getLand(rnLandName); - if (rnAc == null) { - p.sendMessage("§c领地不存在: " + rnLandName); - break; - } - boolean rnOwner = samePlayer(p.getName(), rnAc.owner); - boolean rnAdmin = isAreaAdmin(p); - if (!rnOwner && !rnAdmin) { - p.sendMessage("§c需要领地所有者或管理员权限"); - break; - } - // 设置等待输入状态 - pendingEffectInput.put(p.getUniqueId(), - new String[]{rnLandName, "rename_land"}); - p.sendMessage(AreaCLIManager.header("领地改名: " + rnLandName)); - long[] cd = getRenameCooldown(p); - if (cd != null) { - long rm = cd[0] / 60; - long rs = cd[0] % 60; - p.sendMessage(Component.text("§c冷却中!已用" + cd[1] + "/" + RENAME_LIMIT + "次," - + rm + "分" + rs + "秒后解锁")); - } else { - p.sendMessage(Component.text("§7剩余次数: §a" + getRemainingRenames(p) + "/" + RENAME_LIMIT)); - } - p.sendMessage(Component.text("§e请在聊天栏输入新的领地名:")); - p.sendMessage(Component.text("§7输入 §c取消 §7放弃修改")); - } - break; - case "create": - // 跳转到创建命令 - // 直接转发 - break; - default: - plugin.areaCLIManager.showMainMenu(p); - break; - } - return true; - } - - // ===== 创建 ===== - if (sub.equals("创建") || sub.equals("create")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - 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以自身为中心) - if (args.length < 2) { - if (!pos1.containsKey(u) || !pos2.containsKey(u)) { - // 无选点:快速创建3×3 - if (quickCreateLand(p)) { - p.sendMessage("§7领地创建成功,如需精细控制请使用圈地工具选点后再次使用"); - } - } else { - // 有选点但没有名字:用默认名创建 - String autoName = p.getName(); - if (areaNameExists(autoName)) { - for (int i = 0; i <= 999; i++) { - String c = autoName + i; - if (!areaNameExists(c)) { autoName = c; break; } - } - } - p.sendMessage("§e用法: /protect 创建 <自定义名>"); - p.sendMessage("§7自动创建领地名: §e" + autoName); - // 通过confirm_create走标准流程 - p.performCommand("protect confirm_create " + autoName); - } - return true; - } - - String areaName = args[1].trim(); - // ★ 领地名格式校验:特殊字符会污染按名字落盘的白名单/配置文件 - String nameErr = validateAreaName(areaName); - if (nameErr != null) { - p.sendMessage("§c§l[防护] §f领地名无效: " + nameErr); - return true; - } - // 检查重名(不区分大小写:Windows 文件名大小写不敏感会互相覆盖) - if (areaNameExists(areaName)) { - p.sendMessage("§c区域已存在(大小写不同也算重复)"); - return true; - } - // ★ 检查每人领地数量上限(用户组专属上限)— 同时查内存和DB兜底 - UserGroupManager ugm = plugin.getUserGroup(); - int playerMaxLands = (ugm != null) ? ugm.getPlayerMaxLands(p.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; - long playerLandCount = areas.values().stream() - .filter(a -> samePlayer(p.getName(), a.owner)) - .count(); - // ★ DB兜底:如果内存计数<上限但DB实际已有更多领地(内存可能过期) - if (playerLandCount < playerMaxLands && dbConnection != null) { - try { - java.sql.PreparedStatement cntStmt = dbConnection.prepareStatement( - "SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); - cntStmt.setString(1, p.getName()); - java.sql.ResultSet cntRs = cntStmt.executeQuery(); - if (cntRs.next()) { - long dbCount = cntRs.getLong("cnt"); - if (dbCount > playerLandCount) playerLandCount = dbCount; - } - cntRs.close(); cntStmt.close(); - } catch (Exception ignored) {} - } - if (playerLandCount >= playerMaxLands) { - String groupHint = (ugm != null && ugm.getHighestGroup(p.getName()) != null) - ? "(用户组: " + ugm.getHighestGroup(p.getName()).displayName + ")" : ""; - p.sendMessage("§c§l[防护] §f已达领地上限!每人最多 §e" + playerMaxLands + " §f个领地(当前: " + playerLandCount + "个)" + groupHint); - return true; - } - Location l1 = pos1.get(u); - Location l2 = pos2.get(u); - - // ★ 领地计费:计算面积并预估价格(用户组专属价格) - int width = Math.abs(l2.getBlockX() - l1.getBlockX()) + 1; - int length = Math.abs(l2.getBlockZ() - l1.getBlockZ()) + 1; - int area = width * length; - int effectivePricePerSqm = (ugm != null) ? ugm.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; - int cost = area * effectivePricePerSqm; - String priceSource = (effectivePricePerSqm != globalCreatePricePerSqm) ? "§7(用户组优惠价)" : ""; - - // ★ Issue 4: 创建确认提示 — 显示预估价格 + 交互链接 - BondManager bm = plugin.getBonds(); - int balance = (bm != null) ? bm.getBonds(p.getName()) : 0; - String balanceStr = (bm != null) ? String.valueOf(balance) : "?"; - p.sendMessage("§e§l[防护] §f创建领地 §a" + areaName + " §f的预览:"); - p.sendMessage("§7 面积: §f" + area + "㎡(" + width + "×" + length + ")"); - if (width > globalMaxLandSideBlocks || length > globalMaxLandSideBlocks - || area > globalMaxFreeAreaSqm) { - p.sendMessage("§e§l[防护] §f选区超出免审上限(单次 " + globalMaxFreeAreaSqm - + "㎡ / 边长 " + globalMaxLandSideBlocks + " 格),确认创建后将提交管理员现场审批"); - } - p.sendMessage("§7 单价: §f" + effectivePricePerSqm + "/㎡" + priceSource); - p.sendMessage("§7 费用: §e" + cost + " §7债券 余额: §a" + balanceStr + " §7债券"); - if (bm != null && balance < cost) { - p.sendMessage("§c§l[防护] §f余额不足!需要 §e" + cost + " §f,当前 §e" + balance); - return true; - } - if (p instanceof Player) { - Player pp = (Player) p; - String confirmCmd = "/protect confirm_create " + areaName; - pp.sendMessage(Component.empty() - .append(Component.text("§a§l[✅ 点击确认创建]")) - .hoverEvent(HoverEvent.showText(Component.text("§e点击确认创建领地 §f" + areaName + "\n§7费用: " + cost + " 债券"))) - .clickEvent(ClickEvent.runCommand(confirmCmd)) - ); - pp.sendMessage(Component.text("§7或手动输入: §f/protect 创建 " + areaName)); - } - return true; - } - - // ===== 确认创建领地(交互链接触发) ===== - if (sub.equals("confirm_create")) { - 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); - if (cnErr != null) { - cp.sendMessage("§c§l[防护] §f领地名无效: " + cnErr); - return true; - } - // 检查选点 - if (!pos1.containsKey(cp.getUniqueId()) || !pos2.containsKey(cp.getUniqueId())) { - cp.sendMessage("§c请先用工具选点"); return true; - } - // 检查重名(不区分大小写) - if (areaNameExists(confirmName)) { cp.sendMessage("§c区域已存在(大小写不同也算重复)"); return true; } - // ★ 检查上限(含DB兜底) - UserGroupManager cugm = plugin.getUserGroup(); - int cMaxLands = (cugm != null) ? cugm.getPlayerMaxLands(cp.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; - long cLandCount = areas.values().stream().filter(a -> samePlayer(cp.getName(), a.owner)).count(); - if (cLandCount < cMaxLands && dbConnection != null) { - try { - java.sql.PreparedStatement cs = dbConnection.prepareStatement("SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); - cs.setString(1, cp.getName()); - java.sql.ResultSet cr = cs.executeQuery(); - if (cr.next()) { long dbc = cr.getLong("cnt"); if (dbc > cLandCount) cLandCount = dbc; } - cr.close(); cs.close(); - } catch (Exception ignored) {} - } - if (cLandCount >= cMaxLands) { cp.sendMessage("§c§l[防护] §f已达领地上限!"); return true; } - Location cl1 = pos1.get(cp.getUniqueId()); - Location cl2 = pos2.get(cp.getUniqueId()); - int cw = Math.abs(cl2.getBlockX() - cl1.getBlockX()) + 1; - int cl = Math.abs(cl2.getBlockZ() - cl1.getBlockZ()) + 1; - int cArea = cw * cl; - int cPrice = (cugm != null) ? cugm.getPlayerLandPricePerSqm(cp.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; - int cCost = cArea * cPrice; - - // ★ 创建面积限制(2026-09-29):选区面积/边长超免审上限 → 转管理员现场审批,暂不扣费 - if (!isAreaAdmin(cp) - && (cArea > globalMaxFreeAreaSqm - || cw > globalMaxLandSideBlocks - || cl > globalMaxLandSideBlocks)) { - registerPendingCreate(cp, confirmName, cl1, cl2, cw, cl, cArea, cCost); - return true; - } - - // 扣费 - int chargedCost = 0; - if (cCost > 0) { - BondManager cbm = plugin.getBonds(); - if (cbm != null) { - if (!cbm.deductBonds(cp.getName(), cCost, "land_create", cp.getName(), cp.getName(), "创建领地: " + confirmName + " (" + cArea + "㎡×" + cPrice + ")")) { - cp.sendMessage("§c§l[防护] §f扣费失败,请稍后重试"); return true; - } - String cSrc = (cPrice != globalCreatePricePerSqm) ? "(用户组优惠价)" : ""; - cp.sendMessage("§a§l[防护] §f创建领地扣除 §e" + cCost + " §f债券(" + cArea + "㎡×" + cPrice + "/㎡)" + cSrc); - chargedCost = cCost; - } - } - AreaConfig cac = new AreaConfig(); - cac.name = confirmName; cac.owner = cp.getName(); cac.world = cl1.getWorld().getName(); - cac.x1 = cl1.getBlockX(); cac.z1 = cl1.getBlockZ(); cac.x2 = cl2.getBlockX(); cac.z2 = cl2.getBlockZ(); - cac.yMin = 0; cac.yMax = 255; - cac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数) - saveAreaToDb(cac); - areas.put(confirmName, cac); // ★ 立即更新内存 - cp.sendMessage("§a§l[防护] §f区域 " + confirmName + " 已创建 (owner: " + cp.getName() + ")"); - loadAllAreas(); - return true; - } - - // ===== 和平白名单 ===== - if (sub.equals("addname")) { - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别: " - + args[1] + " 或 " + args[2]); - return true; - } - AreaConfig ac = areas.get(parsed[0]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - // ★ 权限闸门:只有领地主/领地管理员才能改和平白名单 - if (!requireLandOwnership(sender, parsed[0])) return true; - ac.peaceWhitelist.add(parsed[1]); - saveAreaToDb(ac); - sender.sendMessage("§a已添加: " - + parsed[1] + " → " - + parsed[0] + " 和平白名单"); - return true; - } - sender.sendMessage("§c用法: /protect addname " - + "<区域> <名字>"); - return true; - } - if (sub.equals("removename")) { - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别"); - return true; - } - AreaConfig ac = areas.get(parsed[0]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - // ★ 权限闸门:只有领地主/领地管理员才能改和平白名单 - if (!requireLandOwnership(sender, parsed[0])) return true; - ac.peaceWhitelist.remove(parsed[1]); - saveAreaToDb(ac); - sender.sendMessage("§a已移除: " - + parsed[1] + " ← " + parsed[0]); - return true; - } - sender.sendMessage("§c用法: /protect removename " - + "<区域> <名字>"); - return true; - } - if (sub.equals("listname")) { - if (args.length < 2) { - sender.sendMessage( - "§c用法: /protect listname <区域>"); - return true; - } - AreaConfig ac = areas.get(args[1]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - sender.sendMessage("§a" + args[1] - + " 和平白名单:"); - if (ac.peaceWhitelist.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String n : ac.peaceWhitelist) { - sender.sendMessage("§7 - " + n); - } - } - return true; - } - - // ===== 模式排除 ===== - if (sub.equals("addwhite")) { - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别"); - return true; - } - AreaConfig ac = areas.get(parsed[0]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - // ★ 权限闸门:只有领地主/领地管理员才能改模式排除名单 - if (!requireLandOwnership(sender, parsed[0])) return true; - ac.modeExempt.add(parsed[1]); - saveAreaToDb(ac); - sender.sendMessage("§a已添加: " - + parsed[1] + " → " - + parsed[0] + " 模式排除"); - return true; - } - sender.sendMessage("§c用法: /protect addwhite " - + "<区域> <玩家>"); - return true; - } - if (sub.equals("removewhite")) { - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别"); - return true; - } - AreaConfig ac = areas.get(parsed[0]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - // ★ 权限闸门:只有领地主/领地管理员才能改模式排除名单 - if (!requireLandOwnership(sender, parsed[0])) return true; - ac.modeExempt.remove(parsed[1]); - saveAreaToDb(ac); - sender.sendMessage("§a已移除: " - + parsed[1] + " ← " - + parsed[0] + " 模式排除"); - return true; - } - sender.sendMessage("§c用法: /protect removewhite " - + "<区域> <玩家>"); - return true; - } - if (sub.equals("listwhite")) { - if (args.length < 2) { - sender.sendMessage( - "§c用法: /protect listwhite <区域>"); - return true; - } - AreaConfig ac = areas.get(args[1]); - if (ac == null) { - sender.sendMessage("§c区域不存在"); - return true; - } - sender.sendMessage("§a" + args[1] - + " 模式排除名单:"); - if (ac.modeExempt.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String n : ac.modeExempt) { - sender.sendMessage("§7 - " + n); - } - } - return true; - } - - // ===== 列表 ===== - if (sub.equals("列表") || sub.equals("list")) { - if (args.length == 2) { - if (args[1].equalsIgnoreCase("global")) { - sender.sendMessage("§a全局白名单:"); - if (globalPlayerWhitelist.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String n - : globalPlayerWhitelist) { - sender.sendMessage("§7 - " + n); - } - } - return true; - } - String an = args[1]; - Set wl = - areaPlayerWhitelist.get(an); - sender.sendMessage("§a" + an + " 白名单:"); - if (wl == null || wl.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String n : wl) { - sender.sendMessage("§7 - " + n); - } - } - return true; - } - if (areas.isEmpty()) { - sender.sendMessage("§7暂无区域"); - } else { - sender.sendMessage("§e§l区域列表:"); - for (Map.Entry en - : areas.entrySet()) { - sender.sendMessage("§a " + en.getKey() - + " §7规则:" - + en.getValue().ruleCount() - + "条"); - } - } - return true; - } - - // ===== config 全局配置 ===== - if (sub.equals("config") || sub.equals("配置")) { - if (!isAreaAdmin(sender)) { - sender.sendMessage("§c需要管理员权限"); - return true; - } - if (args.length < 3) { - // 查看配置 - sender.sendMessage("§e§l==== 全局配置 ===="); - sender.sendMessage("§a创建价格(每㎡): §f" + globalCreatePricePerSqm); - sender.sendMessage("§a每人最大领地数: §f" + globalMaxLandsPerPlayer); - sender.sendMessage("§a删除退费比例(%): §f" + globalRefundRatioPerCent - + (globalRefundRatioPerCent <= 0 ? " §7(已关闭退费)" - : (globalRefundRatioPerCent >= 100 ? " §7(全额退款)" : " §7(原价×比例)"))); - sender.sendMessage("§a单次扩建免审面积(㎡): §f" + globalMaxFreeAreaSqm + " §7(超限转管理员审批)"); - sender.sendMessage("§a领地免审边长(格): §f" + globalMaxLandSideBlocks + "×" + globalMaxLandSideBlocks + " §7(超限转管理员审批)"); - sender.sendMessage("§7用法: /protect config "); - sender.sendMessage("§7可用key: create_price, max_lands, default_height, peace_duration, refund_ratio, max_free_area, max_land_side"); - return true; - } - String key = args[1]; - String value = args[2]; - try { - switch (key) { - case "create_price": - globalCreatePricePerSqm = parseSmartNumber(value, globalCreatePricePerSqm, 0); - updateAreaConfig("create_price_per_sqm", String.valueOf(globalCreatePricePerSqm)); - break; - case "max_lands": - globalMaxLandsPerPlayer = parseSmartNumber(value, globalMaxLandsPerPlayer, 1); - updateAreaConfig("max_lands_per_player", String.valueOf(globalMaxLandsPerPlayer)); - break; - case "default_height": - int newHeight = parseSmartNumber(value, 255, 1); - updateAreaConfig("default_height", String.valueOf(newHeight)); - break; - case "peace_duration": - int newDuration = parseSmartNumber(value, 3600, 0); - updateAreaConfig("peace_mode_max_duration", String.valueOf(newDuration)); - break; - case "refund_ratio": - int newRatio = parseSmartNumber(value, globalRefundRatioPerCent, 0); - if (newRatio > 100) newRatio = 100; // 上限100%=全额退款 - if (newRatio < 0) newRatio = 0; // 0=关闭退费 - globalRefundRatioPerCent = newRatio; - updateAreaConfig("refund_ratio_per_cent", String.valueOf(newRatio)); - break; - case "max_free_area": - int newFree = parseSmartNumber(value, globalMaxFreeAreaSqm, 1); - if (newFree < 1) newFree = 1; - globalMaxFreeAreaSqm = newFree; - updateAreaConfig("max_free_area_sqm", String.valueOf(newFree)); - break; - case "max_land_side": - int newSide = parseSmartNumber(value, globalMaxLandSideBlocks, 1); - if (newSide < 1) newSide = 1; - globalMaxLandSideBlocks = newSide; - updateAreaConfig("max_land_side_blocks", String.valueOf(newSide)); - break; - default: - sender.sendMessage("§c未知配置项: " + key); - return true; - } - // ★ 配置变更:立即触发PHP同步 - try { - if (plugin.webManager != null) { - plugin.webManager.requestImmediateLandSync(); - } - } catch (Exception ignored) {} - - // ★ 更新后显示完整配置页面(而非仅打印"已更新") - if (plugin.areaCLIManager != null) { - plugin.areaCLIManager.showConfigPage((Player) sender); - } else { - sender.sendMessage("§a配置已更新"); - } - } catch (Exception e) { - sender.sendMessage("§c无效的值: " + value + "(支持: 纯数字、+/-前缀、中文数字、罗马数字、英文数字)"); - } - return true; - } - - // ===== 重载 ===== - if (sub.equals("重载") || sub.equals("reload")) { - reload(); - sender.sendMessage("§a已重载 " - + areas.size() + " 个区域"); - return true; - } - - // ===== 公共建筑设施 ===== - if (sub.equals("public") || sub.equals("公共")) { - if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§c用法: /protect public <领地名>"); - return true; - } - String landName = args[1]; - AreaConfig ac = areas.get(landName); - if (ac == null) { sender.sendMessage("§c领地不存在: " + landName); return true; } - // 检查权限 - if (!samePlayer(p.getName(), ac.owner) && !isAreaAdmin(sender)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - ac.isPublicBuilding = !ac.isPublicBuilding; - saveAreaToDb(ac); - if (ac.isPublicBuilding) { - sender.sendMessage("§a§l[公共建筑] §f" + landName + " §a已设为公共建筑设施"); - sender.sendMessage("§7所有访客将自动获得传送、免疫伤害、攻击敌对生物权限"); - } else { - sender.sendMessage("§a§l[公共建筑] §f" + landName + " §7已取消公共建筑标记"); - } - return true; - } - if (sub.equals("listpublic") || sub.equals("公共列表")) { - if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } - Player p = (Player) sender; - java.util.List publicLands = new ArrayList<>(); - for (AreaConfig ac : areas.values()) { - if (ac.isPublicBuilding) publicLands.add(ac); - } - if (publicLands.isEmpty()) { - sender.sendMessage("§7当前没有公共建筑设施"); - return true; - } - sender.sendMessage("§e§l==== 公共建筑设施 ===="); - int idx = 1; - for (AreaConfig ac : publicLands) { - String warpInfo = (ac.warpX != 0 || ac.warpZ != 0) ? "§a[有传送点]" : "§7[无传送点]"; - // 可点击传送的列表项 - p.sendMessage(Component.empty() - .append(Component.text("§e" + idx + ". §f" + ac.name + " §7所有者:" + ac.owner + " " + warpInfo)) - .append(Component.text(" §a[传送到此]")) - .hoverEvent(HoverEvent.showText(Component.text("§e点击传送到 " + ac.name))) - .clickEvent(ClickEvent.runCommand("/protect tpb " + ac.name)) - ); - idx++; - } - return true; - } - if (sub.equals("tpb") || sub.equals("传送公共")) { - if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§c用法: /protect tpb <序号或领地名>"); - return true; - } - java.util.List publicLands = new ArrayList<>(); - for (AreaConfig ac : areas.values()) { - if (ac.isPublicBuilding) publicLands.add(ac); - } - if (publicLands.isEmpty()) { sender.sendMessage("§c没有公共建筑设施"); return true; } - AreaConfig target = null; - // 尝试序号 - try { - int num = Integer.parseInt(args[1]); - if (num >= 1 && num <= publicLands.size()) { - target = publicLands.get(num - 1); - } - } catch (NumberFormatException ignored) {} - // 尝试名字 - if (target == null) { - for (AreaConfig ac : publicLands) { - if (ac.name.equalsIgnoreCase(args[1])) { target = ac; break; } - } - } - if (target == null) { sender.sendMessage("§c未找到公共建筑: " + args[1]); return true; } - // 传送(优先用传送点,否则传送到领地中心) - Location dest; - if (target.warpX != 0 || target.warpZ != 0 || target.warpY != 0) { - World w = Bukkit.getWorld(target.warpWorld != null && !target.warpWorld.isEmpty() ? target.warpWorld : p.getWorld().getName()); - if (w == null) w = p.getWorld(); - dest = new Location(w, target.warpX, target.warpY, target.warpZ, target.warpYaw, target.warpPitch); - } else { - dest = new Location(p.getWorld(), - (target.x1 + target.x2) / 2.0 + 0.5, - p.getWorld().getHighestBlockYAt((target.x1 + target.x2) / 2, (target.z1 + target.z2) / 2) + 1, - (target.z1 + target.z2) / 2.0 + 0.5); - } - p.teleport(dest); - sender.sendMessage("§a已传送至公共建筑: §f" + target.name); - return true; - } - - // ===== 用户组管理 ===== - if (sub.equals("grouplist") || sub.equals("用户组")) { - if (!sender.hasPermission("sdf1.admin")) { - sender.sendMessage("§c需要管理员权限"); return true; - } - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - - // ★ 先从PHP拉取最新用户组(确保PHP端创建的组能显示) - if (plugin.webManager != null) { - plugin.webManager.pullUserGroupsFromPHP(); - } - - showGroupListPanel(sender); - return true; - } - - if (sub.equals("groupset")) { - if (!sender.hasPermission("sdf1.admin")) { - sender.sendMessage("§c需要管理员权限"); return true; - } - if (args.length < 2) { - sender.sendMessage("§e用法: /protect groupset <组名> [显示名] [颜色] [优先级] [每㎡价格] [最大领地数]"); - sender.sendMessage("§7或: /protect groupset <组名> price <值> | maxlands <值> | priority <值>"); - sender.sendMessage("§7或: /protect groupset sync §c(从PHP同步)"); - return true; - } - // ★ groupset sync → 从PHP拉取用户组 - if (args[1].equalsIgnoreCase("sync")) { - sender.sendMessage("§b正在从PHP拉取用户组..."); - if (plugin.webManager != null) { - plugin.webManager.pullUserGroupsFromPHP(); - } - showGroupListPanel(sender); - return true; - } - String groupName = args[1].toLowerCase(); - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(groupName); - - // ★ 支持单字段编辑: /protect groupset <组名> price/maxlands/priority <值> - if (args.length == 4 && cfg != null) { - String field = args[2].toLowerCase(); - String valStr = args[3]; - // 智能数字解析(中文/罗马/英文数字) - Integer parsed = parseSmartNumberBase(valStr); - if (parsed == null) { - sender.sendMessage("§c无法解析数字: " + valStr); - return true; - } - int val = parsed; - switch (field) { - case "price": - case "价格": - cfg.landPricePerSqm = val; - break; - case "maxlands": - case "上限": - case "领地上限": - cfg.maxLands = val; - break; - case "priority": - case "优先级": - cfg.priority = val; - break; - default: - sender.sendMessage("§c未知字段: " + field + "。支持: price/maxlands/priority"); - return true; - } - ugm.updateGroupConfig(cfg); - sender.sendMessage("§a已更新用户组 §f" + cfg.displayName + " §a的 " + field + " = " + val); - // 刷新编辑面板 - if (sender instanceof Player) showGroupEditCLI((Player) sender, cfg); - return true; - } - - if (cfg == null) cfg = new UserGroupManager.UserGroupConfig(); - cfg.name = groupName; - if (args.length >= 3) cfg.displayName = args[2]; - if (args.length >= 4) cfg.displayColor = args[3]; - if (args.length >= 5) { - try { cfg.priority = Integer.parseInt(args[4]); } catch (Exception ignored) {} - } - if (args.length >= 6) { - try { cfg.landPricePerSqm = Integer.parseInt(args[5]); } catch (Exception ignored) {} - } - if (args.length >= 7) { - try { cfg.maxLands = Integer.parseInt(args[6]); } catch (Exception ignored) {} - } - if (cfg.displayName.isEmpty()) cfg.displayName = groupName; - ugm.saveGroupConfigToDB(cfg); - ugm.loadGroupConfigs(); - sender.sendMessage("§a已创建/更新用户组: §f" + cfg.displayColor + cfg.displayName - + " §7(name=" + groupName + ", 价格=" + cfg.landPricePerSqm + "/㎡" - + ", 上限=" + cfg.maxLands + ")"); - // ★ 创建/更新后重新打印面板,让用户看到组并能操作 - showGroupListPanel(sender); - return true; - } - - if (sub.equals("groupdelconfig")) { - if (!sender.hasPermission("sdf1.admin")) { - sender.sendMessage("§c需要管理员权限"); return true; - } - if (args.length < 2) { - sender.sendMessage("§e用法: /protect groupdelconfig <组名>"); - return true; - } - String groupName = args[1].toLowerCase(); - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - if (groupName.equals(UserGroupManager.DEFAULT_GROUP)) { - sender.sendMessage("§c不能删除默认组"); return true; - } - if (ugm.deleteGroupConfig(groupName)) { - sender.sendMessage("§a已删除用户组: " + groupName); - } else { - sender.sendMessage("§c未找到用户组: " + groupName); - } - showGroupListPanel(sender); - return true; - } - - // ★ 用户组交互式编辑(CLI模式) - if (sub.equals("groupedit")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); return true; - } - if (args.length < 2) { - sender.sendMessage("§e用法: /protect groupedit <组名>"); - return true; - } - Player gp = (Player) sender; - String gName = args[1]; - UserGroupManager ugmEdit = plugin.getUserGroup(); - if (ugmEdit == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - UserGroupManager.UserGroupConfig cfgEdit = ugmEdit.getGroupConfig(gName); - if (cfgEdit == null) { - sender.sendMessage("§c未找到用户组: " + gName); - return true; - } - showGroupEditCLI(gp, cfgEdit); - return true; - } - - if (sub.equals("groupadd") || sub.equals("addmember")) { - if (args.length < 3) { - sender.sendMessage("§e用法: /protect groupadd <组名> <玩家名>"); - return true; - } - String inputName = args[1]; - String playerName = args[2]; - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - UserGroupManager.UserGroupConfig cfgAdd = ugm.getGroupConfig(inputName); - if (cfgAdd == null) { - sender.sendMessage("§c未找到用户组: " + inputName); - return true; - } - String groupName = cfgAdd.name; // 使用DB中的实际组名 - String err = ugm.addPlayer(playerName, groupName, sender.getName()); - if (err == null) { - sender.sendMessage("§a已将 §f" + playerName + " §a加入用户组: §f" + cfgAdd.displayName); - // 通知在线玩家 - Player target = plugin.getServer().getPlayerExact(playerName); - if (target != null) { - target.sendMessage("§a§l[用户组] §f你已被加入用户组: §e" + cfgAdd.displayName); - } - } else { - sender.sendMessage("§c" + err); - } - return true; - } - - if (sub.equals("groupdel")) { - if (args.length < 3) { - sender.sendMessage("§e用法: /protect groupdel <组名> <玩家名>"); - return true; - } - String inputName = args[1]; - String playerName = args[2]; - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - UserGroupManager.UserGroupConfig cfgDel = ugm.getGroupConfig(inputName); - String groupName = (cfgDel != null) ? cfgDel.name : inputName; - if (ugm.removePlayer(playerName, groupName)) { - String displayName = (cfgDel != null) ? cfgDel.displayName : groupName; - sender.sendMessage("§a已将 §f" + playerName + " §a移出用户组: " + displayName); - Player target = plugin.getServer().getPlayerExact(playerName); - if (target != null) { - target.sendMessage("§a§l[用户组] §f你已被移出用户组: §e" + displayName); - } - } else { - sender.sendMessage("§c操作失败(玩家可能不在此组)"); - } - return true; - } - - // ===== 用户组成员管理面板 ===== - if (sub.equals("groupmembers")) { - if (args.length < 2) { - sender.sendMessage("§e用法: /protect groupmembers <组名>"); - return true; - } - String inputName = args[1]; - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } - UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(inputName); - if (cfg == null) { sender.sendMessage("§c未找到用户组: " + inputName); return true; } - String groupName = cfg.name; // 使用DB中的实际组名(保留大小写) - - java.util.List> members = ugm.getGroupMembers(groupName); - sender.sendMessage("§a§l========== " + cfg.displayColor + cfg.displayName + " §a成员管理 =========="); - sender.sendMessage("§7共 §f" + members.size() + " §7名成员"); - - if (members.isEmpty()) { - sender.sendMessage("§7暂无成员"); - } else { - for (Map m : members) { - String pn = (String) m.get("player_name"); - String addedBy = (String) m.get("added_by"); - if (sender instanceof Player) { - Player pp = (Player) sender; - pp.sendMessage(Component.empty() - .append(Component.text(" §f" + pn + " §7(添加者: " + (addedBy != null ? addedBy : "system") + ") ")) - .append(Component.text("§c[移除]")) - .hoverEvent(HoverEvent.showText(Component.text("§c点击移除此成员"))) - .clickEvent(ClickEvent.runCommand("/protect groupdel " + groupName + " " + pn)) - ); - } else { - sender.sendMessage(" §f" + pn + " §7(添加者: " + (addedBy != null ? addedBy : "system") + ")"); - } - } - } - - // 操作入口 - sender.sendMessage(""); - if (sender instanceof Player) { - Player pp = (Player) sender; - // 添加在线玩家 - pp.sendMessage(Component.empty() - .append(Component.text("§a[添加在线玩家] ")) - .hoverEvent(HoverEvent.showText(Component.text("§e输入玩家名添加到此组"))) - .clickEvent(ClickEvent.suggestCommand("/protect groupadd " + groupName + " ")) - ); - // 返回用户组列表(上一层) - pp.sendMessage(Component.empty() - .append(Component.text("§c[返回用户组列表] ")) - .hoverEvent(HoverEvent.showText(Component.text("§c返回用户组管理列表"))) - .clickEvent(ClickEvent.runCommand("/protect grouplist")) - ); - // 返回领地系统首页(CLI模式) - pp.sendMessage(Component.empty() - .append(Component.text("§7[返回领地首页] ")) - .hoverEvent(HoverEvent.showText(Component.text("§7返回领地系统主菜单"))) - .clickEvent(ClickEvent.runCommand("/protect")) - ); - } - sender.sendMessage("§7§l─────────────────────────────────"); - return true; - } - - // ===== 物品黑名单 ===== - if (sub.equals("additem")) { - if (args.length == 2) { - // ★ 全服级物品黑名单,必须管理员 - if (!requireGlobalAdmin(sender, "全局物品黑名单操作")) return true; - String itemName = args[1].toUpperCase(); - if (Material.matchMaterial(itemName) - == null) { - sender.sendMessage( - "§c无效物品ID: " + args[1]); - return true; - } - globalItemBlacklist.add(itemName); - sender.sendMessage("§a已全局加黑: " - + itemName); - return true; - } - if (args.length == 3) { - String r1 = resolveAreaNameStrict(args[1]); - String r2 = resolveAreaNameStrict(args[2]); - String areaName; - String itemName; - if (r1 != null) { - areaName = r1; - itemName = args[2].toUpperCase(); - } else if (r2 != null) { - areaName = r2; - itemName = args[1].toUpperCase(); - } else { - areaName = "global"; - itemName = args[1].toUpperCase(); - } - // ★ 权限闸门:领地黑名单须领地主/领地管理员,global 须插件管理员 - if (!requireLandOwnership(sender, areaName)) return true; - if (Material.matchMaterial(itemName) - == null) { - sender.sendMessage( - "§c无效物品ID: " + itemName); - return true; - } - if (areaName.equalsIgnoreCase("global")) { - globalItemBlacklist.add(itemName); - sender.sendMessage("§a已全局加黑: " - + itemName); - } else { - areaItemBlacklist - .computeIfAbsent(areaName, - k -> ConcurrentHashMap - .newKeySet()) - .add(itemName); - sender.sendMessage("§a已加黑: " - + itemName + " → " + areaName); - } - return true; - } - sender.sendMessage("§c用法: /protect additem " - + "<区域/物品> <区域/物品>"); - return true; - } - if (sub.equals("removeitem")) { - if (args.length == 2) { - // ★ 全服级物品黑名单,必须管理员 - if (!requireGlobalAdmin(sender, "全局物品黑名单操作")) return true; - String itemName = args[1].toUpperCase(); - if (!globalItemBlacklist - .contains(itemName)) { - sender.sendMessage( - "§c全局黑名单中没有: " - + itemName); - return true; - } - globalItemBlacklist.remove(itemName); - sender.sendMessage("§a已移除: " + itemName); - return true; - } - if (args.length == 3) { - String r1 = resolveAreaNameStrict(args[1]); - String r2 = resolveAreaNameStrict(args[2]); - String areaName; - String itemName; - if (r1 != null) { - areaName = r1; - itemName = args[2].toUpperCase(); - } else if (r2 != null) { - areaName = r2; - itemName = args[1].toUpperCase(); - } else { - areaName = "global"; - itemName = args[1].toUpperCase(); - } - // ★ 权限闸门:领地黑名单须领地主/领地管理员,global 须插件管理员 - if (!requireLandOwnership(sender, areaName)) return true; - if (areaName.equalsIgnoreCase("global")) { - globalItemBlacklist.remove(itemName); - } else { - Set list = - areaItemBlacklist.get(areaName); - if (list != null) { - list.remove(itemName); - } - } - sender.sendMessage("§a已移除: " - + itemName); - return true; - } - sender.sendMessage("§c用法: /protect removeitem " - + "<区域/物品> <区域/物品>"); - return true; - } - if (sub.equals("listitem")) { - if (args.length < 2) { - sender.sendMessage( - "§a全局物品黑名单:"); - if (globalItemBlacklist.isEmpty()) { - sender.sendMessage("§7(空)"); - } else { - for (String item - : globalItemBlacklist) { - sender.sendMessage("§f- " + item); - } - } - return true; - } - Set list = - areaItemBlacklist.get(args[1]); - sender.sendMessage("§a" + args[1] - + " 物品黑名单:"); - if (list == null || list.isEmpty()) { - sender.sendMessage("§7(空)"); - } else { - for (String item : list) { - sender.sendMessage("§f- " + item); - } - } - return true; - } - - // ===== accept / deny 面积审批(管理员现场审批) ===== - if (sub.equals("accept") || sub.equals("deny")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - handleApproval((Player) sender, args, sub.equals("accept")); - return true; - } - - // ===== expand 扩建 ===== - if (sub.equals("expand")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - p.sendMessage("§c你不在任何防护区域内"); - return true; - } - // ★ 权限闸门:改边界必须是领地主/领地管理员 - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - p.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - int amount = 5; - if (args.length >= 2) { - try { - amount = Integer.parseInt(args[1]); - } catch (NumberFormatException ignored) {} - } - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - float yaw = p.getLocation().getYaw(); - String dir = yawToDir(yaw); - - // ★ 面积限制(2026-09-29):先按原边界算基数,再算提案边界 - if (amount <= 0) { - p.sendMessage("§c§l[防护] §f扩建格数必须为正整数"); - return true; - } - if (amount > 100000) { - p.sendMessage("§c§l[防护] §f单次扩建格数上限 100000,请分次扩建"); - return true; - } - long oldW = (long) maxX - minX + 1; - long oldL = (long) maxZ - minZ + 1; - long oldAreaSq = oldW * oldL; - - if (dir.equals("北")) minZ -= amount; - else if (dir.equals("南")) maxZ += amount; - else if (dir.equals("东")) maxX += amount; - else if (dir.equals("西")) minX -= amount; - - long newW = (long) maxX - minX + 1; - long newL = (long) maxZ - minZ + 1; - long newAreaSq = newW * newL; - long deltaSq = newAreaSq - oldAreaSq; - boolean overFree = deltaSq > globalMaxFreeAreaSqm - || newW > globalMaxLandSideBlocks - || newL > globalMaxLandSideBlocks; - if (overFree && !isAreaAdmin(p)) { - // 超限 → 转管理员现场审批(插件管理员不受限) - registerPendingExpand(p, ac, minX, maxX, minZ, maxZ, - amount, dir, deltaSq, newW, newL, oldW, oldL); - return true; - } - - // ★ 扩建扣费(2026-10-03):与创建同单价,余额不足则拒绝扩建 - int expPrice = landPricePerSqm(p); - long expCostL = deltaSq * (long) expPrice; - if (expCostL > Integer.MAX_VALUE) { - p.sendMessage("§c§l[防护] §f本次扩建费用超出上限,请分次扩建"); - return true; - } - int expCost = (int) expCostL; - if (expCost > 0) { - BondManager ebm = plugin.getBonds(); - if (ebm == null || !ebm.deductBonds(p.getName(), expCost, "land_expand", - p.getName(), p.getName(), - "扩建领地: " + ac.name + " (+" + deltaSq + "㎡×" + expPrice + ")")) { - p.sendMessage("§c§l[防护] §f余额不足,扩建需 §e" + expCost - + " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡),请先充值再扩建"); - return true; - } - } - ac.x1 = minX; - ac.x2 = maxX; - ac.z1 = minZ; - ac.z2 = maxZ; - ac.createCost += expCost; // ★ 计入删除退费基数(后续删除按比例退) - saveAreaToDb(ac); - p.sendMessage("§a§l[防护] §f区域 §e" + ac.name - + " §f向 §e" + dir + " §f扩建 §e" - + amount + "§f格" - + (expCost > 0 - ? (",扣除 §e" + expCost + " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡)") - : "")); - return true; - } - - // ===== contract 收缩 ===== - if (sub.equals("contraction") - || sub.equals("contract")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - p.sendMessage("§c你不在任何防护区域内"); - return true; - } - // ★ 权限闸门:改边界必须是领地主/领地管理员 - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - p.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - int amount = 5; - if (args.length >= 2) { - try { - amount = Integer.parseInt(args[1]); - } catch (NumberFormatException ignored) {} - } - int minX = Math.min(ac.x1, ac.x2); - int maxX = Math.max(ac.x1, ac.x2); - int minZ = Math.min(ac.z1, ac.z2); - int maxZ = Math.max(ac.z1, ac.z2); - float yaw = p.getLocation().getYaw(); - String dir = yawToDir(yaw); - // ★ 收缩前置校验(2026-10-03):负数会变成"负数扩建"白嫖,一并挡掉 - if (amount <= 0) { - p.sendMessage("§c§l[防护] §f收缩格数必须为正整数"); - return true; - } - if (amount > 100000) { - p.sendMessage("§c§l[防护] §f单次收缩格数上限 100000,请分次收缩"); - return true; - } - long oldSqW = (long) maxX - minX + 1; - long oldSqL = (long) maxZ - minZ + 1; - long oldSq = oldSqW * oldSqL; - if (dir.equals("北")) minZ += amount; - else if (dir.equals("南")) maxZ -= amount; - else if (dir.equals("东")) maxX -= amount; - else if (dir.equals("西")) minX += amount; - if (minX > maxX || minZ > maxZ) { - p.sendMessage("§c收缩过度"); - return true; - } - long newSqW = (long) maxX - minX + 1; - long newSqL = (long) maxZ - minZ + 1; - long shrinkSq = oldSq - newSqW * newSqL; - - // ★ 收缩退款(2026-10-03):单价 × 收缩面积 × 删除退款比例 - // 基数不超过已付 createCost(历史免费地不凭空造钱),退款给领地主 - int shrinkPrice = landPricePerSqm(p); - int shrinkRatio = globalRefundRatioPerCent; - if (shrinkRatio > 100) shrinkRatio = 100; - long baseL = shrinkSq * (long) shrinkPrice; - if (baseL > ac.createCost) baseL = ac.createCost; - if (baseL < 0) baseL = 0; - int refundBase = (int) baseL; - int shrinkRefund = 0; - if (shrinkRatio > 0 && refundBase > 0) { - shrinkRefund = (int) Math.floor(refundBase * (shrinkRatio / 100.0)); - } - if (shrinkRefund > 0) { - BondManager sbm = plugin.getBonds(); - if (sbm == null) { - shrinkRefund = 0; - } else { - int afterBal = sbm.addBonds(ac.owner, shrinkRefund, "land_refund", - ac.owner, p.getName(), - "收缩领地退费: " + ac.name + " (-" + shrinkSq + "㎡)"); - if (afterBal < 0) { - plugin.getLogger().warning("[防护] 领地收缩退费失败(账户冻结?): " - + ac.owner + " +" + shrinkRefund); - shrinkRefund = 0; - } - } - } - ac.createCost -= refundBase; // ★ 收缩掉的部分不再计入删除退费基数 - if (ac.createCost < 0) ac.createCost = 0; - ac.x1 = minX; - ac.x2 = maxX; - ac.z1 = minZ; - ac.z2 = maxZ; - saveAreaToDb(ac); - p.sendMessage("§a§l[防护] §f区域 §e" + ac.name - + " §f向 §e" + dir + " §f收缩 §e" - + amount + "§f格" - + (shrinkRefund > 0 - ? (",退还 §e" + shrinkRefund + " §f债券(" - + shrinkSq + "㎡×" + shrinkPrice + "/㎡×" + shrinkRatio + "%)") - : (shrinkRatio <= 0 ? "§7(退款比例 0,不退费)" : ""))); - return true; - } - - // ===== 删除(带60秒延迟自动删除 + 取消)===== - if (sub.equals("删除") || sub.equals("delete")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§e用法: /protect 删除 <名> [-f]"); - sender.sendMessage("§7-f = 管理员强制删除:跳过60秒倒计时与退费流程,立即删除"); - return true; - } - String areaName = args[1]; - // 严格匹配(写操作不做模糊归一化) - String resolved = resolveAreaNameStrict(areaName); - if (resolved != null) areaName = resolved; - if (!areas.containsKey(areaName)) { - sender.sendMessage("§c领地不存在: " + areaName); - return true; - } - // ★ 领主或管理员均可删除 - AreaProtection.AreaConfig delAc = areas.get(areaName); - boolean isOwner = delAc != null && samePlayer(p.getName(), delAc.owner); - if (!isAreaAdmin(sender) && !isOwner) { - sender.sendMessage("§c需要管理员权限或领地所有权"); - return true; - } - // ★ 管理员强制删除 -f:跳过60秒倒计时与退费流程,立即删除 - boolean forceDelete = false; - for (String argTok : args) { - if (argTok.equalsIgnoreCase("-f") || argTok.equalsIgnoreCase("--force")) { - forceDelete = true; - break; - } - } - // ★ 非管理员携带 -f:不报错、不提示,静默回退到普通60秒删除流程 - if (forceDelete && !isAreaAdmin(sender)) { - forceDelete = false; - } - if (forceDelete) { - if (delAc == null) { - sender.sendMessage("§c领地数据异常: " + areaName); - return true; - } - cancelPendingDelete(areaName); - deleteAreaFromDb(areaName); - areas.remove(areaName); - plugin.getLogger().warning("[防护] 管理员 " + sender.getName() + " 强制删除领地: " + areaName - + " (owner=" + delAc.owner + ", 未退费, 坐标 " + delAc.world + " " - + delAc.x1 + "," + delAc.z1 + " ~ " + delAc.x2 + "," + delAc.z2 + ")"); - sender.sendMessage("§a§l[防护] §f已强制删除领地 §e" + areaName + " §7(跳过倒计时与退费)"); - reportLandRemovalToAdmins("§6§l[防护-强制删除] §f" + sender.getName() - + " 强制删除领地 §e" + areaName + " §7(owner=" + delAc.owner - + ", 不退款) [" + delAc.world + " " + delAc.x1 + "," + delAc.z1 - + " ~ " + delAc.x2 + "," + delAc.z2 + "]"); - return true; - } - // ★ 检查是否已有待删除请求 - PendingDelete existing = pendingDeletes.get(areaName); - if (existing != null) { - // 已有等待期,提示取消方式 - long elapsed = System.currentTimeMillis() - existing.startTime; - long remaining = Math.max(0, (60000 - elapsed) / 1000); - sender.sendMessage("§e领地 §c" + areaName + " §e正在等待删除中,还剩 §c" + remaining + " §e秒后自动删除"); - sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消"); - return true; - } - // ★ 新建等待期:60秒后自动删除 - sender.sendMessage("§e⚠ 此操作不可逆!领地 §c" + areaName + " §e将在 §c60秒 §e后自动删除"); - sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消"); - // 也显示到领地列表(CLI/GUI取消按钮会自动刷新) - UUID uid = p.getUniqueId(); - String finalAreaName = areaName; - final String finalOperator = p.getName(); - long start = System.currentTimeMillis(); - BukkitTask task = new BukkitRunnable() { - @Override - public void run() { - pendingDeletes.remove(finalAreaName); - // ★ 60秒到期后自动删除领地 - if (areas.containsKey(finalAreaName)) { - // ★ 到期删除:按创建扣款比例退费(0%=关闭退费) - refundOnLandDelete(areas.get(finalAreaName), finalOperator, "删除领地退费: " + finalAreaName); - deleteAreaFromDb(finalAreaName); - areas.remove(finalAreaName); - plugin.getLogger().info("[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除"); - // 通知发起者 - Player online = Bukkit.getPlayer(uid); - if (online != null && online.isOnline()) { - online.sendMessage("§a§l[防护] §f领地 §e" + finalAreaName + " §f等待期结束,已自动删除"); - } - } - } - }.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick - pendingDeletes.put(areaName, new PendingDelete(areaName, p.getName(), start, task)); - return true; - } - - // ===== 取消删除 ===== - if (sub.equals("取消删除") || sub.equals("canceldelete")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§e用法: /protect 取消删除 <领地名>"); - return true; - } - String areaName = args[1]; - String resolved = resolveAreaNameStrict(areaName); - if (resolved != null) areaName = resolved; - // ★ 权限闸门:只有领地主/领地管理员能取消删除 - if (!requireLandOwnership(sender, areaName)) return true; - PendingDelete pd = pendingDeletes.remove(areaName); - if (pd == null) { - sender.sendMessage("§c没有待删除的领地: " + areaName); - return true; - } - pd.task.cancel(); - sender.sendMessage("§a§l[防护] §f已取消删除领地: §e" + areaName); - return true; - } - - // ===== 取消管理员改主 ===== - if (sub.equals("canceladminchange")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§e用法: /protect canceladminchange <领地名>"); - return true; - } - String areaName = args[1]; - String resolved = resolveAreaName(areaName); - if (resolved != null) areaName = resolved; - - // 检查领地是否存在 - AreaConfig ac = getLand(areaName); - if (ac == null) { - sender.sendMessage("§c领地不存在: " + areaName); - return true; - } - - // 检查是否是原主人 - if (!samePlayer(p.getName(), ac.owner)) { - sender.sendMessage("§c只有原主人才能撤回改主"); - return true; - } - - // 查找最近的管理员改主记录 - try { - if (dbConnection == null) { - sender.sendMessage("§c数据库未连接"); - return true; - } - PreparedStatement ps = dbConnection.prepareStatement( - "SELECT id, change_data FROM web_admin_changes WHERE change_type = 'owner_change' AND target_name = ? AND status = 'completed' ORDER BY id DESC LIMIT 1"); - ps.setString(1, areaName); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - int changeId = rs.getInt("id"); - String changeDataStr = rs.getString("change_data"); - java.util.Map changeData = WebManager.parseJson(changeDataStr); - String newOwner = (String) changeData.get("new_owner"); - String oldOwner = (String) changeData.get("old_owner"); - - // 验证当前owner是否是newOwner(即已经被改主了) - if (!samePlayer(p.getName(), newOwner)) { - sender.sendMessage("§c当前所有者不是你,无法撤回"); - rs.close(); - ps.close(); - return true; - } - - // 回退owner - PreparedStatement updatePs = dbConnection.prepareStatement( - "UPDATE area_lands SET owner = ? WHERE name = ?"); - updatePs.setString(1, oldOwner); - updatePs.setString(2, areaName); - updatePs.executeUpdate(); - updatePs.close(); - - // 更新内存 - ac.owner = oldOwner; - - // 通知PHP回滚 - plugin.webManager.callbackOwnerChangeToPHP(changeId, false, "原主人撤回"); - - sender.sendMessage("§a§l[防护] §f已撤回领地 §e" + areaName + " §f的改主,恢复为 §a" + oldOwner); - plugin.getLogger().info("[防护] " + p.getName() + " 撤回了领地 " + areaName + " 的管理员改主"); - } else { - sender.sendMessage("§c没有可撤回的改主记录"); - } - rs.close(); - ps.close(); - } catch (Exception e) { - sender.sendMessage("§c撤回失败: " + e.getMessage()); - plugin.getLogger().warning("[防护] 撤回管理员改主失败: " + e.getMessage()); - } - return true; - } - - // ===== add 加白名单 ===== - if (sub.equals("add")) { - if (args.length == 2) { - // ★ 全服级白名单(等于全服免检),必须管理员 - if (!requireGlobalAdmin(sender, "全局白名单操作")) return true; - String name = args[1]; // ★ 保留原始拼写(2026-10-10 精确匹配) - if (globalPlayerWhitelist.contains(name)) { - sender.sendMessage("§e" + name - + " 已在全局白名单中"); - return true; - } - globalPlayerWhitelist.add(name); - saveWhitelists(); - sender.sendMessage("§a已全局加白: " + name); - return true; - } - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别"); - return true; - } - String areaName = parsed[0]; - String playerName = parsed[1]; // ★ 保留原始拼写(2026-10-10 精确匹配) - // ★ 权限闸门:领地白名单须领地主/领地管理员,global 须插件管理员 - if (!requireLandOwnership(sender, areaName)) return true; - if (areaName.equalsIgnoreCase("global")) { - globalPlayerWhitelist.add(playerName); - saveWhitelists(); - sender.sendMessage("§a已全局加白: " - + playerName); - } else { - Set wl = areaPlayerWhitelist - .computeIfAbsent(areaName, - k -> ConcurrentHashMap - .newKeySet()); - wl.add(playerName); - saveWhitelists(); - sender.sendMessage("§a已加白: " - + playerName + " → " + areaName); - } - return true; - } - sender.sendMessage("§c用法: /protect add " - + "<区域/玩家> <区域/玩家>"); - return true; - } - - // ===== remove 移除白名单 ===== - if (sub.equals("remove")) { - if (args.length == 2) { - // ★ 全服级白名单,必须管理员 - if (!requireGlobalAdmin(sender, "全局白名单操作")) return true; - String name = args[1]; // ★ 保留原始拼写;下方按三种历史格式尝试移除 - if (!globalPlayerWhitelist.contains(name) - && !globalPlayerWhitelist.contains(name.toLowerCase()) - && !globalPlayerWhitelist.contains(normName(name))) { - sender.sendMessage("§e" + name - + " 不在全局白名单中"); - return true; - } - globalPlayerWhitelist.remove(name); - globalPlayerWhitelist.remove(name.toLowerCase()); - globalPlayerWhitelist.remove(normName(name)); - saveWhitelists(); - sender.sendMessage("§a已从全局白名单移除: " - + name); - return true; - } - if (args.length == 3) { - String[] parsed = parseAreaAndTarget( - args[1], args[2]); - if (parsed == null) { - sender.sendMessage("§c无法识别"); - return true; - } - String areaName = parsed[0]; - String playerName = parsed[1]; // ★ 保留原始拼写;下方按三种历史格式尝试移除 - // ★ 权限闸门:领地白名单须领地主/领地管理员,global 须插件管理员 - if (!requireLandOwnership(sender, areaName)) return true; - if (areaName.equalsIgnoreCase("global")) { - globalPlayerWhitelist.remove(playerName); - globalPlayerWhitelist.remove(playerName.toLowerCase()); - globalPlayerWhitelist.remove(normName(playerName)); - saveWhitelists(); - sender.sendMessage("§a已移除: " - + playerName); - } else { - Set wl = - areaPlayerWhitelist.get(areaName); - if (wl != null) { - // 精确 / 旧版小写 / normName 三种历史格式都尝试移除 - wl.remove(playerName); - wl.remove(playerName.toLowerCase()); - wl.remove(normName(playerName)); - } - saveWhitelists(); - sender.sendMessage("§a已从 " + areaName - + " 移除: " + playerName); - } - return true; - } - sender.sendMessage("§c用法: /protect remove " - + "<区域/玩家> <区域/玩家>"); - return true; - } - - // ===== on/off/tempon 边框 ===== - if (sub.equals("on")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - p.sendMessage("§c你不在任何防护区域内"); - return true; - } - showBorder(p, ac, false); - p.sendMessage("§a§l[防护] §f已显示 " - + ac.name + " 边框"); - return true; - } - if (sub.equals("off")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - cancelBorder((Player) sender); - ((Player) sender).sendMessage( - "§a§l[防护] §f已关闭边框显示"); - return true; - } - if (sub.equals("tempon")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - p.sendMessage("§c你不在任何防护区域内"); - return true; - } - showBorder(p, ac, true); - p.sendMessage("§a§l[防护] §f临时显示 " - + ac.name + " 边框(15秒)"); - return true; - } - - // ===== shop 权限商店 ===== - if (sub.equals("shop")) { - String shopAction = args.length >= 2 ? args[1] : ""; - // shop create <领地> <价格> [时长秒] — 所有者创建权限售卖 - if (shopAction.equals("create")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 4) { - sender.sendMessage("§e用法: /protect shop create <领地> <价格> [时长秒]"); - sender.sendMessage("§7时长默认86400秒(24h),最大86400秒"); - return true; - } - String areaName = resolveAreaName(args[2]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[2]); - return true; - } - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - int price; - try { price = Integer.parseInt(args[3]); } - catch (NumberFormatException e) { - sender.sendMessage("§c价格必须是整数"); - return true; - } - if (price < 1000) { - sender.sendMessage("§c最低价格为1000债券"); - return true; - } - int duration = 86400; // 默认24h - if (args.length >= 5) { - try { duration = Integer.parseInt(args[4]); } - catch (NumberFormatException e) { - sender.sendMessage("§c时长必须是整数(秒)"); - return true; - } - } - if (duration > 86400) { - sender.sendMessage("§c最大时长为86400秒(24小时)"); - return true; - } - // 获取land_id - int landId = getLandIdFromDb(areaName); - if (landId <= 0) { - sender.sendMessage("§c领地数据库记录不存在"); - return true; - } - // 插入商店记录 - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "INSERT INTO area_permission_shop (land_id, seller, permission, price, duration, created_at) " - + "VALUES (?, ?, 'visitor', ?, ?, ?)"); - stmt.setInt(1, landId); - stmt.setString(2, p.getName()); - stmt.setInt(3, price); - stmt.setInt(4, duration); - stmt.setLong(5, System.currentTimeMillis()); - stmt.executeUpdate(); - stmt.close(); - sender.sendMessage("§a§l[权限商店] §f已上架访客权限: §e" + areaName + " §f价格: §e" + price + "债券 §f时长: §e" + formatDuration(duration)); - } catch (SQLException e) { - sender.sendMessage("§c数据库错误: " + e.getMessage()); - } - return true; - } - - // shop remove — 下架 - if (shopAction.equals("remove")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 3) { - sender.sendMessage("§e用法: /protect shop remove <商品ID>"); - return true; - } - int shopId; - try { shopId = Integer.parseInt(args[2]); } - catch (NumberFormatException e) { - sender.sendMessage("§cID必须是整数"); - return true; - } - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "DELETE FROM area_permission_shop WHERE id = ? AND seller = ? AND status = 'active'"); - stmt.setInt(1, shopId); - stmt.setString(2, p.getName()); - int rows = stmt.executeUpdate(); - stmt.close(); - if (rows > 0) { - sender.sendMessage("§a§l[权限商店] §f已下架商品 #" + shopId); - } else { - sender.sendMessage("§c商品不存在或你不是卖家"); - } - } catch (SQLException e) { - sender.sendMessage("§c数据库错误: " + e.getMessage()); - } - return true; - } - - // shop list — 查看可购买的权限 - if (shopAction.equals("list") || shopAction.isEmpty()) { - try { - Statement stmt = dbConnection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT s.id, l.name AS land_name, s.seller, s.price, s.duration " - + "FROM area_permission_shop s " - + "JOIN area_lands l ON s.land_id = l.id " - + "WHERE s.status = 'active' AND s.buyer = '' " - + "ORDER BY s.created_at DESC LIMIT 50"); - sender.sendMessage("§e§l==== 权限商店 ===="); - boolean found = false; - while (rs.next()) { - found = true; - int id = rs.getInt("id"); - String landName = rs.getString("land_name"); - String seller = rs.getString("seller"); - int price = rs.getInt("price"); - int dur = rs.getInt("duration"); - sender.sendMessage("§a#" + id + " §f" + landName + " §7卖家:" + seller + " §e" + price + "债券 §7" + formatDuration(dur)); - } - if (!found) { - sender.sendMessage("§7(暂无在售权限)"); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - sender.sendMessage("§c数据库错误: " + e.getMessage()); - } - return true; - } - - // shop buy — 购买权限 - if (shopAction.equals("buy")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 3) { - sender.sendMessage("§e用法: /protect shop buy <商品ID>"); - return true; - } - int shopId; - try { shopId = Integer.parseInt(args[2]); } - catch (NumberFormatException e) { - sender.sendMessage("§cID必须是整数"); - return true; - } - // 查询商品 - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT s.*, l.name AS land_name FROM area_permission_shop s " - + "JOIN area_lands l ON s.land_id = l.id " - + "WHERE s.id = ? AND s.status = 'active' AND s.buyer = ''"); - stmt.setInt(1, shopId); - ResultSet rs = stmt.executeQuery(); - if (!rs.next()) { - sender.sendMessage("§c商品不存在或已售出"); - stmt.close(); - return true; - } - String seller = rs.getString("seller"); - int price = rs.getInt("price"); - int duration = rs.getInt("duration"); - int landId = rs.getInt("land_id"); - String landName = rs.getString("land_name"); - rs.close(); - stmt.close(); - - // 不能买自己的(精确匹配:离线服里大小写不同的名字是不同玩家) - if (samePlayer(seller, p.getName())) { - sender.sendMessage("§c不能购买自己上架的权限"); - return true; - } - - // 扣款 - BondManager bm = plugin.getBonds(); - if (bm == null) { - sender.sendMessage("§c债券系统未初始化"); - return true; - } - if (bm.getBonds(p.getName()) < price) { - sender.sendMessage("§c债券不足,需要 §e" + price + " §c债券"); - return true; - } - if (!bm.deductBonds(p.getName(), price, "permission_shop_buy", "", "权限商店", "购买权限: " + landName)) { - sender.sendMessage("§c扣款失败"); - return true; - } - - // 分账:80%给卖家,20%税 - int ownerShare = price * 80 / 100; - int tax = price - ownerShare; - bm.addBonds(seller, ownerShare, "permission_shop_sell", p.getName(), "权限商店", "出售权限: " + landName); - - // 标记已售出 - PreparedStatement updateStmt = dbConnection.prepareStatement( - "UPDATE area_permission_shop SET status = 'sold', buyer = ?, bought_at = ? WHERE id = ?"); - updateStmt.setString(1, p.getName()); - updateStmt.setLong(2, System.currentTimeMillis()); - updateStmt.setInt(3, shopId); - updateStmt.executeUpdate(); - updateStmt.close(); - - // 写入访客权限(带过期时间) - long expiresAt = System.currentTimeMillis() + (long) duration * 1000; - PermRow existingPerm = findPermRow(landId, p.getName()); - if (existingPerm != null) { - // 已有行(含大小写/变种变体)→ 原地更新,杜绝 BINARY 唯一约束产生的重复行 - PreparedStatement permStmt = dbConnection.prepareStatement( - "UPDATE area_land_permissions SET role = 'visitor', permissions = 'shop_purchase', " - + "granted_at = ?, expires_at = ? WHERE id = ?"); - permStmt.setLong(1, System.currentTimeMillis()); - permStmt.setLong(2, expiresAt); - permStmt.setInt(3, existingPerm.id); - permStmt.executeUpdate(); - permStmt.close(); - } else { - PreparedStatement permStmt = dbConnection.prepareStatement( - "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at, expires_at) " - + "VALUES (?, ?, 'visitor', 'shop_purchase', ?, ?)"); - permStmt.setInt(1, landId); - permStmt.setString(2, p.getName()); - permStmt.setLong(3, System.currentTimeMillis()); - permStmt.setLong(4, expiresAt); - permStmt.executeUpdate(); - permStmt.close(); - } - - sender.sendMessage("§a§l[权限商店] §f购买成功!"); - sender.sendMessage("§f领地: §e" + landName + " §f时长: §e" + formatDuration(duration)); - sender.sendMessage("§f花费: §e" + price + " §f债券 (卖家得 §e" + ownerShare + " §f债券)"); - - // 通知卖家 - Player sellerPlayer = Bukkit.getPlayerExact(seller); - if (sellerPlayer != null) { - sellerPlayer.sendMessage("§a§l[权限商店] §f你的权限商品被 §e" + p.getName() + " §f购买,到账 §e" + ownerShare + " §f债券"); - } - } catch (SQLException e) { - sender.sendMessage("§c数据库错误: " + e.getMessage()); - } - return true; - } - - // shop my — 查看我购买的权限 - if (shopAction.equals("my")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - try { - PreparedStatement stmt = dbConnection.prepareStatement( - "SELECT p2.*, l.name AS land_name FROM area_land_permissions p2 " - + "JOIN area_lands l ON p2.land_id = l.id " - + "WHERE p2.player_name = ? AND p2.role = 'visitor' " - + "AND (p2.expires_at = 0 OR p2.expires_at > ?) " - + "ORDER BY p2.expires_at DESC"); - stmt.setString(1, p.getName()); - stmt.setLong(2, System.currentTimeMillis()); - ResultSet rs = stmt.executeQuery(); - sender.sendMessage("§e§l==== 我的领地权限 ===="); - boolean found = false; - long now = System.currentTimeMillis(); - while (rs.next()) { - found = true; - String landName = rs.getString("land_name"); - long expiresAt = rs.getLong("expires_at"); - if (expiresAt > 0) { - long remain = expiresAt - now; - sender.sendMessage("§a" + landName + " §7剩余: §e" + formatDuration((int)(remain / 1000))); - } else { - sender.sendMessage("§a" + landName + " §7永久"); - } - } - if (!found) { - sender.sendMessage("§7(暂无领地权限)"); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - sender.sendMessage("§c数据库错误: " + e.getMessage()); - } - return true; - } - - sender.sendMessage("§e用法: /protect shop "); - return true; - } - - // ===== info 显示当前领地权限信息 ===== - if (sub.equals("info")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - p.sendMessage("§c你不在任何防护区域内"); - return true; - } - PermissionLevel level = getPermissionLevel(p, ac); - String levelName = level != null ? level.name() : "无权限"; - p.sendMessage("§e§l==== 领地信息 ===="); - p.sendMessage("§f名称: §e" + ac.name); - p.sendMessage("§f所有者: §e" + (ac.owner != null && !ac.owner.isEmpty() ? ac.owner : "无")); - p.sendMessage("§f你的权限: §a" + levelName); - if (level == PermissionLevel.OWNER) { - p.sendMessage("§7(你是此领地的所有者)"); - } else if (level == PermissionLevel.ADMIN) { - p.sendMessage("§7(你是服务器管理员)"); - } else if (level == PermissionLevel.MEMBER) { - p.sendMessage("§7(你是此领地的已授权成员)"); - } else if (level == PermissionLevel.VISITOR) { - p.sendMessage("§7(你是此领地的访客)"); - } - // ★ info时展示领地边框粒子(5秒) - showBorderParticles(p, ac, 100); - return true; - } - - // ===== setowner 设置领地所有者(管理员命令)===== - if (sub.equals("setowner") || sub.equals("setowner")) { - if (!isAreaAdmin(sender)) { - sender.sendMessage("§c需要管理员权限"); - return true; - } - // If args.length < 3, try to auto-assign: use current location's land and set admin as owner - if (args.length < 3) { - if (!(sender instanceof Player)) { - sender.sendMessage("§e用法: /protect setowner <领地> <玩家>"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = getArea(p.getWorld().getName(), p.getLocation().getBlockX(), p.getLocation().getBlockY(), (int) p.getLocation().getZ()); - if (ac == null) { - sender.sendMessage("§c你不在任何领地内"); - return true; - } - if (ac.owner != null && !ac.owner.isEmpty()) { - sender.sendMessage("§c该领地已有所有者: " + ac.owner + "。用法: /protect setowner <领地> <新玩家>"); - return true; - } - ac.owner = p.getName(); - saveAreaToDb(ac); - cancelPendingDelete(ac.name); - sender.sendMessage("§a已自动将 §e" + ac.name + " §a的所有者设为 §e" + p.getName()); - return true; - } - String areaName = resolveAreaName(args[1]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[1]); - return true; - } - AreaConfig ac = areas.get(areaName); - ac.owner = args[2]; - saveAreaToDb(ac); - // ★ 删除冷却期间变更所有者 → 自动取消删除 - cancelPendingDelete(areaName); - sender.sendMessage("§a已设置 §e" + areaName + " §a的所有者为 §e" + args[2]); - return true; - } - - // ===== setadmin 设置领地管理员(限指定领地,DB role='admin',不再授予全局标签)===== - if (sub.equals("setadmin")) { - if (!isAreaAdmin(sender)) { - sender.sendMessage("§c需要管理员权限"); - return true; - } - if (args.length < 3) { - sender.sendMessage("§e用法: /protect setadmin <领地名> <玩家>"); - return true; - } - String landName = args[1]; - String targetName = args[2]; - AreaConfig ac = getLand(landName); - if (ac == null) { - sender.sendMessage("§c领地不存在: " + landName); - return true; - } - Player target = Bukkit.getPlayerExact(targetName); - if (target == null) { - sender.sendMessage("§c玩家不在线: " + targetName); - return true; - } - if (isLandAdmin(landName, targetName)) { - sender.sendMessage("§e" + targetName + " §c已经是该领地管理员"); - return true; - } - setLandAdmin(landName, targetName, true); - sender.sendMessage("§a已将 §e" + targetName + " §a设为领地 §b" + landName - + " §a的管理员(仅在该领地生效,不跨领地)"); - target.sendMessage("§6[区域防护] §a你已被设为领地 " + landName + " 的管理员"); - return true; - } - - // ===== unsetadmin 移除领地管理员(限指定领地)===== - if (sub.equals("unsetadmin")) { - if (!isAreaAdmin(sender)) { - sender.sendMessage("§c需要管理员权限"); - return true; - } - if (args.length < 3) { - sender.sendMessage("§e用法: /protect unsetadmin <领地名> <玩家>"); - return true; - } - String landName = args[1]; - String targetName = args[2]; - AreaConfig ac = getLand(landName); - if (ac == null) { - sender.sendMessage("§c领地不存在: " + landName); - return true; - } - Player target = Bukkit.getPlayerExact(targetName); - if (target == null) { - sender.sendMessage("§c玩家不在线: " + targetName); - return true; - } - boolean hadRole = isLandAdmin(landName, targetName); - // 兼容旧版:旧 setadmin 会给玩家全局管理员ScoreboardTag,造成跨领地越权; - // 撤销时一并清除该遗留标签,确保彻底收回全局权限 - boolean hadTag = false; - ConfigManager cfg = plugin.getConfigMgr(); - String tag = cfg != null ? cfg.areaProtectAdminTag : null; - if (tag != null && !tag.isEmpty() && target.getScoreboardTags().contains(tag)) { - hadTag = true; - target.removeScoreboardTag(tag); - } - if (hadRole) setLandAdmin(landName, targetName, false); - if (!hadRole && !hadTag) { - sender.sendMessage("§e" + targetName + " §c不是该领地管理员,也未持有全局管理员标签"); - return true; - } - sender.sendMessage("§a已移除 §e" + targetName + " §a在领地 §b" + landName + " §a的管理员权限" - + (hadTag ? "(并清除其遗留的全局管理员标签)" : "")); - target.sendMessage("§6[区域防护] §c你在领地 " + landName + " 的管理员权限已被移除" - + (hadTag ? "(全局管理员标签已清除)" : "")); - return true; - } - - // ===== addvisitor 添加访客(所有者+管理员)===== - if (sub.equals("addvisitor")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - String[] parsed = parseAreaAndTarget(args.length >= 3 ? args[1] : null, args.length >= 3 ? args[2] : null); - if (parsed == null || args.length < 2) { - // 尝试获取玩家当前所在领地 - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - sender.sendMessage("§c用法: /protect addvisitor <领地> <玩家>"); - return true; - } - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - // ★ 删除冷却期间添加成员 → 自动取消删除 - if (hasPendingDelete(ac.name)) { - cancelPendingDelete(ac.name); - p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); - } - if (args.length < 2) { - sender.sendMessage("§c用法: /protect addvisitor <玩家>"); - return true; - } - // ★ 领地主不能作为成员添加 - if (samePlayer(args[1], ac.owner)) { - sender.sendMessage("§c领地所有者本身就是领地主,无需添加为成员"); - return true; - } - // ★ 校验玩家是否在login.db中注册 - { - DatabaseManager dbMgr = plugin.getDb(); - if (dbMgr != null && !dbMgr.userExists(args[1])) { - sender.sendMessage("§c§l[添加成员] §f玩家 " + args[1] + " 不存在(未在login.db中注册)"); - 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的访客"); - return true; - } - String areaName = parsed[0]; - String playerName = parsed[1]; - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - // ★ 领地主不能作为成员添加 - if (samePlayer(playerName, ac.owner)) { - sender.sendMessage("§c领地所有者本身就是领地主,无需添加为成员"); - return true; - } - // ★ 校验玩家是否在login.db中注册 - { - DatabaseManager dbMgr = plugin.getDb(); - if (dbMgr != null && !dbMgr.userExists(playerName)) { - sender.sendMessage("§c§l[添加成员] §f玩家 " + playerName + " 不存在(未在login.db中注册)"); - return true; - } - } - // ★ 删除冷却期间添加成员 → 自动取消删除 - if (hasPendingDelete(areaName)) { - 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的访客"); - return true; - } - - // ===== removevisitor / removemember 移除访客/成员 ===== - if (sub.equals("removevisitor") || sub.equals("removemember")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - String[] parsed = parseAreaAndTarget(args.length >= 3 ? args[1] : null, args.length >= 3 ? args[2] : null); - if (parsed == null || args.length < 2) { - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - sender.sendMessage("§c用法: /protect removevisitor <领地> <玩家>"); - return true; - } - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - // ★ 删除冷却期间移除成员 → 自动取消删除 - if (hasPendingDelete(ac.name)) { - cancelPendingDelete(ac.name); - p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); - } - if (args.length < 2) { - sender.sendMessage("§c用法: /protect removevisitor <玩家>"); - return true; - } - removePlayerFromAreaWhitelist(ac.name, args[1]); - sender.sendMessage("§a已移除 §e" + args[1] + " §a的 §e" + ac.name + " §a访客权限"); - return true; - } - String areaName = parsed[0]; - String playerName = parsed[1]; - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - // ★ 删除冷却期间移除成员 → 自动取消删除 - if (hasPendingDelete(areaName)) { - cancelPendingDelete(areaName); - p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); - } - removePlayerFromAreaWhitelist(areaName, playerName); - sender.sendMessage("§a已移除 §e" + playerName + " §a的 §e" + areaName + " §a访客权限"); - return true; - } - - // ===== listvisitors 列出访客 ===== - if (sub.equals("listvisitors")) { - String areaName = args.length >= 2 ? resolveAreaName(args[1]) : null; - if (areaName == null && sender instanceof Player) { - Player p = (Player) sender; - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac != null) areaName = ac.name; - } - if (areaName == null) { - sender.sendMessage("§c用法: /protect listvisitors <领地>"); - return true; - } - Set visitors = areaPlayerWhitelist.get(areaName); - sender.sendMessage("§e§l==== " + areaName + " 访客列表 ===="); - if (visitors == null || visitors.isEmpty()) { - sender.sendMessage("§7(无访客)"); - } else { - for (String v : visitors) { - sender.sendMessage("§f- §a" + v); - } - } - return true; - } - - // ===== transfer / transfeland 转让领地 ===== - if (sub.equals("transfer") || sub.equals("transfeland")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§e用法: /protect transfer <领地> <新所有者>"); - return true; - } - // ★ 如果只给了领地名,进入交互式输入流程 - if (args.length == 2) { - String areaName = resolveAreaName(args[1]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[1]); - return true; - } - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - // 记录待输入状态 - pendingTransferInput.put(p.getUniqueId(), new String[]{areaName}); - p.sendMessage("§e请在聊天栏输入新所有者名称(输入 §c取消§e)"); - p.sendMessage("§7§l───────────────────────────────"); - return true; - } - String areaName = resolveAreaName(args[1]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[1]); - return true; - } - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - String newOwner = args[2]; - String oldOwner = ac.owner; - - // ★ 验证新所有者名格式 - if (!newOwner.matches("^[a-zA-Z0-9_]{3,16}$")) { - sender.sendMessage("§c玩家名格式无效:仅允许英文字母、数字和下划线(3-16位)"); - return true; - } - if (samePlayer(newOwner, oldOwner)) { - sender.sendMessage("§c不能转让给自己"); - return true; - } - - // ★ 验证新所有者是否存在(login.db) - DatabaseManager dbMgr = plugin.getDb(); - if (dbMgr != null && !dbMgr.userExists(newOwner)) { - sender.sendMessage("§c玩家 §e" + newOwner + " §c尚未注册"); - return true; - } - - // ★ 验证玩家注册时间必须超过5分钟(防止注册秒退玩家接收领地) - if (dbMgr != null) { - long registerTime = 0; - try { - Object regTimeObj = dbMgr.getField(newOwner, "register_time"); - if (regTimeObj instanceof Number) { - // ★ 第十一批:秒/毫秒混存归一化 - registerTime = DurationParser.toMs( - ((Number) regTimeObj).longValue()); - } - } catch (Exception e) { - // 忽略异常 - } - if (registerTime > 0) { - long now = System.currentTimeMillis(); - long fiveMinutesMs = 5 * 60 * 1000; - if (now - registerTime < fiveMinutesMs) { - long minutesLeft = (fiveMinutesMs - (now - registerTime)) / 60000; - sender.sendMessage("§c玩家 §e" + newOwner + " §c注册时间不足5分钟(还差" + minutesLeft + "分钟)"); - return true; - } - } - } - - // ★ 执行转让 - ac.owner = newOwner; - saveAreaToDb(ac); - - // ★ 获取权限快照 + 追踪cooldown - if (plugin.webManager != null) { - String snapshot = getLandPermissionsSnapshot(areaName); - long expiresAt = System.currentTimeMillis() + 60000; // 60秒cooldown - WebManager.TransferInfo info = new WebManager.TransferInfo( - areaName, oldOwner, newOwner, expiresAt, 0, 0, snapshot - ); - plugin.webManager.trackTransfer(areaName, info); - // 通知PHP同步 - plugin.webManager.requestImmediateLandSync(); - } - - sender.sendMessage("§a已将 §e" + areaName + " §a从 §e" + (oldOwner != null ? oldOwner : "无") + " §a转让给 §e" + newOwner + "§a,§e60秒冷却期内可取消"); - return true; - } - - // ===== canceltransfer 取消过户 ===== - if (sub.equals("canceltransfer")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - if (args.length < 2) { - sender.sendMessage("§e用法: /protect canceltransfer <领地>"); - return true; - } - String areaName = resolveAreaName(args[1]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[1]); - return true; - } - AreaConfig ac = areas.get(areaName); - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - - // ★ 查找并取消进行中的过户 - if (plugin.webManager != null && plugin.webManager.cancelTransfer(areaName, p.getName())) { - sender.sendMessage("§a过户已取消,领地 §e" + areaName + " §a已恢复"); - } else { - sender.sendMessage("§c没有可取消的过户(可能已过期或不存在)"); - } - return true; - } - - // ===== settp 设置领地传送点 ===== - if (sub.equals("settp") || sub.equals("setwarp")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = null; - // ★ 支持指定领地名:/protect settp <领地名> - if (args.length >= 2) { - String targetName = args[1]; - String resolved = resolveAreaName(targetName); - if (resolved != null) targetName = resolved; - ac = areas.get(targetName); - if (ac == null) { - sender.sendMessage("§c领地不存在: " + targetName); - return true; - } - } else { - // 找到玩家所在的领地 - ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - sender.sendMessage("§c你不在任何领地内,请指定领地名: /protect settp <领地名>"); - return true; - } - } - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - sender.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - ac.warpX = p.getLocation().getX(); - ac.warpY = p.getLocation().getY(); - ac.warpZ = p.getLocation().getZ(); - ac.warpYaw = p.getLocation().getYaw(); - ac.warpPitch = p.getLocation().getPitch(); - ac.warpWorld = p.getWorld().getName(); - saveAreaToDb(ac); - cancelPendingDelete(ac.name); - sender.sendMessage("§a§l[防护] §f已将 §e" + ac.name + " §f的传送点设置为当前坐标"); - return true; - } - - // ===== tp 传送到领地传送点 ===== - if (sub.equals("tp") || sub.equals("warp")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); - return true; - } - Player p = (Player) sender; - AreaConfig ac = null; - if (args.length >= 2) { - String areaName = resolveAreaName(args[1]); - if (areaName == null) { - sender.sendMessage("§c领地不存在: " + args[1]); - return true; - } - ac = areas.get(areaName); - } else { - // 传送到当前所在领地的传送点 - ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - sender.sendMessage("§c你不在任何领地内,请指定领地名: /protect tp <领地名>"); - return true; - } - } - if (ac == null) { - sender.sendMessage("§c领地不存在"); - return true; - } - // ★ 传送权限检查:领地主/插件管理员/领地管理员 无条件允许;其余需授权 - if (!canTeleportToLand(p, ac)) { - sender.sendMessage("§c你没有权限传送到该领地,需领地主或管理员授权(/protect cli memberperm " + ac.name + ")"); - return true; - } - // 检查传送点是否已设置 - if (ac.warpWorld == null || ac.warpWorld.isEmpty()) { - // 没有传送点,传送到领地中心 - int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2; - int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2; - World w = Bukkit.getWorld(ac.world); - plugin.getLogger().info("[防护-TP] ★★★ 无传送点,传送到领地中心: 区域=" + ac.name - + " 世界=" + ac.world + " 中心=(" + cx + "," + cz + ") worldObj=" + (w != null ? "OK" : "NULL")); - if (w == null) { - sender.sendMessage("§c世界不存在: " + ac.world); - return true; - } - // 找到安全位置:从最高向下找第一个上方有2格空气、下方有实体的方块 - Location safeLoc = null; - for (int y = w.getMaxHeight(); y >= w.getMinHeight(); y--) { - Location check = new Location(w, cx, y, cz); - // 脚下必须有实体方块 - if (!check.getBlock().getType().isSolid()) continue; - // 脚上和头顶至少2格空气(站立空间) - if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue; - if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue; - safeLoc = check.clone().add(0, 1, 0); // 站在方块上表面+1的位置 - break; - } - if (safeLoc != null) { - p.teleport(safeLoc); - sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(未设置传送点)"); - } else { - sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)"); - } - } else { - World w = Bukkit.getWorld(ac.warpWorld); - plugin.getLogger().info("[防护-TP] ★★★ 有传送点: 区域=" + ac.name - + " warpWorld=" + ac.warpWorld + " warpPos=(" + ac.warpX + "," + ac.warpY + "," + ac.warpZ + ")" - + " worldObj=" + (w != null ? "OK" : "NULL")); - if (w == null) { - // 传送点世界不存在,回退到领地中心传送 - plugin.getLogger().warning("[防护-TP] 传送点世界不存在,回退到领地中心: " + ac.warpWorld); - int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2; - int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2; - World fallback = Bukkit.getWorld(ac.world); - if (fallback == null) { - sender.sendMessage("§c世界不存在: " + ac.world); - return true; - } - // 找到安全位置 - Location safeLoc = null; - for (int y = fallback.getMaxHeight(); y >= fallback.getMinHeight(); y--) { - Location check = new Location(fallback, cx, y, cz); - if (!check.getBlock().getType().isSolid()) continue; - if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue; - if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue; - safeLoc = check.clone().add(0, 1, 0); - break; - } - if (safeLoc != null) { - p.teleport(safeLoc); - sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(传送点世界不存在)"); - } else { - sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)"); - } - return true; - } - Location warp = new Location(w, ac.warpX, ac.warpY, ac.warpZ, ac.warpYaw, ac.warpPitch); - p.teleport(warp); - sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f传送点"); - } - return true; - } - - showHelp(sender); - return true; - } - - /** - * ★ 删除领地时按创建扣款比例退费 - * 退费比例读取 area_config.refund_ratio_per_cent: - * 0 = 关闭退费(不退) - * 100 = 全额退款(原价 100%) - * 其他= 原价 × 比例% - * 债券走 BondManager.addBonds(记 land_refund 流水) - */ - private void refundOnLandDelete(AreaConfig ac, String operator, String reason) { - if (ac == null) return; - int ratio = globalRefundRatioPerCent; - if (ratio <= 0) return; // 0 = 关闭退费 - if (ratio > 100) ratio = 100; // 上限 100% - if (ac.createCost <= 0) return; // 未扣款(历史数据/管理员创建)不退 - int refund = (int) Math.floor(ac.createCost * (ratio / 100.0)); - if (refund <= 0) return; - BondManager bm = plugin.getBonds(); - if (bm == null) return; - String op = (operator == null || operator.isEmpty()) ? "system" : operator; - int after = bm.addBonds(ac.owner, refund, "land_refund", ac.owner, op, reason); - if (after < 0) { - plugin.getLogger().warning("[防护] 领地退费失败(账户冻结?): " + ac.owner + " +" + refund); - return; - } - plugin.getLogger().info("[防护] 领地 " + ac.name + " 删除退费: " + ac.owner - + " +" + refund + " (原价 " + ac.createCost + " × " + ratio + "%)"); - Player ownerOnline = Bukkit.getPlayerExact(ac.owner); - if (ownerOnline != null && ownerOnline.isOnline()) { - ownerOnline.sendMessage("§a§l[防护] §f领地 §e" + ac.name + " §f已删除,退还债券 §e" + refund - + " §7(创建原价 " + ac.createCost + " × " + ratio + "%)"); - } - } - - /** - * ★ 向在线的插件管理员 & OP 上报领地强制删除信息(含坐标) - */ - private void reportLandRemovalToAdmins(String message) { - int sent = 0; - for (Player online : Bukkit.getOnlinePlayers()) { - if (online.isOp() || isAreaAdmin(online)) { - online.sendMessage(message); - sent++; - } - } - plugin.getLogger().info("[防护] 已上报 " + sent + " 名在线管理员: " + message.replaceAll("§.", "")); - } - - /** - * ★ 扩建 / 收缩单价:与创建同一口径(用户组可覆盖,缺省为创建单价 globalCreatePricePerSqm)。 - * 扩建收费、收缩退费都按这个单价算,保证"创建一次扣费、扩建不再白嫖"。 - */ - private int landPricePerSqm(Player p) { - UserGroupManager ug = plugin.getUserGroup(); - return (ug != null) - ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) - : globalCreatePricePerSqm; - } - - // ==================== 领地面积审批流(2026-09-29) ==================== - - /** 待审批的面积超限请求(expand=扩建提案 / create=创建提案) */ - private static class PendingExpand { - String type; // "expand" | "create" - String landName; // 领地名(create 为拟定名) - String requester; // 申请人 - String world; - int oldX1, oldX2, oldZ1, oldZ2; // expand=原边界快照 / create=选区 - int newX1, newX2, newZ1, newZ2; // 提案边界(create 时同选区) - int amount; - String dir; - long deltaSq, newW, newL, oldW, oldL; // 展示用(expand) - int area, cost; // 展示用(create,申请时价格快照) - long createdAt; - } - - /** 领地名(小写) → 待审批请求;同一领地重复申请以最新为准 */ - private final Map pendingExpands = new ConcurrentHashMap<>(); - - private PendingExpand findPending(String key) { - PendingExpand byLand = pendingExpands.get(key.toLowerCase()); - if (byLand != null) return byLand; - PendingExpand found = null; - for (PendingExpand pe : pendingExpands.values()) { - if (pe.requester.equalsIgnoreCase(key)) { - if (found != null) return null; // 多个匹配 → 要求用领地名精确指定 - found = pe; - } - } - return found; - } - - /** 申请人名下待审批条数(用于"多条申请请用领地名"的精确提示) */ - private int countPendingByRequester(String name) { - int n = 0; - for (PendingExpand pe : pendingExpands.values()) { - if (pe.requester.equalsIgnoreCase(name)) n++; - } - return n; - } - - /** 待审批提案有效期:24 小时(超时自动作废,防陈旧提案被误批) */ - private static final long PENDING_EXPIRE_MS = 24L * 60 * 60 * 1000L; - - /** 惰性清理超时提案(读取待审批列表前调用) */ - private void pruneExpiredPending() { - if (pendingExpands.isEmpty()) return; - long now = System.currentTimeMillis(); - for (java.util.Iterator> it = - pendingExpands.entrySet().iterator(); it.hasNext();) { - Map.Entry e = it.next(); - PendingExpand pe = e.getValue(); - if (now - pe.createdAt <= PENDING_EXPIRE_MS) continue; - it.remove(); - Player req = Bukkit.getPlayerExact(pe.requester); - if (req != null && req.isOnline()) { - req.sendMessage("§c§l[防护] §f你的领地 §e" + pe.landName + " §f" - + ("expand".equals(pe.type) ? "扩建" : "创建") - + "申请已超时(24小时)自动作废,请重新提交"); - } - plugin.getLogger().info("[领地审批] 提案超时作废: " + pe.landName - + " (" + pe.type + ", 申请人 " + pe.requester + ")"); - } - } - - /** ★ 扩建超限 → 登记审批请求 + 通知申请人与在线管理员 */ - private void registerPendingExpand(Player p, AreaConfig ac, - int nx1, int nx2, int nz1, int nz2, - int amount, String dir, long deltaSq, long newW, long newL, - long oldW, long oldL) { - PendingExpand pe = new PendingExpand(); - pe.type = "expand"; - pe.landName = ac.name; - pe.requester = p.getName(); - pe.world = ac.world; - pe.oldX1 = Math.min(ac.x1, ac.x2); - pe.oldX2 = Math.max(ac.x1, ac.x2); - pe.oldZ1 = Math.min(ac.z1, ac.z2); - pe.oldZ2 = Math.max(ac.z1, ac.z2); - pe.newX1 = nx1; pe.newX2 = nx2; pe.newZ1 = nz1; pe.newZ2 = nz2; - pe.amount = amount; - pe.dir = dir; - pe.deltaSq = deltaSq; pe.newW = newW; pe.newL = newL; - pe.oldW = oldW; pe.oldL = oldL; - pe.createdAt = System.currentTimeMillis(); - pendingExpands.put(ac.name.toLowerCase(), pe); - - p.sendMessage("§e§l[防护] §f扩建超出免审上限(本次 +§e" + deltaSq - + "§f㎡,上限 §e" + globalMaxFreeAreaSqm + "§f㎡;新尺寸 §e" - + newW + "×" + newL + "§f,边长上限 §e" + globalMaxLandSideBlocks - + "§f),已提交管理员现场审批。"); - p.sendMessage("§7管理员需进入该领地后执行 §a/protect accept " + ac.name + " §7或 §c/protect deny " + ac.name); - long peCostL = deltaSq * (long) landPricePerSqm(p); - p.sendMessage("§7审批通过时按创建单价 §e" + landPricePerSqm(p) - + "/㎡ §f收取 §e" + (peCostL > Integer.MAX_VALUE ? "(超额,请分次扩建)" : String.valueOf(peCostL)) - + " §f债券"); - reportLandRemovalToAdmins("§6§l[领地审批] §f玩家 §e" + p.getName() - + " §f申请扩建领地 §e" + ac.name + " §7(+" + deltaSq + "㎡, 向" + dir + amount + "格, 新尺寸 " - + newW + "×" + newL + ") §f——管理员到现场后 §a/protect accept " + ac.name + "§f/§cdeny"); - plugin.getLogger().info("[领地审批] 扩建申请: " + p.getName() + " → " + ac.name - + " +" + deltaSq + "㎡ (上限" + globalMaxFreeAreaSqm + ") 新尺寸 " + newW + "×" + newL); - } - - /** ★ 创建超限 → 登记审批请求(不扣费,审批通过时按当时价格扣) */ - private void registerPendingCreate(Player p, String name, - Location cl1, Location cl2, int cw, int cl, int cArea, int cCost) { - PendingExpand pe = new PendingExpand(); - pe.type = "create"; - pe.landName = name; - pe.requester = p.getName(); - pe.world = cl1.getWorld().getName(); - pe.oldX1 = cl1.getBlockX(); pe.oldZ1 = cl1.getBlockZ(); - pe.oldX2 = cl2.getBlockX(); pe.oldZ2 = cl2.getBlockZ(); - pe.newX1 = Math.min(cl1.getBlockX(), cl2.getBlockX()); - pe.newX2 = Math.max(cl1.getBlockX(), cl2.getBlockX()); - pe.newZ1 = Math.min(cl1.getBlockZ(), cl2.getBlockZ()); - pe.newZ2 = Math.max(cl1.getBlockZ(), cl2.getBlockZ()); - pe.area = cArea; - pe.cost = cCost; - pe.newW = cw; pe.newL = cl; - pe.createdAt = System.currentTimeMillis(); - pendingExpands.put(name.toLowerCase(), pe); - - p.sendMessage("§e§l[防护] §f创建选区 §e" + name + " §f(" + cw + "×" + cl - + " = " + cArea + "㎡)超出免审上限(单次 " + globalMaxFreeAreaSqm - + "㎡ / 边长 " + globalMaxLandSideBlocks + "格),已提交管理员现场审批,暂未扣费。"); - p.sendMessage("§7管理员需进入选区后执行 §a/protect accept " + name + " §7或 §c/protect deny " + name); - reportLandRemovalToAdmins("§6§l[领地审批] §f玩家 §e" + p.getName() - + " §f申请创建领地 §e" + name + " §7(" + cw + "×" + cl + " = " + cArea - + "㎡) §f——管理员到选区后 §a/protect accept " + name + "§f/§cdeny"); - plugin.getLogger().info("[领地审批] 创建申请: " + p.getName() + " → " + name - + " " + cw + "×" + cl + " (" + cArea + "㎡)"); - } - - /** ★ 审批命令核心:/protect accept|deny <领地名|申请人> */ - private void handleApproval(Player adm, String[] args, boolean accept) { - pruneExpiredPending(); - if (!isAreaAdmin(adm)) { - adm.sendMessage("§c§l[审批] §f需要插件管理员权限"); - return; - } - if (args.length < 2) { - adm.sendMessage("§e用法: /protect " + (accept ? "accept" : "deny") + " <领地名|申请人>"); - if (pendingExpands.isEmpty()) { - adm.sendMessage("§7当前没有待审批的面积申请"); - return; - } - adm.sendMessage("§e==== 待审批列表 ===="); - for (PendingExpand pe : pendingExpands.values()) { - adm.sendMessage((("expand".equals(pe.type)) ? "§e[扩建] " : "§e[创建] ") - + "§f" + pe.landName + " §7申请人=" + pe.requester - + ("expand".equals(pe.type) - ? (" +" + pe.deltaSq + "㎡ 新尺寸" + pe.newW + "×" + pe.newL) - : (" " + pe.newW + "×" + pe.newL + " = " + pe.area + "㎡"))); - } - adm.sendMessage("§7审批必须亲自站到领地/选区范围内"); - return; - } - PendingExpand pe = findPending(args[1]); - if (pe == null) { - if (countPendingByRequester(args[1]) > 1) { - adm.sendMessage("§c§l[审批] §f申请人 §e" + args[1] - + " §f名下有多条申请,请用领地名指定"); - } else { - adm.sendMessage("§c§l[审批] §f没有找到待审批的申请: §e" + args[1]); - } - return; - } - - // ★ 现场校验:审批的管理员必须在领地/选区范围内 - Location admLoc = adm.getLocation(); - boolean inRange; - String rangeDesc; - if ("expand".equals(pe.type)) { - AreaConfig ac = areas.get(pe.landName); - AreaConfig here = getArea(admLoc.getWorld().getName(), - admLoc.getBlockX(), admLoc.getBlockY(), admLoc.getBlockZ()); - inRange = (ac != null && here != null && here.name.equalsIgnoreCase(pe.landName)); - rangeDesc = "领地 §e" + pe.landName; - } else { - inRange = admLoc.getWorld().getName().equals(pe.world) - && admLoc.getBlockX() >= Math.min(pe.newX1, pe.newX2) - && admLoc.getBlockX() <= Math.max(pe.newX1, pe.newX2) - && admLoc.getBlockZ() >= Math.min(pe.newZ1, pe.newZ2) - && admLoc.getBlockZ() <= Math.max(pe.newZ1, pe.newZ2); - rangeDesc = "创建选区 §e" + pe.landName; - } - if (!inRange) { - adm.sendMessage("§c§l[审批] §f审批" + (accept ? "通过" : "驳回") - + "需要你亲自站在" + rangeDesc + " §f范围内(当前不在)"); - return; - } - - Player requester = Bukkit.getPlayerExact(pe.requester); - - if (!accept) { - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§a§l[审批] §f已驳回 §e" + pe.landName + " §f的" - + ("expand".equals(pe.type) ? "扩建" : "创建") + "申请"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§c§l[防护] §f你的领地 §e" + pe.landName + " §f" - + ("expand".equals(pe.type) ? "扩建" : "创建") + "申请被管理员驳回"); - } - plugin.getLogger().info("[领地审批] 驳回: " + pe.landName + " (申请人 " + pe.requester - + ", 审批人 " + adm.getName() + ")"); - return; - } - - if ("expand".equals(pe.type)) { - // 边界已被改动的提案作废(防申请后领地被转让/缩放再套用旧提案) - AreaConfig ac = areas.get(pe.landName); - if (ac == null) { - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§c§l[审批] §f领地 §e" + pe.landName + " §f已不存在,申请作废"); - return; - } - int curX1 = Math.min(ac.x1, ac.x2), curX2 = Math.max(ac.x1, ac.x2); - int curZ1 = Math.min(ac.z1, ac.z2), curZ2 = Math.max(ac.z1, ac.z2); - if (curX1 != pe.oldX1 || curX2 != pe.oldX2 - || curZ1 != pe.oldZ1 || curZ2 != pe.oldZ2) { - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§c§l[审批] §f领地 §e" + pe.landName - + " §f边界在申请后已发生变化,提案作废,请申请人重新提交"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§c§l[防护] §f你的扩建申请作废:领地边界已变化,请重新 /protect expand"); - } - return; - } - // ★ 扩建审批扣费(2026-10-03):审批时按创建单价 × 新增面积收款 - UserGroupManager ePugm = plugin.getUserGroup(); - int ePrice = (ePugm != null) - ? ePugm.getPlayerLandPricePerSqm(pe.requester, globalCreatePricePerSqm) - : globalCreatePricePerSqm; - long eCostL = pe.deltaSq * (long) ePrice; - if (eCostL > Integer.MAX_VALUE) { - adm.sendMessage("§c§l[审批] §f该扩建费用超出上限,无法扣费,申请保留待处理"); - return; - } - int eCost = (int) eCostL; - int eCharged = 0; - if (eCost > 0) { - BondManager ebm = plugin.getBonds(); - if (ebm == null || !ebm.deductBonds(pe.requester, eCost, "land_expand", - pe.requester, pe.requester, - "管理员审批扩建领地: " + pe.landName + " (+" + pe.deltaSq + "㎡×" + ePrice + ")")) { - adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester - + " §f余额不足(需 §e" + eCost + "§f),无法扣费,申请保留待处理"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§c§l[防护] §f你的扩建申请因余额不足(需 " + eCost - + " 债券)未能完成,请充值后请管理员重新审批"); - } - return; - } - eCharged = eCost; - } - ac.x1 = pe.newX1; ac.x2 = pe.newX2; - ac.z1 = pe.newZ1; ac.z2 = pe.newZ2; - ac.createCost += eCharged; // ★ 计入删除退费基数 - saveAreaToDb(ac); - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§a§l[审批] §f已批准 §e" + pe.landName + " §f扩建:+§e" - + pe.deltaSq + "§f㎡(" + pe.oldW + "×" + pe.oldL + " → §e" - + pe.newW + "×" + pe.newL + "§f),向申请人扣除 §e" + eCharged + " §f债券"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§a§l[防护] §f你的领地 §e" + pe.landName - + " §f扩建申请已被管理员批准,已生效,扣除 " + eCharged + " 债券"); - } - plugin.getLogger().info("[领地审批] 扩建通过: " + pe.landName + " (申请人 " + pe.requester - + ", 审批人 " + adm.getName() + ") +" + pe.deltaSq + "㎡"); - } else { - // ---- 创建审批通过:重名/上限/扣费 重新校验后落库 ---- - if (areaNameExists(pe.landName)) { - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§c§l[审批] §f领地名 §e" + pe.landName + " §f已被占用,申请作废"); - return; - } - UserGroupManager aPugm = plugin.getUserGroup(); - int aMax = (aPugm != null) ? aPugm.getPlayerMaxLands(pe.requester, globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; - long aCnt = areas.values().stream().filter(a -> samePlayer(pe.requester, a.owner)).count(); - if (aCnt >= aMax) { - pendingExpands.remove(pe.landName.toLowerCase()); - adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester + " §f已达领地上限(" + aMax + "),申请作废"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§c§l[防护] §f你的领地数量已达上限,创建申请作废"); - } - return; - } - int aPrice = (aPugm != null) ? aPugm.getPlayerLandPricePerSqm(pe.requester, globalCreatePricePerSqm) : globalCreatePricePerSqm; - int aCost = pe.area * aPrice; - int charged = 0; - if (aCost > 0) { - BondManager abm = plugin.getBonds(); - if (abm != null) { - if (!abm.deductBonds(pe.requester, aCost, "land_create", pe.requester, pe.requester, - "管理员审批创建领地: " + pe.landName + " (" + pe.area + "㎡×" + aPrice + ")")) { - adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester - + " §f余额不足(需 §e" + aCost + "§f),无法扣费,申请保留待处理"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§c§l[防护] §f你的领地创建申请因余额不足(需 " + aCost + " 债券)未能完成,请充值后请管理员重新审批"); - } - return; - } - charged = aCost; - } - } - AreaConfig nc = new AreaConfig(); - nc.name = pe.landName; - nc.owner = pe.requester; - nc.world = pe.world; - nc.x1 = pe.newX1; nc.z1 = pe.newZ1; - nc.x2 = pe.newX2; nc.z2 = pe.newZ2; - nc.yMin = 0; nc.yMax = 255; - nc.createCost = charged; - saveAreaToDb(nc); - areas.put(pe.landName, nc); - pendingExpands.remove(pe.landName.toLowerCase()); - loadAllAreas(); - adm.sendMessage("§a§l[审批] §f已批准创建领地 §e" + pe.landName + " §f(§e" - + pe.area + "㎡§f),向申请人扣除 §e" + charged + " §f债券"); - if (requester != null && requester.isOnline()) { - requester.sendMessage("§a§l[防护] §f你的领地 §e" + pe.landName - + " §f已由管理员审批创建成功(" + pe.area + "㎡,扣除 " + charged + " 债券)"); - } - plugin.getLogger().info("[领地审批] 创建通过: " + pe.landName + " (申请人 " + pe.requester - + ", 审批人 " + adm.getName() + ", 扣费 " + charged + ")"); - } - } - - /** ★ 供 Tab 补全:待审批目标(领地名 + 申请人名) */ - public List getPendingApprovalTargets() { - pruneExpiredPending(); - List out = new ArrayList<>(); - for (PendingExpand pe : pendingExpands.values()) { - out.add(pe.landName); - if (!out.contains(pe.requester)) out.add(pe.requester); - } - return out; - } - - // ==================== 封禁/注销领主的领地自动清理 ==================== - - private BukkitTask bannedOwnerSweepTask; - /** 扫描周期:5分钟 */ - private static final long SWEEP_INTERVAL_TICKS = 6000L; - /** 剩余封禁时长 ≥ 9年,视为"10年封禁" */ - private static final long LONG_BAN_REMAIN_MS = 9L * 365 * 24 * 3600 * 1000L; - - /** - * ★ 启动封禁/注销领主的领地自动清理任务 - * 命中以下任一条件的领地 → 立即强制删除(不退款、不注销账号), - * 并把领地坐标上报给在线的插件管理员 & OP: - * 1) 领主被永久封禁 - * 2) 领主被 10 年封禁 - * 3) 领主账户已注销 - */ - public void startBannedOwnerSweepTask() { - if (bannedOwnerSweepTask != null) { - bannedOwnerSweepTask.cancel(); - } - bannedOwnerSweepTask = new BukkitRunnable() { - @Override - public void run() { - try { - sweepBannedOwnerLands(); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 领地自动清理任务异常: " + e.getMessage()); - } - } - }.runTaskTimer(plugin, 200L, SWEEP_INTERVAL_TICKS); - } - - public void stopBannedOwnerSweepTask() { - if (bannedOwnerSweepTask != null) { - bannedOwnerSweepTask.cancel(); - bannedOwnerSweepTask = null; - } - } - - /** - * ★ 两阶段执行: - * 阶段1(主线程):只做 Bukkit 侧判定(封禁表 / 在线状态),不碰 DB; - * 阶段2(异步):userExists 判定 + deleteAreaFromDb 落库 + 触发 PHP 同步(含 HTTP)。 - * 上报要遍历在线玩家,回主线程。 - */ - private void sweepBannedOwnerLands() { - if (areas.isEmpty()) return; - - // ---- 阶段1(主线程,纯 Bukkit 只读判定)---- - final List candidates = new ArrayList<>(); - for (AreaConfig ac : new ArrayList<>(areas.values())) { - if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue; - // 快照遍历期间已被删除/重载的跳过 - if (areas.get(ac.name) != ac) continue; - - String reason = null; - - // 1/2) 永久封禁 / 10年封禁 - try { - org.bukkit.BanEntry ban = - Bukkit.getBanList(org.bukkit.BanList.Type.NAME).getBanEntry(ac.owner); - if (ban != null) { - java.util.Date exp = ban.getExpiration(); - if (exp == null) { - reason = "永久封禁"; - } else if (exp.getTime() - System.currentTimeMillis() >= LONG_BAN_REMAIN_MS) { - reason = "10年封禁"; - } - } - } catch (Exception ignored) {} - - if (reason == null) { - // 3) 账户已注销(在线领主一律跳过,避免误删)—— 在线判定是 Bukkit API - Player ownerOnline = Bukkit.getPlayerExact(ac.owner); - if (ownerOnline != null && ownerOnline.isOnline()) continue; - } - // reason 为 null = 还需异步阶段用 DB 判定是否注销 - candidates.add(new Object[]{ac, reason}); - } - if (candidates.isEmpty()) return; - - final DatabaseManager dbMgr = plugin.getDb(); - // ---- 阶段2(异步:DB 判定 + 删除落库 + PHP 同步)---- - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - final List reports = new ArrayList<>(); - try { - for (Object[] o : candidates) { - AreaConfig ac = (AreaConfig) o[0]; - String reason = (String) o[1]; - if (reason == null) { - if (dbMgr == null) continue; - try { - if (!dbMgr.userExists(ac.owner)) { - reason = "账户已注销"; - } - } catch (Exception ignored) {} - if (reason == null) continue; - } - if (areas.get(ac.name) != ac) continue; - - // 不退款、不注销账号,直接强制删除 - cancelPendingDelete(ac.name); - deleteAreaFromDb(ac.name); - areas.remove(ac.name); - String coord = ac.world + " " + ac.x1 + "," + ac.z1 + " ~ " + ac.x2 + "," + ac.z2; - plugin.getLogger().warning("[防护] 自动清理领地 " + ac.name + ":领主 " + ac.owner - + " 因" + reason + ",已强制删除(不退款)坐标 " + coord); - reports.add("§6§l[防护-自动清理] §f领主 §e" + ac.owner - + " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name - + " §7(不退款) [" + coord + "]"); - } - } catch (Exception e) { - plugin.getLogger().warning("[防护] 领地自动清理(异步阶段)异常: " + e.getMessage()); - } - if (reports.isEmpty()) return; - // ---- 阶段3(回主线程上报:遍历在线玩家 + OP/管理员判定)---- - Bukkit.getScheduler().runTask(plugin, () -> { - for (String m : reports) reportLandRemovalToAdmins(m); - }); - }); - } - - /** - * 如果领地有待删除请求,取消它(玩家操作领地时自动解除冻结) - */ - // ★ package-private供AreaCLIManager访问 - void cancelPendingDelete(String areaName) { - PendingDelete pd = pendingDeletes.remove(areaName); - if (pd != null) { - pd.task.cancel(); - Player p = Bukkit.getPlayerExact(pd.playerName); - if (p != null && p.isOnline()) { - p.sendMessage("§c§l[防护] §f领地 §e" + areaName + " §f已被操作,删除请求已自动取消"); - } - } - } - - - private String[] parseAreaAndTarget(String arg1, String arg2) { - // ★ 严格匹配:写操作绝不做模糊归一化,避免部分/大小写/特殊字符输入命中他人领地 - String r1 = resolveAreaNameStrict(arg1); - if (r1 != null) { - return new String[]{r1, arg2}; - } - String r2 = resolveAreaNameStrict(arg2); - if (r2 != null) { - return new String[]{r2, arg1}; - } - return null; - } - - private boolean playerExists(String playerName) { - // 在线玩家 - for (Player online : Bukkit.getOnlinePlayers()) { - if (online.getName().equalsIgnoreCase(playerName)) return true; - } - // 离线玩家 - @SuppressWarnings("deprecation") - OfflinePlayer op = Bukkit.getOfflinePlayer(playerName); - return op.hasPlayedBefore(); - } - - - private boolean validateArea(CommandSender sender, String areaName) { - if (!areas.containsKey(areaName)) { - sender.sendMessage("§c区域不存在: " + areaName); - return false; - } - return true; - } - - - private String matname(Material mat) { - String name = mat.name(); - String[] parts = name.split("_"); - StringBuilder sb = new StringBuilder(); - for (String part : parts) { - if (sb.length() > 0) sb.append(" "); - sb.append(part.substring(0, 1).toUpperCase()) - .append(part.substring(1).toLowerCase()); - } - return sb.toString(); - } - private String yawToDir(float yaw) { - // MC中: 0=南, 90=西, 180=北, -90/270=东 - yaw = yaw % 360; - if (yaw < 0) yaw += 360; - if (yaw >= 315 || yaw < 45) return "南"; - if (yaw >= 45 && yaw < 135) return "西"; - if (yaw >= 135 && yaw < 225) return "北"; - return "东"; - } - private void saveAreaConfig(AreaConfig ac) { - File f = new File(rootDir, ac.name + ".txt"); - if (!f.exists()) return; - try { - List lines = new ArrayList<>(); - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - if (line.trim().startsWith("起点:")) { - lines.add("起点: " + ac.world - + "," + ac.x1 + "," + ac.z1); - } else if (line.trim().startsWith("终点:")) { - lines.add("终点: " + ac.world - + "," + ac.x2 + "," + ac.z2); - } else { - lines.add(line); - } - } - br.close(); - PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(f), - StandardCharsets.UTF_8)); - for (String l : lines) pw.println(l); - pw.close(); - } catch (IOException ignored) {} - } - - /** - * 保存领地配置到数据库(新方法) - */ - public void saveAreaToDb(AreaConfig ac) { - if (dbConnection == null) return; - try { - // ★ 使用 INSERT ... ON CONFLICT 替代 INSERT OR REPLACE: - // INSERT OR REPLACE 会删除旧行插入新行,导致自增 id 改变, - // 进而使 area_land_permissions.land_id 指向不存在的老 id,造成管理员/成员权限丢失。 - PreparedStatement stmt = dbConnection.prepareStatement( - "INSERT INTO area_lands (name, owner, world, x1, z1, x2, z2, y_min, y_max, " - + "confiscate_items, deny_use_items, give_effects, clear_effects, clear_all_bad, " - + "punish_commands, deny_block_place, deny_block_break, deny_pvp, deny_fall_damage, " - + "deny_hunger, deny_all_damage, deny_drop, deny_mount, deny_ender_pearl, " - + "deny_bow, deny_potion, deny_explosion, deny_raid, deny_fire_spread, " - + "deny_all_effects, deny_item_frame, deny_entity_interact, deny_move, deny_pickup, deny_fire, " - + "peace_mode, peace_mode_duration, " - + "peace_whitelist, enforce_game_mode, mode_exempt, enter_msg, leave_msg, " - + "confiscate_msg, enable_announce, announce_template, txt_content, created_at, " - + "deny_thrown_projectiles, deny_glowing, deny_redstone_interaction, deny_door_interaction, " - + "deny_noteblock_jukebox, deny_lead, deny_crop_harvest, deny_wool_shear, deny_animal_feeding, " - + "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport, deny_farmland_trample, deny_ender_teleport, create_cost, deny_spawn_egg, deny_wax, bad_effects, deny_villager_trade, confiscate_count, return_on_leave) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - + "ON CONFLICT(name) DO UPDATE SET " - + "owner=excluded.owner, world=excluded.world, x1=excluded.x1, z1=excluded.z1, x2=excluded.x2, z2=excluded.z2, y_min=excluded.y_min, y_max=excluded.y_max, " - + "confiscate_items=excluded.confiscate_items, deny_use_items=excluded.deny_use_items, give_effects=excluded.give_effects, clear_effects=excluded.clear_effects, clear_all_bad=excluded.clear_all_bad, " - + "punish_commands=excluded.punish_commands, deny_block_place=excluded.deny_block_place, deny_block_break=excluded.deny_block_break, deny_pvp=excluded.deny_pvp, deny_fall_damage=excluded.deny_fall_damage, " - + "deny_hunger=excluded.deny_hunger, deny_all_damage=excluded.deny_all_damage, deny_drop=excluded.deny_drop, deny_mount=excluded.deny_mount, deny_ender_pearl=excluded.deny_ender_pearl, " - + "deny_bow=excluded.deny_bow, deny_potion=excluded.deny_potion, deny_explosion=excluded.deny_explosion, deny_raid=excluded.deny_raid, deny_fire_spread=excluded.deny_fire_spread, " - + "deny_all_effects=excluded.deny_all_effects, deny_item_frame=excluded.deny_item_frame, deny_entity_interact=excluded.deny_entity_interact, deny_move=excluded.deny_move, deny_pickup=excluded.deny_pickup, deny_fire=excluded.deny_fire, " - + "peace_mode=excluded.peace_mode, peace_mode_duration=excluded.peace_mode_duration, " - + "peace_whitelist=excluded.peace_whitelist, enforce_game_mode=excluded.enforce_game_mode, mode_exempt=excluded.mode_exempt, enter_msg=excluded.enter_msg, leave_msg=excluded.leave_msg, " - + "confiscate_msg=excluded.confiscate_msg, enable_announce=excluded.enable_announce, announce_template=excluded.announce_template, txt_content=excluded.txt_content, created_at=excluded.created_at, " - + "deny_thrown_projectiles=excluded.deny_thrown_projectiles, deny_glowing=excluded.deny_glowing, deny_redstone_interaction=excluded.deny_redstone_interaction, deny_door_interaction=excluded.deny_door_interaction, " - + "deny_noteblock_jukebox=excluded.deny_noteblock_jukebox, deny_lead=excluded.deny_lead, deny_crop_harvest=excluded.deny_crop_harvest, deny_wool_shear=excluded.deny_wool_shear, deny_animal_feeding=excluded.deny_animal_feeding, " - + "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport, deny_farmland_trample=excluded.deny_farmland_trample, deny_ender_teleport=excluded.deny_ender_teleport, create_cost=excluded.create_cost, deny_spawn_egg=excluded.deny_spawn_egg, deny_wax=excluded.deny_wax, bad_effects=excluded.bad_effects, deny_villager_trade=excluded.deny_villager_trade, confiscate_count=excluded.confiscate_count, return_on_leave=excluded.return_on_leave"); - - stmt.setString(1, ac.name); - stmt.setString(2, ac.owner != null ? ac.owner : ""); - stmt.setString(3, ac.world); - stmt.setInt(4, ac.x1); - stmt.setInt(5, ac.z1); - stmt.setInt(6, ac.x2); - stmt.setInt(7, ac.z2); - stmt.setInt(8, ac.yMin); - stmt.setInt(9, ac.yMax); - stmt.setString(10, String.join(",", ac.confiscateItems)); - stmt.setString(11, String.join(",", ac.denyUseItems)); - stmt.setString(12, effectsToString(ac.giveEffects)); - stmt.setString(13, String.join(",", ac.clearEffects)); - stmt.setInt(14, ac.clearAllBadEffects ? 1 : 0); - stmt.setString(15, String.join("|", ac.punishCommands)); - stmt.setInt(16, ac.denyBlockPlace ? 1 : 0); - stmt.setInt(17, ac.denyBlockBreak ? 1 : 0); - stmt.setInt(18, ac.denyPVP ? 1 : 0); - stmt.setInt(19, ac.denyFallDamage ? 1 : 0); - stmt.setInt(20, ac.denyHunger ? 1 : 0); - stmt.setInt(21, ac.denyAllDamage ? 1 : 0); - stmt.setInt(22, ac.allowDrop ? 1 : 0); - stmt.setInt(23, ac.denyMount ? 1 : 0); - stmt.setInt(24, ac.denyEnderPearl ? 1 : 0); - stmt.setInt(25, ac.denyBow ? 1 : 0); - stmt.setInt(26, ac.denyPotion ? 1 : 0); - stmt.setInt(27, ac.denyExplosion ? 1 : 0); - stmt.setInt(28, ac.denyRaid ? 1 : 0); - stmt.setInt(29, ac.denyFireSpread ? 1 : 0); - stmt.setInt(30, ac.denyAllEffects ? 1 : 0); - stmt.setInt(31, ac.denyItemFrame ? 1 : 0); - stmt.setInt(32, ac.denyEntityInteract ? 1 : 0); // ★ 修复:deny_entity_interact 列 - stmt.setInt(33, ac.denyMove ? 1 : 0); - stmt.setInt(34, ac.allowPickup ? 1 : 0); - stmt.setInt(35, ac.denyFire ? 1 : 0); - stmt.setInt(36, ac.peaceMode ? 1 : 0); - stmt.setInt(37, ac.peaceModeDuration / 1000); - stmt.setString(38, String.join(",", ac.peaceWhitelist)); - stmt.setString(39, ac.enforceGameMode != null ? ac.enforceGameMode : ""); - stmt.setString(40, String.join(",", ac.modeExempt)); - stmt.setString(41, ac.enterMsg); - stmt.setString(42, ac.leaveMsg); - stmt.setString(43, ac.confiscateMsg); - stmt.setInt(44, ac.enableAnnounce ? 1 : 0); - stmt.setString(45, ac.announceTemplate); - stmt.setString(46, ac.txtContent != null ? ac.txtContent : ""); - stmt.setLong(47, System.currentTimeMillis() / 1000); - stmt.setInt(48, ac.denyThrownProjectiles ? 1 : 0); - stmt.setInt(49, ac.denyGlowing ? 1 : 0); - stmt.setInt(50, ac.denyRedstoneInteraction ? 1 : 0); - stmt.setInt(51, ac.denyDoorInteraction ? 1 : 0); - stmt.setInt(52, ac.denyNoteblockJukebox ? 1 : 0); - stmt.setInt(53, ac.denyLead ? 1 : 0); - stmt.setInt(54, ac.denyCropHarvest ? 1 : 0); - stmt.setInt(55, ac.denyWoolShear ? 1 : 0); - stmt.setInt(56, ac.denyAnimalFeeding ? 1 : 0); - stmt.setDouble(57, ac.warpX); - stmt.setDouble(58, ac.warpY); - stmt.setDouble(59, ac.warpZ); - stmt.setFloat(60, ac.warpYaw); - stmt.setFloat(61, ac.warpPitch); - stmt.setString(62, ac.warpWorld != null ? ac.warpWorld : ""); - stmt.setInt(63, ac.denyContainer ? 1 : 0); - stmt.setInt(64, ac.denyMobAttack ? 1 : 0); - stmt.setInt(65, ac.denySignEdit ? 1 : 0); - stmt.setInt(66, ac.isPublicBuilding ? 1 : 0); - stmt.setInt(67, ac.allowVisitorTeleport ? 1 : 0); - stmt.setInt(68, ac.denyFarmlandTrample ? 1 : 0); - stmt.setInt(69, ac.denyEnderTeleport ? 1 : 0); - stmt.setInt(70, ac.createCost); - stmt.setInt(71, ac.denySpawnEgg ? 1 : 0); - stmt.setInt(72, ac.denyWax ? 1 : 0); - stmt.setString(73, effectsToString(ac.badEffects)); - stmt.setInt(74, ac.denyVillagerTrade ? 1 : 0); - stmt.setInt(75, ac.confiscateCount); - stmt.setInt(76, ac.returnOnLeave ? 1 : 0); - stmt.executeUpdate(); - stmt.close(); - // ★ 领地设置变更:立即触发PHP同步(防抖10秒) - try { - if (plugin.webManager != null) { - plugin.webManager.requestImmediateLandSync(); - } - } catch (Exception ignored) {} - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 保存到DB失败: " + ac.name + " - " + e.getMessage()); - } - } - - /** - * 从数据库删除领地 - */ - public void deleteAreaFromDb(String name) { - if (dbConnection == null) return; - try { - PreparedStatement stmt = dbConnection.prepareStatement("DELETE FROM area_lands WHERE name = ?"); - stmt.setString(1, name); - stmt.executeUpdate(); - stmt.close(); - // ★ 领地删除:立即触发PHP同步 - try { - if (plugin.webManager != null) { - plugin.webManager.requestImmediateLandSync(); - } - } catch (Exception ignored) {} - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 从DB删除领地失败: " + name + " - " + e.getMessage()); - } - } - - /** - * 获取所有领地数据(供WebManager同步到PHP) - */ - public List> getAllLandsForSync() { - List> result = new ArrayList<>(); - if (dbConnection == null) return result; - try { - Statement stmt = dbConnection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM area_lands"); - int cols = rs.getMetaData().getColumnCount(); - while (rs.next()) { - Map map = new HashMap<>(); - for (int i = 1; i <= cols; i++) { - String colName = rs.getMetaData().getColumnName(i); - map.put(colName, rs.getObject(colName)); - } - result.add(map); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 获取领地数据失败: " + e.getMessage()); - } - return result; - } - - /** - * 获取权限商店数据(供WebManager同步到PHP) - */ - public List> getPermissionShopForSync() { - List> result = new ArrayList<>(); - if (dbConnection == null) return result; - try { - Statement stmt = dbConnection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT s.id, s.land_id, l.name AS land_name, s.seller, s.permission, s.price, s.duration, s.status, s.buyer, s.bought_at, s.created_at " - + "FROM area_permission_shop s " - + "LEFT JOIN area_lands l ON s.land_id = l.id"); - while (rs.next()) { - Map map = new HashMap<>(); - map.put("id", rs.getInt("id")); - map.put("land_id", rs.getInt("land_id")); - map.put("land_name", rs.getString("land_name")); - map.put("seller", rs.getString("seller")); - map.put("permission", rs.getString("permission")); - map.put("price", rs.getInt("price")); - map.put("duration", rs.getInt("duration")); - map.put("status", rs.getString("status")); - map.put("buyer", rs.getString("buyer")); - map.put("bought_at", rs.getLong("bought_at")); - map.put("created_at", rs.getLong("created_at")); - result.add(map); - } - rs.close(); - stmt.close(); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 获取权限商店数据失败: " + e.getMessage()); - } - return result; - } - - /** - * 从数据库获取领地ID - */ - public int getLandIdFromDb(String name) { - if (dbConnection == null) return -1; - try { - PreparedStatement stmt = dbConnection.prepareStatement("SELECT id FROM area_lands WHERE name = ?"); - stmt.setString(1, name); - ResultSet rs = stmt.executeQuery(); - int id = rs.next() ? rs.getInt("id") : -1; - rs.close(); - stmt.close(); - return id; - } catch (SQLException e) { - return -1; - } - } - - /** - * 获取领地权限快照(用于过户cooldown期间检测权限变更) - * 返回JSON字符串,包含owner、members、visitors等关键字段 - */ - public String getLandPermissionsSnapshot(String landName) { - AreaConfig ac = areas.get(landName); - if (ac == null) return null; - try { - StringBuilder sb = new StringBuilder(); - sb.append("{\"owner\":\"").append(ac.owner != null ? ac.owner : "").append("\""); - // 成员列表(areaPlayerWhitelist) - sb.append(",\"members\":["); - Set members = areaPlayerWhitelist.get(landName); - if (members != null && !members.isEmpty()) { - java.util.List sorted = new java.util.ArrayList<>(members); - java.util.Collections.sort(sorted); - boolean first = true; - for (String m : sorted) { - if (!first) sb.append(","); - sb.append("\"").append(m).append("\""); - first = false; - } - } - sb.append("]"); - sb.append("}"); - return sb.toString(); - } catch (Exception e) { - return null; - } - } - - /** - * PHP端修改领地所有者时调用 - */ - public void setLandOwnerFromWeb(String landName, String newOwner) { - if (dbConnection == null) return; - try { - // ★ 幂等检查:如果所有者已相同则跳过,避免ack失败后重复打印 - AreaConfig ac = getLand(landName); - if (ac != null && samePlayer(newOwner, ac.owner)) { - return; - } - - // 更新数据库 - PreparedStatement ps = dbConnection.prepareStatement("UPDATE area_lands SET owner = ? WHERE name = ?"); - ps.setString(1, newOwner); - ps.setString(2, landName); - ps.executeUpdate(); - ps.close(); - - // 更新内存中的AreaConfig - if (ac != null) { - ac.owner = newOwner; - } - - plugin.getLogger().info("[防护] PHP端更新领地所有者: " + landName + " → " + newOwner); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 更新领地所有者失败: " + e.getMessage()); - } - } - - /** - * PHP端清除成员权限时调用 - */ - public void clearPlayerPermFromWeb(String landName, String playerName) { - if (dbConnection == null) return; - try { - int landId = getLandIdFromDb(landName); - if (landId < 0) return; - - // 清除数据库中的自定义权限(按 normName 匹配到的所有行,避免大小写变种残留) - for (PermRow row : findPermRows(landId, playerName)) { - PreparedStatement ps = dbConnection.prepareStatement( - "UPDATE area_land_permissions SET permissions = '' WHERE id = ?"); - ps.setInt(1, row.id); - ps.executeUpdate(); - ps.close(); - } - - plugin.getLogger().info("[防护] PHP端清除成员权限: " + playerName + " @ " + landName); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 清除成员权限失败: " + e.getMessage()); - } - } - - /** - * ★ PHP端修改领地字段时调用(效果管理等) - * field: give_effects, clear_effects, clear_all_bad, deny_all_effects等 - * value: 新值(已转为Java格式) - */ - public void updateLandFieldFromWeb(String landName, String field, String value) { - if (dbConnection == null) return; - try { - // 白名单校验字段名(防SQL注入) - java.util.Set allowedFields = new java.util.HashSet<>(java.util.Arrays.asList( - "give_effects", "bad_effects", "clear_effects", "clear_all_bad", "deny_all_effects", - "deny_block_place", "deny_block_break", "deny_pvp", "deny_fall_damage", - "deny_hunger", "deny_all_damage", "deny_drop", "deny_mount", "deny_ender_pearl", - "deny_bow", "deny_potion", "deny_explosion", "deny_raid", "deny_fire_spread", - "deny_all_effects", "deny_item_frame", "deny_entity_interact", "deny_move", "deny_pickup", "deny_fire", - "confiscate_items", "deny_use_items", "punish_commands", - "peace_mode", "peace_mode_duration", "enforce_game_mode", - "enter_msg", "leave_msg", "confiscate_msg", - "deny_thrown_projectiles", "deny_glowing", "deny_redstone_interaction", - "deny_door_interaction", "deny_noteblock_jukebox", "deny_lead", - "deny_crop_harvest", "deny_wool_shear", "deny_animal_feeding", - "deny_container", "deny_mob_attack", "deny_fluid", - "is_public_building", "allow_visitor_teleport", - "deny_spawn_egg", "deny_wax", "deny_villager_trade", - "confiscate_count", "return_on_leave" - )); - if (!allowedFields.contains(field)) { - plugin.getLogger().warning("[防护] PHP端更新未知字段: " + field); - return; - } - - PreparedStatement ps = dbConnection.prepareStatement( - "UPDATE area_lands SET " + field + " = ? WHERE name = ?"); - ps.setString(1, value); - ps.setString(2, landName); - int affected = ps.executeUpdate(); - ps.close(); - - if (affected > 0) { - // ★ 直接更新内存 AreaConfig 字段(reloadAreaConfigFromDb 只重载 area_config 全局表,不碰 area_lands) - AreaConfig ac = getLand(landName); - if (ac != null) { - applyFieldToAreaConfig(ac, field, value); - } - plugin.getLogger().info("[防护] PHP端更新领地字段: " + landName + "." + field + " = " + value); - } - } catch (SQLException e) { - plugin.getLogger().warning("[防护] PHP端更新领地字段失败: " + e.getMessage()); - } - } - - /** - * ★ 领地改名(带冷却检查) - * @return null=成功, 非null=错误/冷却信息 - */ - public String renameLand(Player p, String oldName, String newName) { - // 0. 权限校验:只有领地主/领地管理员可改名(纵深防御) - AreaConfig ownerAc = areas.get(oldName); - if (ownerAc != null - && !isAreaAdmin(p) - && !samePlayer(p.getName(), ownerAc.owner)) { - return "需要领地所有者或管理员权限"; - } - // 1. 基本校验(格式 + 特殊字符,防止按名字落盘的文件被覆盖或路径穿越) - if (newName == null || newName.trim().isEmpty()) return "新名字不能为空"; - newName = newName.trim(); - String fmtErr = validateAreaName(newName); - if (fmtErr != null) return fmtErr; - if (newName.equalsIgnoreCase(oldName)) return "新名字和旧名字相同"; - - // 2. 重名检查(不区分大小写:大小写变体会指向同一份文件) - if (areaNameExists(newName)) return "已存在同名领地: " + newName; - - // 3. 冷却检查 - long now = System.currentTimeMillis(); - java.util.List timestamps = renameCooldownMap.computeIfAbsent( - p.getUniqueId(), k -> new java.util.ArrayList<>()); - - // 清除过期记录(超出1小时窗口的) - timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); - - if (timestamps.size() >= RENAME_LIMIT) { - long firstInWindow = timestamps.get(0); - long unlockTime = firstInWindow + RENAME_WINDOW_MS; - long remainSec = (unlockTime - now) / 1000; - long remainMin = remainSec / 60; - long remainSecR = remainSec % 60; - return "改名冷却中!本小时已改名" + RENAME_LIMIT + "次," - + remainMin + "分" + remainSecR + "秒后解锁"; - } - - // 4. 执行改名 - try { - if (dbConnection == null || dbConnection.isClosed()) return "数据库未连接"; - - // 4a. 更新 area_lands 表的 name - PreparedStatement ps = dbConnection.prepareStatement( - "UPDATE area_lands SET name = ? WHERE name = ?"); - ps.setString(1, newName); - ps.setString(2, oldName); - int affected = ps.executeUpdate(); - ps.close(); - - if (affected <= 0) return "数据库中未找到领地: " + oldName; - - // 4b. 更新 area_land_permissions 表的 land_id(如果用name关联) - // land_id 是 area_lands 的自增 id,改名不影响 id,所以无需更新 - - // 4c. 更新内存 - AreaConfig ac = areas.remove(oldName); - if (ac != null) { - ac.name = newName; - areas.put(newName, ac); - } - - // 4d. 更新内存中的成员权限缓存等(如果有以旧名字为key的缓存) - // areaPlayerWhitelist 也以 landName 为 key,需同步 - java.util.Set whitelist = areaPlayerWhitelist.remove(oldName); - if (whitelist != null) { - areaPlayerWhitelist.put(newName, whitelist); - } - - // 4e. 记录冷却 - timestamps.add(now); - - // 4f. 触发PHP同步 - try { - plugin.webManager.syncLandData(); - } catch (Exception ignored) {} - - plugin.getLogger().info("[防护] 领地改名: " + oldName + " → " + newName + " (by " + p.getName() + ")"); - return null; // 成功 - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 领地改名失败: " + e.getMessage()); - return "数据库错误: " + e.getMessage(); - } - } - - /** - * ★ 获取改名冷却信息(用于显示在菜单中) - * @return [剩余秒数, 已用次数] 或 null(无冷却) - */ - public long[] getRenameCooldown(Player p) { - long now = System.currentTimeMillis(); - java.util.List timestamps = renameCooldownMap.get(p.getUniqueId()); - if (timestamps == null || timestamps.isEmpty()) return null; - - // 清除过期 - timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); - - int used = timestamps.size(); - if (used == 0) return null; - - // ★ 仅在达到上限时才提示冷却,未达上限时只显示剩余次数 - if (used < RENAME_LIMIT) return null; - - long firstInWindow = timestamps.get(0); - long unlockTime = firstInWindow + RENAME_WINDOW_MS; - if (now >= unlockTime) return null; // 已解锁 - - long remainSec = (unlockTime - now) / 1000; - return new long[]{remainSec, used}; - } - - /** - * 获取剩余改名次数 - */ - public int getRemainingRenames(Player p) { - long now = System.currentTimeMillis(); - java.util.List timestamps = renameCooldownMap.get(p.getUniqueId()); - if (timestamps == null) return RENAME_LIMIT; - timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); - return Math.max(0, RENAME_LIMIT - timestamps.size()); - } - - /** - * 将 PHP 端同步过来的单个字段值直接写入内存 AreaConfig 对象 - */ - private void applyFieldToAreaConfig(AreaConfig ac, String field, String value) { - switch (field) { - // boolean 权限字段 (DB int 0/1 → boolean) - case "deny_block_place": ac.denyBlockPlace = "1".equals(value); break; - case "deny_block_break": ac.denyBlockBreak = "1".equals(value); break; - case "deny_container": ac.denyContainer = "1".equals(value); break; - case "deny_sign_edit": ac.denySignEdit = "1".equals(value); break; - case "deny_pvp": ac.denyPVP = "1".equals(value); break; - case "deny_fall_damage": ac.denyFallDamage = "1".equals(value); break; - case "deny_hunger": ac.denyHunger = "1".equals(value); break; - case "deny_all_damage": ac.denyAllDamage = "1".equals(value); break; - case "deny_mount": ac.denyMount = "1".equals(value); break; - case "deny_ender_pearl": ac.denyEnderPearl = "1".equals(value); break; - case "deny_bow": ac.denyBow = "1".equals(value); break; - case "deny_potion": ac.denyPotion = "1".equals(value); break; - case "deny_explosion": ac.denyExplosion = "1".equals(value); break; - case "deny_move": ac.denyMove = "1".equals(value); break; - case "deny_fire": ac.denyFire = "1".equals(value); break; - case "deny_fire_spread": ac.denyFireSpread = "1".equals(value); break; - case "deny_all_effects": ac.denyAllEffects = "1".equals(value); break; - case "deny_raid": ac.denyRaid = "1".equals(value); break; - case "deny_thrown_projectiles": ac.denyThrownProjectiles = "1".equals(value); break; - case "deny_glowing": ac.denyGlowing = "1".equals(value); break; - case "deny_redstone_interaction": ac.denyRedstoneInteraction = "1".equals(value); break; - case "deny_door_interaction": ac.denyDoorInteraction = "1".equals(value); break; - case "deny_noteblock_jukebox": ac.denyNoteblockJukebox = "1".equals(value); break; - case "deny_lead": ac.denyLead = "1".equals(value); break; - case "deny_crop_harvest": ac.denyCropHarvest = "1".equals(value); break; - case "deny_wool_shear": ac.denyWoolShear = "1".equals(value); break; - case "deny_animal_feeding": ac.denyAnimalFeeding = "1".equals(value); break; - case "deny_spawn_egg": ac.denySpawnEgg = "1".equals(value); break; - case "deny_wax": ac.denyWax = "1".equals(value); break; - case "deny_mob_attack": ac.denyMobAttack = "1".equals(value); break; - case "deny_item_frame": ac.denyItemFrame = "1".equals(value); break; - case "deny_villager_trade": ac.denyVillagerTrade = "1".equals(value); break; - case "return_on_leave": ac.returnOnLeave = "1".equals(value); break; - case "confiscate_count": - try { ac.confiscateCount = Integer.parseInt(value); } catch (Exception ignored) {} - break; - case "deny_entity_interact": ac.denyEntityInteract = "1".equals(value); break; - case "deny_pickup": ac.allowPickup = "0".equals(value); break; // deny_pickup 反向 - case "deny_drop": ac.allowDrop = "0".equals(value); break; // deny_drop 反向 - case "is_public_building": ac.isPublicBuilding = "1".equals(value); break; - case "allow_visitor_teleport": ac.allowVisitorTeleport = "1".equals(value); break; - // boolean 配置字段 - case "peace_mode": ac.peaceMode = "1".equals(value); break; - case "clear_all_bad": ac.clearAllBadEffects = "1".equals(value); break; - // int 字段 - case "peace_mode_duration": - try { ac.peaceModeDuration = Integer.parseInt(value); } catch (Exception ignored) {} - break; - // String 字段 - case "enter_msg": ac.enterMsg = value; break; - case "leave_msg": ac.leaveMsg = value; break; - case "confiscate_msg": ac.confiscateMsg = value; break; - case "enforce_game_mode": ac.enforceGameMode = value; break; - // 复合字段(JSON 数组或管道格式 → List) - // ★ 2026-10-04:负面效果拆到 bad_effects,PHP 推来的字段名同样是 bad_effects - case "give_effects": - case "bad_effects": { - boolean isBad = "bad_effects".equals(field); - List parsed = parseEffectsString(value); - // ★ 数量上限截断:增益与负面共用同一份 max_effects 预算(按合计算) - try { - int maxEff = getMaxGiveEffects(ac.owner); - int otherCount = isBad ? ac.giveEffects.size() : ac.badEffects.size(); - int room = Math.max(0, maxEff - otherCount); - if (parsed.size() > room) { - plugin.getLogger().warning("[防护] 领地 " - + ac.name + (isBad ? " 负面效果 " : " 增益效果 ") - + parsed.size() + " 个超出上限 " - + maxEff + "(另一类已占 " + otherCount - + "),已截断保留前 " + room + " 个"); - parsed = new java.util.ArrayList<>(parsed.subList(0, room)); - } - // ★ 药效等级封顶(2026-10-04 任务4):逐个效果按「生存可获取上限 + 用户组配置」 - for (String[] ef : parsed) { - if (ef.length >= 2) { - try { - int lv = Integer.parseInt(ef[1]); - int capped = clampEffectLevel(lv, ef[0], ac.owner); - if (capped != lv) { - plugin.getLogger().info("[防护] 领地 " + ac.name + " 效果 " - + ef[0] + " 等级 " + lv + " 超出上限,已封顶为 " + capped); - ef[1] = String.valueOf(capped); - } - } catch (NumberFormatException ignored) {} - } - } - } catch (Exception ex) { - plugin.getLogger().warning("[防护] " + field + " 上限校验异常: " - + ex.getMessage()); - } - if (isBad) { - ac.badEffects = parsed; - } else { - ac.giveEffects = parsed; - } - break; - } - case "clear_effects": - ac.clearEffects.clear(); - if (value != null && !value.isEmpty()) { - ac.clearEffects.addAll(splitToList(value)); - } - break; - default: - plugin.getLogger().warning("[防护] applyFieldToAreaConfig: 未处理字段 " + field); - break; - } - } - - /** - * 从 ["name","level","dur"] 格式提取效果数组 - */ - private String[] parseEffectJson(String s) { - try { - String inner = s.trim(); - if (inner.startsWith("[") && inner.endsWith("]")) inner = inner.substring(1, inner.length() - 1); - String[] parts = inner.split(","); - String name = parts[0].trim().replaceAll("^\"|\"$", ""); - String lvl = parts.length > 1 ? parts[1].trim() : "1"; - String dur = parts.length > 2 ? parts[2].trim() : "60"; - return new String[]{name, lvl, dur}; - } catch (Exception e) { - return new String[]{"", "1", "60"}; - } - } - - /** - * PHP端更新访客/成员权限时调用 - * landName 可能是JSON数组格式 "[领地名]" 或纯领地名 - */ - /** - * 从Web端(PHP)同步访客权限到本地库。 - * @param role 角色标识。为空表示仅调整权限(不改动角色,已授权的管理员不会被降级); - * "admin"/"visitor" 表示显式切换角色(来自 change_visitor_role)。 - */ - public void updateVisitorPermFromWeb(String landName, String playerName, String permsJson, String role) { - if (dbConnection == null) return; - try { - // 解析可能的JSON数组格式 - String[] landNames = landName.startsWith("[") ? parseJsonArray(landName) : new String[]{landName}; - - // 防御:清除PHP双重JSON编码引入的反斜杠(JSON中 uXXXX 被双重转义后残留在领地名中) - for (int i = 0; i < landNames.length; i++) { - if (landNames[i].contains("\\")) { - landNames[i] = landNames[i].replace("\\", ""); - } - } - - for (String lname : landNames) { - int landId = getLandIdFromDb(lname); - if (landId < 0) continue; - - // ★ 角色同步铁律:admin 永远不允许被 PHP 的 perm_change 降级。 - // PHP 的 handleUpdateVisitorPerm 本身已拦截 admin/member, - // 但竞态(admin 在 Java 设置后、同步到 PHP 前,PHP 已发 perm_change) - // 仍可能导致此方法收到 role="visitor" 降级请求。 - // 解决:admin 只能通过 change_visitor_role(显式角色切换)改变, - // perm_change 只更新 permissions JSON,不动 role。 - String effectiveRole = "visitor"; - boolean isAdmin = false; - { - String localRole = getLocalRole(landId, playerName); - if (localRole != null && !localRole.isEmpty()) { - effectiveRole = localRole; - } - isAdmin = "admin".equalsIgnoreCase(effectiveRole); - } - - if (role != null && !role.isEmpty()) { - // ★ PHP显式携带role(来自changeVisitorRole):允许任何角色切换 - effectiveRole = role.toLowerCase(); - if ("admin".equalsIgnoreCase(role)) { - setLandAdmin(lname, playerName, true); - } else if ("member".equals(effectiveRole) || "visitor".equals(effectiveRole)) { - // 降级:取消管理员标记 - setLandAdmin(lname, playerName, false); - } - } else if (isAdmin) { - // 无显式role + 本地是admin:保留admin角色,只更新permissions - effectiveRole = "admin"; - } - // role 为空或 visitor + 非 admin:effectiveRole 保持 localRole 或默认 visitor - - plugin.getLogger().info("[防护][更新权限] 入参 land=" + lname + " player=" + playerName - + " role=" + role + " localRole=" + getLocalRole(landId, playerName) - + " effectiveRole=" + effectiveRole - + " permsLen=" + (permsJson == null ? "null" : String.valueOf(permsJson.length())) - + (role != null && !role.isEmpty() ? " (PHP显式携带role)" : " (沿用本地)")); - - // 更新数据库中的自定义权限(同时写回 effectiveRole,确保新记录角色正确) - // ★ 先按 normName 定位已有行再 UPDATE/INSERT,避免大小写/变种字符 - // 与 UNIQUE(land_id, player_name) 的 BINARY 约束不一致而产生重复行 - PermRow existing = findPermRow(landId, playerName); - if (existing != null) { - PreparedStatement ps = dbConnection.prepareStatement( - "UPDATE area_land_permissions SET permissions = ?, role = ?, granted_at = ? WHERE id = ?"); - ps.setString(1, permsJson); - ps.setString(2, effectiveRole); - ps.setLong(3, System.currentTimeMillis() / 1000); - ps.setInt(4, existing.id); - ps.executeUpdate(); - ps.close(); - } else { - PreparedStatement ps = dbConnection.prepareStatement( - "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + - "VALUES (?, ?, ?, ?, ?)"); - ps.setInt(1, landId); - ps.setString(2, playerName); - ps.setString(3, effectiveRole); - ps.setString(4, permsJson); - ps.setLong(5, System.currentTimeMillis() / 1000); - ps.executeUpdate(); - ps.close(); - } - } - - plugin.getLogger().info("[防护] PHP端更新权限: " + playerName + " @ " + Arrays.toString(landNames)); - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 更新权限失败: " + e.getMessage()); - } - } - - /** - * 查询本地 area_land_permissions 表中指定玩家在当前领地的角色 - */ - private String getLocalRole(int landId, String playerName) { - PermRow row = findPermRow(landId, playerName); - return row == null ? null : row.role; - } - - /** - * 解析JSON数组格式的领地名字符串 - */ - private String[] parseJsonArray(String json) { - try { - String inner = json.replaceAll("^\\[|\\]$", ""); - String[] parts = inner.split("\""); - java.util.List result = new java.util.ArrayList<>(); - for (String part : parts) { - String trimmed = part.trim().replaceAll("§[0-9a-fk-or]", ""); - if (!trimmed.isEmpty()) result.add(trimmed); - } - return result.toArray(new String[0]); - } catch (Exception e) { - return new String[]{json}; - } - } - - /** - * 格式化时长(秒 → 可读字符串) - */ - private String formatDuration(int seconds) { - if (seconds >= 86400) return (seconds / 86400) + "天"; - if (seconds >= 3600) return (seconds / 3600) + "小时"; - if (seconds >= 60) return (seconds / 60) + "分钟"; - return seconds + "秒"; - } - - /** - * 更新area_config表中的配置值 - */ - private void updateAreaConfig(String key, String value) { - if (dbConnection == null) return; - // ★ 去除前导零:01 → 1,防止静默失败 - try { value = String.valueOf(Integer.parseInt(value.trim())); } catch (Exception ignored) {} - try { - PreparedStatement ps = dbConnection.prepareStatement( - "INSERT INTO area_config (key, value) VALUES (?, ?) " - + "ON CONFLICT(key) DO UPDATE SET value = ?"); - ps.setString(1, key); - ps.setString(2, value); - ps.setString(3, value); - ps.executeUpdate(); - ps.close(); - // ★ 同步更新内存字段,使Web配置变更立即生效(无需等待下次轮询reload) - try { - switch (key) { - case "create_price_per_sqm": globalCreatePricePerSqm = Integer.parseInt(value); break; - case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(value); break; - case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(value); break; - case "default_height": globalDefaultHeight = Integer.parseInt(value); break; - case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(value); break; - case "max_free_area_sqm": globalMaxFreeAreaSqm = Integer.parseInt(value); break; - case "max_land_side_blocks": globalMaxLandSideBlocks = Integer.parseInt(value); break; - } - } catch (NumberFormatException ignored) {} - } catch (Exception e) { - plugin.getLogger().warning("[防护] 更新配置失败: " + e.getMessage()); - } - } - - /** - * ★ 获取全局家(home)数量上限 - */ - public int getGlobalMaxHomePerPlayer() { - return globalMaxHomePerPlayer; - } - - /** - * ★ 获取area_config表中的配置值 - */ - public String getAreaConfigValue(String key) { - if (dbConnection == null) return null; - try { - PreparedStatement ps = dbConnection.prepareStatement("SELECT value FROM area_config WHERE key = ?"); - ps.setString(1, key); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - String val = rs.getString("value"); - rs.close(); - ps.close(); - return val; - } - rs.close(); - ps.close(); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 读取配置失败: " + e.getMessage()); - } - return null; - } - - /** - * ★ 设置area_config表中的配置值(公开方法) - */ - public void setAreaConfigValue(String key, String value) { - updateAreaConfig(key, value); - } - - /** - * ★ 获取全部area_config配置(用于同步到PHP) - */ - public Map getAllAreaConfigForSync() { - Map config = new LinkedHashMap<>(); - if (dbConnection == null) return config; - try { - ResultSet rs = dbConnection.createStatement().executeQuery("SELECT key, value FROM area_config"); - while (rs.next()) { - config.put(rs.getString("key"), rs.getString("value")); - } - rs.close(); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 读取配置同步数据失败: " + e.getMessage()); - } - return config; - } - - // ==================== 管理员配置输入(GUI→聊天栏) ==================== - - /** - * ★ 设置等待配置输入状态 - */ - public void setPendingConfigInput(java.util.UUID uuid, String configKey) { - pendingConfigInput.put(uuid, configKey); - } - - /** - * ★ 检查并处理配置输入(在onPlayerChat中调用) - * @return true 如果是配置输入,已处理 - */ - public boolean handleConfigInput(Player p, String message) { - String configKey = pendingConfigInput.remove(p.getUniqueId()); - if (configKey == null) return false; - - // ★ 智能数字解析:支持纯数字、中文数字、中文大写、罗马数字、英文数字 - Integer newValue = parseSmartNumber(message.trim()); - if (newValue == null) { - p.sendMessage("§c§l[配置] §f无法识别输入的数字: " + message); - p.sendMessage("§7支持: 纯数字123、中文一二三、大写壹佰贰拾叁、罗马I II III、英文one two three"); - return true; - } - - // ★ 退费比例范围钳制:0=关闭,100=全额 - if ("refund_ratio_per_cent".equals(configKey)) { - if (newValue > 100) newValue = 100; - if (newValue < 0) newValue = 0; - } - - // 读取当前值 - int currentValue = 0; - try { - String v = getAreaConfigValue(configKey); - if (v != null) currentValue = Integer.parseInt(v); - } catch (Exception ignored) {} - - // 应用新值(如果比原来大=加钱,比原来小=减钱,直接使用绝对值) - setAreaConfigValue(configKey, String.valueOf(newValue)); - - // 更新内存变量 - switch (configKey) { - case "create_price_per_sqm": globalCreatePricePerSqm = newValue; break; - case "max_lands_per_player": globalMaxLandsPerPlayer = newValue; break; - case "default_height": globalDefaultHeight = newValue; break; - case "refund_ratio_per_cent": globalRefundRatioPerCent = newValue; break; - case "max_free_area_sqm": globalMaxFreeAreaSqm = Math.max(1, newValue); break; - case "max_land_side_blocks": globalMaxLandSideBlocks = Math.max(1, newValue); break; - } - - String configName = getConfigNameByKey(configKey); - p.sendMessage("§a§l[配置] §f" + configName + " 已更新: §e" + currentValue + " → " + newValue); - - // 刷新GUI(必须切主线程:handleConfigInput来自AsyncPlayerChatEvent) - if (plugin.areaGUIManager != null) { - Sdf1_login.Main pl = plugin; - org.bukkit.entity.Player pp = p; - org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openAdminPanel(pp)); - } - return true; - } - - /** - * 设置等待添加成员输入状态 - */ - public void setPendingAddMemberInput(java.util.UUID uuid, String landName) { - pendingAddMemberInput.put(uuid, landName); - } - - /** - * 检查并处理添加成员输入 - * @return true 如果是添加成员输入,已处理 - */ - public boolean handleAddMemberInput(Player p, String message) { - String landName = pendingAddMemberInput.remove(p.getUniqueId()); - if (landName == null) return false; - - String playerName = message.trim(); - // 验证玩家名格式 - if (!playerName.matches("^[a-zA-Z0-9_]{3,16}$")) { - p.sendMessage("§c§l[添加成员] §f无效的玩家名格式(3-16位字母数字下划线): " + playerName); - p.sendMessage("§7请重新输入有效玩家名,或输入 cancel 取消"); - return true; - } - - // 检查玩家是否已存在(本地) - Player target = Bukkit.getPlayerExact(playerName); - if (target == null) { - // 玩家不在线,但仍然可以添加(因为可能在数据库中有记录) - p.sendMessage("§e§l[添加成员] §f玩家 " + playerName + " 当前不在线,但仍可添加为成员"); - } - - // 检查是否已是成员 - Set members = getLandMembers(landName); - if (members.contains(playerName)) { - p.sendMessage("§c§l[添加成员] §f" + playerName + " 已经是该领地成员"); - return true; - } - - // 检查是否是领地主 - AreaConfig ac = getLand(landName); - if (ac != null && ac.owner != null && samePlayer(ac.owner, playerName)) { - p.sendMessage("§c§l[添加成员] §f领地主不能作为成员添加"); - return true; - } - - // ★ 校验玩家是否在login.db中注册(防止添加不存在的玩家) - DatabaseManager dbMgr = plugin.getDb(); - if (dbMgr != null && !dbMgr.userExists(playerName)) { - p.sendMessage("§c§l[添加成员] §f玩家 " + playerName + " 不存在(未在login.db中注册)"); - p.sendMessage("§7请确认玩家名是否正确,或输入 cancel 取消"); - return true; - } - - // 添加成员 - addLandMember(landName, playerName); - insertLandPermission(landName, playerName, "visitor"); - p.sendMessage("§a§l[添加成员] §f已成功添加 " + playerName + " 为领地成员"); - - // 刷新GUI(必须切主线程) - if (plugin.areaGUIManager != null) { - Sdf1_login.Main pl = plugin; - org.bukkit.entity.Player pp = p; - String ln = landName; - org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openAddMember(pp, ln)); - } - return true; - } - - /** - * 设置等待效果管理输入状态 - * @param subPage 子页面索引(1=开关, 2=单清, 3=增益) - */ - public void setPendingClearEffectInput(java.util.UUID uuid, String landName, String inputType, int subPage) { - pendingEffectInput.put(uuid, new String[]{landName, inputType, String.valueOf(subPage)}); - } - - /** - * ★ 通用设置效果输入状态 - */ - public void setPendingEffectInput(java.util.UUID uuid, String landName, String inputType, int subPage) { - pendingEffectInput.put(uuid, new String[]{landName, inputType, String.valueOf(subPage)}); - } - - /** - * 检查并处理效果管理输入 - * @return true 如果是效果管理输入,已处理 - */ - public boolean handleEffectInput(Player p, String message) { - String[] landAndType = pendingEffectInput.remove(p.getUniqueId()); - if (landAndType == null) return false; - - String landName = landAndType[0]; - String inputType = landAndType[1]; - AreaProtection.AreaConfig land = getLand(landName); - if (land == null) { - p.sendMessage("§c领地不存在"); - return true; - } - - // 获取subPage(如果有) - int subPage = 1; - if (landAndType.length >= 3) { - try { - subPage = Integer.parseInt(landAndType[2]); - } catch (NumberFormatException ignored) { - } - } - - if ("clear".equals(inputType)) { - // 添加单清效果 - String effName = message.trim(); - if (effName.isEmpty()) { - p.sendMessage("§c§l[添加清除效果] §f效果名不能为空"); - return true; - } - // ★ 验证效果名是否有效 - PotionEffectType resolved = resolveEffectType(effName); - if (resolved == null) { - p.sendMessage("§c§l[添加清除效果] §f无效的效果名: §e" + effName); - p.sendMessage("§7支持的中文名: 缓慢、挖掘疲劳、瞬间伤害、反胃、失明、饥饿、虚弱、中毒、凋零、飘浮、霉运、黑暗、蓄风、盘丝、渗浆、寄生"); - p.sendMessage("§7中性: 不祥之兆、袭击之兆、试炼之兆"); - p.sendMessage("§7正面: 迅捷、急迫、力量、瞬间治疗、跳跃提升、生命恢复、抗性提升、抗火、水下呼吸、隐身、夜视、发光、生命提升、伤害吸收、饱和、幸运、村庄英雄、缓降、潮涌能量、海豚的恩惠"); - return true; - } - if (!land.clearEffects.contains(effName)) { - land.clearEffects.add(effName); - saveAreaToDb(land); - p.sendMessage("§a§l[添加清除效果] §f已添加: §e" + effName); - } else { - p.sendMessage("§e§l[添加清除效果] §f效果 §e" + effName + " §f已在列表中"); - } - } else if ("give".equals(inputType)) { - // 添加增益效果: 格式 效果名 [等级] [秒数] - String[] parts = message.trim().split("\\s+"); - if (parts.length < 1) { - p.sendMessage("§c§l[添加增益效果] §f请输入: 效果名 [等级] [秒数]"); - return true; - } - String effName = parts[0].trim(); - int effLv = 1; - int effDur = 300; // 默认5分钟 - try { - if (parts.length >= 2) effLv = Integer.parseInt(parts[1]); - if (parts.length >= 3) effDur = Integer.parseInt(parts[2]); - } catch (NumberFormatException ignored) { - p.sendMessage("§c§l[添加增益效果] §f等级和秒数必须是数字"); - return true; - } - // ★ 验证等级和秒数范围(等级上限按「该效果生存可获取上限 + 用户组配置」,2026-10-04 任务4) - int maxLvAdd = getEffectLevelCap(effName, land.owner); - if (effLv < 1 || effLv > maxLvAdd) { - p.sendMessage("§c§l[添加增益效果] §f§e" + effName + " §f等级范围: 1~" + maxLvAdd); - p.sendMessage("§7上限说明: " + effName + " 生存模式最高可获得 " + getSurvivalMaxLevel(effName) + " 级"); - p.sendMessage("§7你当前领地可用上限 " + maxLvAdd + " 级(受用户组配置约束)"); - return true; - } - if (effDur < 1 || effDur > 3600) { - p.sendMessage("§c§l[添加增益效果] §f持续时间范围: 1~3600秒"); - return true; - } - // ★ 验证效果名是否有效 - PotionEffectType resolved = resolveEffectType(effName); - if (resolved == null) { - p.sendMessage("§c§l[添加增益效果] §f无效的效果名: §e" + effName); - p.sendMessage("§7推荐增益: 迅捷、急迫、力量、瞬间治疗、跳跃提升、生命恢复、抗性提升、抗火、水下呼吸、隐身、夜视、发光、生命提升、伤害吸收、饱和、幸运、村庄英雄、缓降、潮涌能量、海豚的恩惠"); - return true; - } - // ★ 检查是否已存在同名增益 - for (String[] existing : land.giveEffects) { - if (existing[0].equals(effName)) { - p.sendMessage("§e§l[添加增益效果] §f增益 §e" + effName + " §f已存在,先移除旧的再添加"); - return true; - } - } - // ★ 数量上限(2026-10-04 任务3):普通玩家 5 个,用户组可覆盖 - // 2026-10-04 负面拆出后,增益与负面共用同一份 max_effects 预算,故按合计判 - int maxEffCnt = getMaxGiveEffects(land.owner); - if (land.giveEffects.size() + land.badEffects.size() >= maxEffCnt) { - p.sendMessage("§c§l[添加增益效果] §f效果已达上限 §e" - + maxEffCnt + " §f个(增益+负面合计,按用户组配置),请先移除再添加"); - return true; - } - String[] effRecord = {effName, String.valueOf(effLv), String.valueOf(effDur)}; - land.giveEffects.add(effRecord); - saveAreaToDb(land); - p.sendMessage("§a§l[添加增益效果] §f已添加: §a" + effName + " §fLv" + effLv + " " + effDur + "秒"); - } else if (inputType.startsWith("editGive_")) { - // ★ 编辑增益效果等级/时长: editGive_level_3 或 editGive_duration_3 - String[] parts = inputType.split("_"); - if (parts.length < 3) return true; - String editField = parts[1]; // level 或 duration - int idx; - try { idx = Integer.parseInt(parts[2]); } catch (Exception e) { return true; } - if (idx < 1 || idx > land.giveEffects.size()) { - p.sendMessage("§c序号超出范围"); - return true; - } - if (message.trim().equalsIgnoreCase("取消")) { - p.sendMessage("§7已取消编辑"); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3); - return true; - } - String[] eff = land.giveEffects.get(idx - 1); - if ("level".equals(editField)) { - int newLevel; - try { newLevel = Integer.parseInt(message.trim()); } catch (Exception e) { - p.sendMessage("§c请输入有效数字"); - return true; - } - int maxLvEdit = getEffectLevelCap(eff[0], land.owner); - if (newLevel < 1 || newLevel > maxLvEdit) { - p.sendMessage("§c等级范围 1~" + maxLvEdit); - p.sendMessage("§7上限说明: " + eff[0] + " 生存模式最高可获得 " - + getSurvivalMaxLevel(eff[0]) + " 级"); - p.sendMessage("§7你当前领地可用上限 " + maxLvEdit + " 级(受用户组配置约束)"); - return true; - } - if (eff.length < 2) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, eff.length); eff = tmp; land.giveEffects.set(idx - 1, eff); } - eff[1] = String.valueOf(newLevel); - p.sendMessage("§a§l[编辑增益] §f已将 §e" + eff[0] + " §f等级修改为 " + newLevel); - } else { - int newDur; - try { newDur = Integer.parseInt(message.trim()); } catch (Exception e) { - p.sendMessage("§c请输入有效数字"); - return true; - } - if (newDur < 1 || newDur > 86400) { p.sendMessage("§c时长范围1~86400秒"); return true; } - if (eff.length < 3) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, Math.min(eff.length, 3)); eff = tmp; land.giveEffects.set(idx - 1, eff); } - eff[2] = String.valueOf(newDur); - p.sendMessage("§a§l[编辑增益] §f已将 §e" + eff[0] + " §f时长修改为 " + newDur + "秒"); - } - saveAreaToDb(land); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3); - } else if (inputType.startsWith("editBad_")) { - // ★ 编辑负面效果等级/时长: editBad_level_3 或 editBad_duration_3 - // (2026-10-04 负面从增益拆出,回写落到 badEffects 而不是 giveEffects) - String[] parts = inputType.split("_"); - if (parts.length < 3) return true; - String editField = parts[1]; // level 或 duration - int idx; - try { idx = Integer.parseInt(parts[2]); } catch (Exception e) { return true; } - if (idx < 1 || idx > land.badEffects.size()) { - p.sendMessage("§c序号超出范围"); - return true; - } - if (message.trim().equalsIgnoreCase("取消")) { - p.sendMessage("§7已取消编辑"); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 6); - return true; - } - String[] eff = land.badEffects.get(idx - 1); - if ("level".equals(editField)) { - int newLevel; - try { newLevel = Integer.parseInt(message.trim()); } catch (Exception e) { - p.sendMessage("§c请输入有效数字"); - return true; - } - int maxLvEdit = getEffectLevelCap(eff[0], land.owner); - if (newLevel < 1 || newLevel > maxLvEdit) { - p.sendMessage("§c等级范围 1~" + maxLvEdit); - p.sendMessage("§7上限说明: " + eff[0] + " 生存模式最高可获得 " - + getSurvivalMaxLevel(eff[0]) + " 级"); - p.sendMessage("§7你当前领地可用上限 " + maxLvEdit + " 级(受用户组配置约束)"); - return true; - } - if (eff.length < 2) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, eff.length); eff = tmp; land.badEffects.set(idx - 1, eff); } - eff[1] = String.valueOf(newLevel); - p.sendMessage("§c§l[编辑负面] §f已将 §e" + eff[0] + " §f等级修改为 " + newLevel); - } else { - int newDur; - try { newDur = Integer.parseInt(message.trim()); } catch (Exception e) { - p.sendMessage("§c请输入有效数字"); - return true; - } - if (newDur < 1 || newDur > 86400) { p.sendMessage("§c时长范围1~86400秒"); return true; } - if (eff.length < 3) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, Math.min(eff.length, 3)); eff = tmp; land.badEffects.set(idx - 1, eff); } - eff[2] = String.valueOf(newDur); - p.sendMessage("§c§l[编辑负面] §f已将 §e" + eff[0] + " §f时长修改为 " + newDur + "秒"); - } - saveAreaToDb(land); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 6); - } else if ("rename_land".equals(inputType)) { - // ★ 领地改名输入处理 - if (message.trim().equals("取消") || message.trim().equals("0")) { - p.sendMessage(Component.text("§e已取消改名,保持原样")); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, landName, 1); - return true; - } - // ★ URL过滤:包含链接则拒绝,不计入改名次数 - ChatFilterManager cf = plugin.getChatFilter(); - if (cf != null) { - java.util.List urls = cf.extractUrls(message); - if (!urls.isEmpty()) { - p.sendMessage(Component.text("§c领地名不能包含链接!")); - pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"}); - return true; - } - } - String newName = message.trim(); - String error = renameLand(p, landName, newName); - if (error != null) { - p.sendMessage(Component.text("§c改名失败: " + error)); - p.sendMessage(Component.text("§7请重新输入(或输入 §c取消§7)")); - pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"}); - return true; - } - p.sendMessage(Component.text("§a✓ 领地已改名: §f" + landName + " §a→ §f" + newName)); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, newName, 1); - return true; - } else if ("confiscate_add".equals(inputType)) { - // ★ 违禁品管理面板:添加访客禁带物品(2026-10-11) - if (message.trim().equals("取消")) { - p.sendMessage(Component.text("§e已取消添加,保持原样")); - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName); - return true; - } - String itemId = message.trim(); - if (itemId.isEmpty()) { - p.sendMessage(Component.text("§c§l[添加禁带物品] §f物品ID不能为空")); - pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"}); - return true; - } - // ★ 翻译中间人:支持中文名(钻石)与英文ID(diamond、minecraft:gold_ingot)(2026-10-11) - StringBuilder cnHint = new StringBuilder(); - org.bukkit.Material mat = ItemNameResolver.resolveWithHint(itemId, cnHint); - if (mat == null) { - p.sendMessage(Component.text("§c§l[添加禁带物品] §f无效的物品: §e" + itemId)); - p.sendMessage(Component.text("§7支持中文: 钻石、下界合金剑、黑曜石")); - p.sendMessage(Component.text("§7也支持英文: diamond、minecraft:gold_ingot、netherite_sword")); - pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"}); - return true; - } - // 统一为小写标准名,避免重复 - String stdId = mat.name().toLowerCase(); - if (land.confiscateItems.contains(stdId)) { - p.sendMessage(Component.text("§e§l[添加禁带物品] §f物品 §e" + stdId + " §f已在列表中")); - } else { - land.confiscateItems.add(stdId); - saveAreaToDb(land); - String matched = cnHint.length() > 0 - ? " §7(匹配到: " + cnHint + ")" : ""; - p.sendMessage(Component.text("§a§l[添加禁带物品] §f已添加访客禁带物品: §e" + stdId + matched)); - } - if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName); - return true; - } - - // 刷新GUI,回到原来的subPage(必须切主线程) - if (plugin.areaGUIManager != null) { - Sdf1_login.Main pl = plugin; - org.bukkit.entity.Player pp = p; - String ln = landName; - int sp = subPage; - org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openEffectsManagement(pp, ln, sp)); - } - return true; - } - - /** - * ★ 检查并处理过户领地输入(新所有者名) - * @return true 如果是过户输入,已处理 - */ - public boolean handleTransferInput(Player p, String message) { - String[] data = pendingTransferInput.remove(p.getUniqueId()); - if (data == null) return false; - - String areaName = data[0]; - if (message.trim().equalsIgnoreCase("取消")) { - p.sendMessage("§7已取消过户"); - return true; - } - - String newOwner = message.trim(); - // 验证格式 - if (!newOwner.matches("^[a-zA-Z0-9_]{3,16}$")) { - p.sendMessage("§c玩家名格式无效:仅允许英文字母、数字和下划线(3-16位)"); - p.sendMessage("§7请重新输入(或输入 §c取消§7)"); - pendingTransferInput.put(p.getUniqueId(), data); // 恢复等待状态 - return true; - } - - // 验证领地存在 - AreaConfig ac = areas.get(areaName); - if (ac == null) { - p.sendMessage("§c领地不存在: " + areaName); - return true; - } - if (!hasPermission(p, ac, PermissionLevel.OWNER)) { - p.sendMessage("§c需要领地所有者或管理员权限"); - return true; - } - if (samePlayer(newOwner, ac.owner)) { - p.sendMessage("§c不能转让给自己"); - return true; - } - - // 验证新所有者是否存在 - DatabaseManager dbMgr = plugin.getDb(); - if (dbMgr != null && !dbMgr.userExists(newOwner)) { - p.sendMessage("§c玩家 §e" + newOwner + " §c尚未注册"); - return true; - } - - // ★ 验证玩家注册时间必须超过5分钟(防止注册秒退玩家接收领地) - if (dbMgr != null) { - long registerTime = 0; - try { - Object regTimeObj = dbMgr.getField(newOwner, "register_time"); - if (regTimeObj instanceof Number) { - // ★ 第十一批:秒/毫秒混存归一化 - registerTime = DurationParser.toMs( - ((Number) regTimeObj).longValue()); - } - } catch (Exception e) { - // 忽略异常 - } - if (registerTime > 0) { - long now = System.currentTimeMillis(); - long fiveMinutesMs = 5 * 60 * 1000; - if (now - registerTime < fiveMinutesMs) { - long minutesLeft = (fiveMinutesMs - (now - registerTime)) / 60000; - p.sendMessage("§c玩家 §e" + newOwner + " §c注册时间不足5分钟(还差" + minutesLeft + "分钟)"); - return true; - } - } - } - - // 执行转让 - String oldOwner = ac.owner; - ac.owner = newOwner; - saveAreaToDb(ac); - - // 获取权限快照 + 追踪cooldown - if (plugin.webManager != null) { - String snapshot = getLandPermissionsSnapshot(areaName); - long expiresAt = System.currentTimeMillis() + 60000; - WebManager.TransferInfo info = new WebManager.TransferInfo( - areaName, oldOwner, newOwner, expiresAt, 0, 0, snapshot - ); - plugin.webManager.trackTransfer(areaName, info); - plugin.webManager.requestImmediateLandSync(); - } - - p.sendMessage("§a已将 §e" + areaName + " §a从 §e" + (oldOwner != null ? oldOwner : "无") + " §a转让给 §e" + newOwner + "§a,§e60秒冷却期内可取消"); - // ★ 可点击取消超链接 - if (p instanceof Player) { - Player pp = (Player) p; - pp.sendMessage(Component.empty() - .append(Component.text("§c§l[点击取消过户] ")) - .hoverEvent(HoverEvent.showText(Component.text("§c点击立即取消领地 §e" + areaName + " §c的过户"))) - .clickEvent(ClickEvent.runCommand("/protect canceltransfer " + areaName)) - ); - } - return true; - } - - /** - * ★ 检查并处理用户组配置编辑输入 - * @return true 如果是用户组编辑输入,已处理 - */ - public boolean handleGroupEditInput(Player p, String message) { - String[] data = pendingGroupEditInput.remove(p.getUniqueId()); - if (data == null) return false; - - String groupName = data[0]; - String field = data[1]; // "price", "maxlands", "priority" - - if (message.trim().equalsIgnoreCase("取消")) { - p.sendMessage("§7已取消编辑"); - if (plugin.areaGUIManager != null) { - Sdf1_login.Main pl = plugin; - org.bukkit.entity.Player pp = p; - String gn = groupName; - org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openGroupEditPanel(pp, gn)); - } - return true; - } - - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { - p.sendMessage("§c用户组系统未初始化"); - return true; - } - UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(groupName); - if (cfg == null) { - p.sendMessage("§c用户组不存在: " + groupName); - return true; - } - - // ★ 智能数字解析:支持中文、罗马、英文数字 - Integer parsed = parseSmartNumberBase(message.trim()); - if (parsed == null) { - p.sendMessage("§c无法解析数字: " + message.trim()); - p.sendMessage("§7支持: 纯数字123、中文一二三、大写壹佰贰拾叁、罗马I II III、英文one two three"); - p.sendMessage("§7或输入 §c取消§7 放弃编辑"); - pendingGroupEditInput.put(p.getUniqueId(), data); // 恢复等待 - return true; - } - - String fieldLabel; - switch (field) { - case "price": - if (parsed < 0 || parsed > 999999) { - p.sendMessage("§c价格范围: 0~999999(0=使用全局默认)"); - pendingGroupEditInput.put(p.getUniqueId(), data); - return true; - } - cfg.landPricePerSqm = parsed; - fieldLabel = "每㎡价格"; - break; - case "maxlands": - if (parsed < 0 || parsed > 999) { - p.sendMessage("§c最大领地数范围: 0~999(0=使用全局默认)"); - pendingGroupEditInput.put(p.getUniqueId(), data); - return true; - } - cfg.maxLands = parsed; - fieldLabel = "最大领地数"; - break; - case "priority": - if (parsed < 0 || parsed > 100) { - p.sendMessage("§c优先级范围: 0~100"); - pendingGroupEditInput.put(p.getUniqueId(), data); - return true; - } - cfg.priority = parsed; - fieldLabel = "优先级"; - break; - default: - return true; - } - - // 保存 - ugm.updateGroupConfig(cfg); - p.sendMessage("§a已将用户组 §e" + cfg.displayName + " §a的 §f" + fieldLabel + " §a修改为 §e" + parsed); - - // 刷新GUI(必须切主线程) - if (plugin.areaGUIManager != null) { - Sdf1_login.Main pl = plugin; - org.bukkit.entity.Player pp = p; - String gn = groupName; - org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openGroupEditPanel(pp, gn)); - } - return true; - } - - private String getConfigNameByKey(String key) { - switch (key) { - case "create_price_per_sqm": return "创建价格(每㎡)"; - case "max_lands_per_player": return "每人最大领地数"; - case "default_height": return "默认高度"; - case "peace_mode_max_duration": return "和平模式最大时长(秒)"; - case "refund_ratio_per_cent": return "删除退费比例(%)"; - case "max_free_area_sqm": return "单次扩建免审面积上限(㎡)"; - case "max_land_side_blocks": return "领地免审边长上限(格)"; - default: return key; - } - } - - /** - * ★ 智能数字解析:支持多种格式 - * 纯数字、中文数字、中文大写、罗马数字、英文数字 - */ - public static Integer parseSmartNumber(String input) { - if (input == null || input.isEmpty()) return null; - input = input.trim().toLowerCase(); - - // ★ 支持 +/- 前缀(相对增减) - if (input.startsWith("+") || input.startsWith("-")) { - // +/- 前缀需要有后续数字 - String numPart = input.substring(1).trim(); - if (numPart.isEmpty()) return null; - Integer delta = parseSmartNumberBase(numPart); - if (delta == null) return null; - return input.startsWith("-") ? -delta : delta; - } - - return parseSmartNumberBase(input); - } - - /** - * ★ 智能数字解析(带默认值和下限) - * @param input 用户输入 - * @param current 当前值(用于 +/- 计算) - * @param floor 最小值(封底) - */ - public static int parseSmartNumber(String input, int current, int floor) { - if (input == null || input.isEmpty()) return current; - input = input.trim().toLowerCase(); - - // ★ 支持 +/- 前缀(相对增减) - if (input.startsWith("+") || input.startsWith("-")) { - String numPart = input.substring(1).trim(); - if (numPart.isEmpty()) return current; - Integer delta = parseSmartNumberBase(numPart); - if (delta == null) return current; - int result = input.startsWith("-") ? current - delta : current + delta; - return Math.max(result, floor); - } - - // 纯数字(绝对值) - Integer abs = parseSmartNumberBase(input); - if (abs == null) return current; - return Math.max(abs, floor); - } - - public static Integer parseSmartNumberBase(String input) { - if (input == null || input.isEmpty()) return null; - input = input.trim().toLowerCase(); - - // 1. 纯数字 - try { - return Integer.parseInt(input); - } catch (Exception ignored) {} - - // 2. 中文数字(小写) - Integer result = parseChineseNumber(input); - if (result != null) return result; - - // 3. 罗马数字 - result = parseRomanNumber(input); - if (result != null) return result; - - // 4. 英文数字单词 - result = parseEnglishNumber(input); - if (result != null) return result; - - return null; - } - - /** - * 解析中文数字(一~九十九万九千九百九十九) - */ - private static Integer parseChineseNumber(String input) { - if (input.isEmpty()) return null; - - // 中文大写数字映射 - String[][] chineseMap = { - {"零", "0"}, {"一", "1"}, {"二", "2"}, {"三", "3"}, {"四", "4"}, - {"五", "5"}, {"六", "6"}, {"七", "7"}, {"八", "8"}, {"九", "9"}, - {"壹", "1"}, {"贰", "2"}, {"叁", "3"}, {"肆", "4"}, {"伍", "5"}, - {"陆", "6"}, {"柒", "7"}, {"捌", "8"}, {"玖", "9"}, - {"十", "10"}, {"百", "100"}, {"千", "1000"}, {"万", "10000"}, - {"拾", "10"}, {"佰", "100"}, {"仟", "1000"} - }; - - // 替换中文大写为小写 - String normalized = input; - for (String[] pair : chineseMap) { - normalized = normalized.replace(pair[0], pair[1]); - } - - // 检查是否包含数字 - if (!normalized.matches("[0-9零一二三四五六七八九十百千万]+")) { - return null; - } - - // 简单解析:直接替换后计算 - try { - // 处理 "十二" 这种格式 → "12" - normalized = normalized.replace("零", ""); - normalized = normalized.replace("一十", "10"); - normalized = normalized.replace("二十", "20"); - normalized = normalized.replace("三十", "30"); - normalized = normalized.replace("四十", "40"); - normalized = normalized.replace("五十", "50"); - normalized = normalized.replace("六十", "60"); - normalized = normalized.replace("七十", "70"); - normalized = normalized.replace("八十", "80"); - normalized = normalized.replace("九十", "90"); - - // 处理 "一百" 格式 - if (normalized.contains("百")) { - String[] parts = normalized.split("百"); - int hundreds = Integer.parseInt(parts[0]) * 100; - int tens = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; - return hundreds + tens; - } - - // 处理 "千" 格式 - if (normalized.contains("千")) { - String[] parts = normalized.split("千"); - int thousands = Integer.parseInt(parts[0]) * 1000; - int rest = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; - return thousands + rest; - } - - // 处理 "万" 格式 - if (normalized.contains("万")) { - String[] parts = normalized.split("万"); - int tenThousands = Integer.parseInt(parts[0]) * 10000; - int rest = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; - return tenThousands + rest; - } - - // 纯数字 - return Integer.parseInt(normalized); - } catch (Exception e) { - return null; - } - } - - /** - * 解析罗马数字 - */ - private static Integer parseRomanNumber(String input) { - input = input.toUpperCase(); - if (!input.matches("[IVXLCDM]+")) return null; - - int result = 0; - int prev = 0; - for (int i = input.length() - 1; i >= 0; i--) { - int val = 0; - switch (input.charAt(i)) { - case 'I': val = 1; break; - case 'V': val = 5; break; - case 'X': val = 10; break; - case 'L': val = 50; break; - case 'C': val = 100; break; - case 'D': val = 500; break; - case 'M': val = 1000; break; - } - if (val < prev) result -= val; - else result += val; - prev = val; - } - return result > 0 ? result : null; - } - - /** - * 解析英文数字单词 - */ - private static Integer parseEnglishNumber(String input) { - input = input.toLowerCase().trim(); - switch (input) { - case "zero": return 0; - case "one": return 1; - case "two": return 2; - case "three": return 3; - case "four": return 4; - case "five": return 5; - case "six": return 6; - case "seven": return 7; - case "eight": return 8; - case "nine": return 9; - case "ten": return 10; - case "eleven": return 11; - case "twelve": return 12; - case "thirteen": return 13; - case "fourteen": return 14; - case "fifteen": return 15; - case "sixteen": return 16; - case "seventeen": return 17; - case "eighteen": return 18; - case "nineteen": return 19; - case "twenty": return 20; - case "thirty": return 30; - case "forty": return 40; - case "fifty": return 50; - case "sixty": return 60; - case "seventy": return 70; - case "eighty": return 80; - case "ninety": return 90; - case "hundred": return 100; - case "thousand": return 1000; - default: return null; - } - } - - /** - * 检查玩家的访客权限是否有效(未过期) - */ - public boolean hasValidVisitorPermission(Player player, AreaConfig ac) { - if (dbConnection == null || ac == null) return false; - int landId = getLandIdFromDb(ac.name); - if (landId <= 0) return false; - // ★ 2026-10-10 起仅精确匹配:离线模式大小写不同=不同玩家, - // normName 模糊匹配会让大小写变种玩家冒充访客身份、绕过全部 deny* 限制。 - PermRow row = findPermRowExact(landId, player.getName()); - if (row == null || !"visitor".equalsIgnoreCase(row.role)) return false; - return row.expiresAt == 0 || row.expiresAt > System.currentTimeMillis(); - } - - /** 显示用户组管理面板(CLI可点击交互) */ - private void showGroupListPanel(CommandSender sender) { - UserGroupManager ugm = plugin.getUserGroup(); - if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return; } - - Map groups = ugm.getGroupConfigs(); - sender.sendMessage("§a§l========== 用户组管理 =========="); - - if (groups.isEmpty()) { - sender.sendMessage("§7当前暂无用户组定义"); - sender.sendMessage("§7你可以通过以下方式创建用户组:"); - sender.sendMessage("§e /protect groupset <组名> [显示名] [颜色] [优先级] [价格] [上限]"); - sender.sendMessage("§7或通过PHP管理后台创建"); - } else { - sender.sendMessage("§f共 §a" + groups.size() + " §f个用户组:"); - sender.sendMessage(""); - for (UserGroupManager.UserGroupConfig cfg : groups.values()) { - String priceStr = cfg.landPricePerSqm >= 0 ? cfg.landPricePerSqm + "/㎡" : "全局默认"; - String maxStr = cfg.maxLands >= 0 ? String.valueOf(cfg.maxLands) : "全局默认"; - sender.sendMessage("§f" + cfg.displayColor + "§l" + cfg.displayName - + " §7(ID=" + cfg.name + ", 优先级=" + cfg.priority + ")"); - sender.sendMessage("§7 价格=" + priceStr + " 上限=" + maxStr + " 默认权限=" + cfg.defaultPerms); - // ★ 每个组提供编辑和删除操作入口 - if (sender instanceof Player) { - Player pp = (Player) sender; - // ★ 成员管理 - pp.sendMessage(Component.empty() - .append(Component.text(" §a[成员管理] ")) - .hoverEvent(HoverEvent.showText(Component.text("§a管理此用户组的成员"))) - .clickEvent(ClickEvent.runCommand("/protect groupmembers " + cfg.name)) - ); - pp.sendMessage(Component.empty() - .append(Component.text(" §e[编辑] ")) - .hoverEvent(HoverEvent.showText(Component.text("§e交互式编辑此用户组配置"))) - .clickEvent(ClickEvent.runCommand("/protect groupedit " + cfg.name)) - ); - if (!cfg.name.equals(UserGroupManager.DEFAULT_GROUP)) { - pp.sendMessage(Component.empty() - .append(Component.text(" §c[删除] ")) - .hoverEvent(HoverEvent.showText(Component.text("§c删除此用户组"))) - .clickEvent(ClickEvent.runCommand("/protect groupdelconfig " + cfg.name)) - ); - } - } - } - } - - // 操作入口 - sender.sendMessage(""); - if (sender instanceof Player) { - Player pp = (Player) sender; - // 创建新组 - pp.sendMessage(Component.empty() - .append(Component.text("§a[创建新用户组] §e点击交互式创建")) - .hoverEvent(HoverEvent.showText(Component.text("§e点击开始创建流程"))) - .clickEvent(ClickEvent.suggestCommand("/protect groupset ")) - ); - // 从PHP同步 - pp.sendMessage(Component.empty() - .append(Component.text("§b[从PHP同步] §e点击拉取PHP端用户组")) - .hoverEvent(HoverEvent.showText(Component.text("§e从管理后台同步用户组数据"))) - .clickEvent(ClickEvent.runCommand("/protect groupset sync")) - ); - // ★ 返回领地系统首页(CLI模式) - pp.sendMessage(Component.empty() - .append(Component.text("§7[返回领地首页] ")) - .hoverEvent(HoverEvent.showText(Component.text("§7返回领地系统主菜单"))) - .clickEvent(ClickEvent.runCommand("/protect")) - ); - } - sender.sendMessage("§7§l─────────────────────────────────"); - } - - /** - * ★ 用户组CLI交互式编辑面板 - * 每个字段显示当前值,点击可输入新值 - */ - private void showGroupEditCLI(Player p, UserGroupManager.UserGroupConfig cfg) { - p.sendMessage(Component.text("§6§l────────── 编辑用户组: " + cfg.displayName + " ──────────")); - - // 价格 - p.sendMessage(Component.empty() - .append(Component.text("§a每㎡价格: §f" + cfg.landPricePerSqm + " ")) - .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " price " + (cfg.landPricePerSqm + 1)))) - .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " price " + Math.max(0, cfg.landPricePerSqm - 1)))) - .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " price "))) - ); - p.sendMessage(Component.empty() - .append(Component.text(" §7点击§e[输入]§7后在聊天栏输入新值(支持中文/罗马数字)")) - ); - - // 最大领地数 - p.sendMessage(Component.empty() - .append(Component.text("§a最大领地数: §f" + cfg.maxLands + " ")) - .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " maxlands " + (cfg.maxLands + 1)))) - .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " maxlands " + Math.max(0, cfg.maxLands - 1)))) - .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " maxlands "))) - ); - - // 优先级 - p.sendMessage(Component.empty() - .append(Component.text("§a优先级: §f" + cfg.priority + " ")) - .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " priority " + (cfg.priority + 1)))) - .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " priority " + Math.max(0, cfg.priority - 1)))) - .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " priority "))) - ); - - p.sendMessage(""); - // 返回按钮 - p.sendMessage(Component.empty() - .append(Component.text("§c[返回用户组列表] ")) - .hoverEvent(HoverEvent.showText(Component.text("§c返回用户组管理列表"))) - .clickEvent(ClickEvent.runCommand("/protect grouplist")) - ); - p.sendMessage(Component.text("§6§l─────────────────────────────────")); - } - - private void showHelp(CommandSender s) { - s.sendMessage("§e§l==== 区域防护 ===="); - s.sendMessage("§a§l点击下方命令可直接执行:"); - - // 可点击的帮助消息 - sendClickableHelp(s, "/protect menu", "打开GUI菜单"); - sendClickableHelp(s, "/protect 工具", "获取选地工具"); - sendClickableHelp(s, "/protect 创建", "创建区域(先选点)"); - sendClickableHelp(s, "/protect 列表", "区域列表"); - sendClickableHelp(s, "/protect 重载", "重载配置"); - sendClickableHelp(s, "/protect 删除", "删除当前所在区域"); - sendClickableHelp(s, "/protect info", "当前领地信息"); - sendClickableHelp(s, "/protect add", "玩家加白(需在领地内)"); - sendClickableHelp(s, "/protect remove", "玩家删白(需在领地内)"); - sendClickableHelp(s, "/protect additem", "物品加黑"); - sendClickableHelp(s, "/protect removeitem", "物品删黑"); - sendClickableHelp(s, "/protect on", "显示边框"); - sendClickableHelp(s, "/protect off", "关闭边框"); - sendClickableHelp(s, "/protect tempon", "临时边框(15秒)"); - sendClickableHelp(s, "/protect list", "列出白名单(需在领地内)"); - sendClickableHelp(s, "/protect listitem", "列出物品黑名单"); - sendClickableHelp(s, "/protect config", "查看/修改全局配置"); - sendClickableHelp(s, "/protect expand [格数]", "扩建当前领地(超限转审批)"); - s.sendMessage("§e§l---- 面积审批(管理员,需在现场) ----"); - sendClickableHelp(s, "/protect accept <领地|申请人>", "批准超限扩建/创建"); - sendClickableHelp(s, "/protect deny <领地|申请人>", "驳回超限扩建/创建"); - - s.sendMessage("§e§l---- 权限管理 ----"); - sendClickableHelp(s, "/protect setowner", "设置当前领地所有者(管理员)"); - sendClickableHelp(s, "/protect addvisitor", "添加访客(需在领地内)"); - sendClickableHelp(s, "/protect removevisitor", "移除访客(需在领地内)"); - sendClickableHelp(s, "/protect listvisitors", "列出访客(需在领地内)"); - sendClickableHelp(s, "/protect transfer", "转让领地(需在领地内)"); - sendClickableHelp(s, "/protect settp", "设置领地传送点"); - sendClickableHelp(s, "/protect tp <领地>", "传送到领地传送点"); - - s.sendMessage("§e§l---- 权限商店 ----"); - sendClickableHelp(s, "/protect shop list", "查看在售权限"); - sendClickableHelp(s, "/protect shop my", "查看我的权限"); - sendClickableHelp(s, "/protect info", "当前领地信息"); - - s.sendMessage("§e§l---- 设置 ----"); - sendClickableHelp(s, "/protect uimode", "切换UI模式(GUI/CLI)"); - - s.sendMessage("§b§l欢迎游玩草原探险服务器"); - s.sendMessage("§b§l服务器ip:mc2.ypshidifu.cn 端口30679"); - } - - /** - * 发送可点击的帮助消息 - * 玩家点击后自动执行命令 - */ - private void sendClickableHelp(CommandSender s, String command, String description) { - if (!(s instanceof Player)) { - // 控制台直接显示纯文本 - s.sendMessage("§a" + command + " §7" + description); - return; - } - - Player p = (Player) s; - // 使用Adventure API创建可点击消息 - net.kyori.adventure.text.Component msg = net.kyori.adventure.text.Component.empty() - .append(net.kyori.adventure.text.Component.text("§a§l" + command + " ")) - .append(net.kyori.adventure.text.Component.text("§7" + description) - .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText( - net.kyori.adventure.text.Component.text("§e点击执行该命令"))) - .clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand(command))); - p.sendMessage(msg); - } -// ==================== 白名单管理公共方法 ==================== - - // ==================== 白名单操作 ==================== - - public boolean isPlayerGlobalWhitelisted( - String playerName) { - return whiteContains(globalPlayerWhitelist, playerName); - } - - public boolean addPlayerToGlobalWhitelist( - String playerName) { - // ★ 2026-10-10 精确存储原始拼写(离线模式大小写不同=不同玩家) - boolean ok = globalPlayerWhitelist.add(playerName); - if (ok) saveWhitelists(); - return ok; - } - - public boolean removePlayerFromGlobalWhitelist( - String playerName) { - boolean ok = globalPlayerWhitelist.remove(playerName); - if (!ok) ok = globalPlayerWhitelist.remove(playerName.toLowerCase()); - if (!ok) ok = globalPlayerWhitelist.remove(normName(playerName)); - if (ok) saveWhitelists(); - return ok; - } - - public boolean isPlayerAreaWhitelisted( - String areaName, String playerName) { - return whiteContains(areaPlayerWhitelist.get(areaName), playerName); - } - - public boolean addPlayerToAreaWhitelist( - String areaName, String playerName) { - Set set = areaPlayerWhitelist - .computeIfAbsent(areaName, - k -> ConcurrentHashMap.newKeySet()); - // ★ 2026-10-10 精确存储原始拼写(离线模式大小写不同=不同玩家) - boolean ok = set.add(playerName); - if (ok) saveWhitelists(); - return ok; - } - - public boolean removePlayerFromAreaWhitelist( - String areaName, String playerName) { - Set wl = - areaPlayerWhitelist.get(areaName); - boolean ok = false; - if (wl != null) { - // ★ 精确 / 旧版默认Locale小写 / normName 三种格式全部尝试移除 - ok = wl.remove(playerName); - if (!ok) ok = wl.remove(playerName.toLowerCase()); - if (!ok) ok = wl.remove(normName(playerName)); - if (ok) saveWhitelists(); - } - - // ★ 无论内存白名单是否有该区域条目,都同步删除 area_land_permissions 记录: - // 旧版在 wl == null 时直接返回,DB 中残留的 admin/visitor 行会让玩家 - // 继续被 getPermissionLevel 判为 ADMIN/VISITOR,从而绕过 denyMove。 - if (dbConnection != null) { - try { - int landId = getLandIdFromDb(areaName); - if (landId > 0) { - // 按 normName 匹配删除所有大小写/变种变体行,确保身份一并清除 - for (PermRow row : findPermRows(landId, playerName)) { - PreparedStatement ps = dbConnection.prepareStatement( - "DELETE FROM area_land_permissions WHERE id = ?"); - ps.setInt(1, row.id); - ps.executeUpdate(); - ps.close(); - } - } - } catch (SQLException e) { - plugin.getLogger().warning("[防护] 移除成员时清理权限失败: " + e.getMessage()); - } - } - return ok; - } - - - - public int handleConfiscate(Player p, AreaConfig ac) { - // 管理员和领地所有者免检 - if (hasPermission(p, ac, PermissionLevel.OWNER)) return 0; - Set allItems = new HashSet<>(ac.confiscateItems); - allItems.addAll(globalItemBlacklist); - Set areaList = areaItemBlacklist.get(ac.name); - if (areaList != null) allItems.addAll(areaList); - - if (allItems.isEmpty()) return 0; - - int totalRemoved = 0; - String confiscatedNames = ""; - // ★ 违禁品管理面板(2026-10-11):捕获被没收的物品实体,供退出领地时归还 - java.util.List captured = new ArrayList<>(); - - for (String itemId : allItems) { - // 过滤掉区域名和无效ID - Material mat = Material.matchMaterial(itemId); - if (mat == null) { - plugin.getLogger().warning( - "[防护] 跳过无效物品: " + itemId); - continue; - } - ItemStack[] all = p.getInventory().getContents(); - int count = 0; - for (int i = 0; i < all.length; i++) { - if (all[i] != null && all[i].getType() == mat) { - count += all[i].getAmount(); - captured.add(all[i].clone()); - all[i] = null; - } - } - p.getInventory().setContents(all); - - ItemStack off = p.getInventory().getItemInOffHand(); - if (off != null && off.getType() == mat) { - count += off.getAmount(); - captured.add(off.clone()); - p.getInventory().setItemInOffHand(null); - } - - if (count > 0) { - totalRemoved += count; - if (!confiscatedNames.isEmpty()) - confiscatedNames += "§7、§e"; - confiscatedNames += "§e" + matname(mat) - + " x" + count; - } - } - - if (totalRemoved > 0) { - // ★ 没收计数 +1 并落库(领地主可在违禁品面板查看累计没收次数) - ac.confiscateCount++; - saveAreaToDb(ac); - // ★ 暂存被没收物品:退出领地时按 returnOnLeave 决定是否归还 - java.util.Map> byPlayer = - confiscatedStore.computeIfAbsent(p.getUniqueId(), - k -> new ConcurrentHashMap<>()); - java.util.List store = byPlayer.computeIfAbsent(ac.name, - k -> new ArrayList<>()); - store.addAll(captured); - - String msg = (ac.confiscateMsg != null - && !ac.confiscateMsg.isEmpty()) - ? ac.confiscateMsg - : "你携带了违禁品,已被没收"; - // ★ 替换变量 - msg = msg.replace("{player}", p.getName()) - .replace("{area}", ac.name) - .replace("{count}", String.valueOf(totalRemoved)) - .replace("{items}", confiscatedNames); - p.sendMessage("§c§l[区域防护] §f" + msg); - p.sendMessage("§7没收物品: " + confiscatedNames); - - if (ac.enableAnnounce - && ac.announceTemplate != null - && !ac.announceTemplate.isEmpty()) { - String ann = ac.announceTemplate - .replace("{player}", p.getName()) - .replace("{area}", ac.name) - .replace("{count}", String.valueOf(totalRemoved)) - .replace("{items}", confiscatedNames); - Bukkit.broadcastMessage("§c§l[区域防护] §f" + ann); - } - for (String cmd : ac.punishCommands) { - String finalCmd = cmd - .replace("{player}", p.getName()) - .replace("{area}", ac.name) - .replace("{count}", String.valueOf(totalRemoved)); - Bukkit.getScheduler().runTask(plugin, - () -> Bukkit.dispatchCommand( - Bukkit.getConsoleSender(), finalCmd)); - } - } - return totalRemoved; - } - public void clearBlacklists() { - globalItemBlacklist.clear(); - areaItemBlacklist.clear(); - plugin.getLogger().info("[防护] 已清空所有黑名单"); - } - - /** - * ★ 违禁品归还(2026-10-11 违禁品管理面板)。 - * - *

玩家离开领地时调用:若该领地 {@code returnOnLeave=true},把此前 - * {@link #handleConfiscate} 暂存的没收物品退还给玩家(背包满则掉落脚下); - * 若为 false(默认),直接清空暂存(不退还)。两种情况都会清掉暂存记录。 - */ - public void handleConfiscateReturn(Player p, String areaName) { - if (areaName == null) return; - java.util.Map> byPlayer = - confiscatedStore.get(p.getUniqueId()); - if (byPlayer == null) return; - java.util.List store = byPlayer.remove(areaName); - if (byPlayer.isEmpty()) confiscatedStore.remove(p.getUniqueId()); - if (store == null || store.isEmpty()) return; - - AreaConfig ac = areas.get(areaName); - if (ac == null || !ac.returnOnLeave) { - // 默认不退还:仅清空暂存 - return; - } - - int returned = 0; - StringBuilder names = new StringBuilder(); - for (ItemStack it : store) { - if (it == null || it.getType().isAir()) continue; - java.util.HashMap left = - p.getInventory().addItem(it); - if (!left.isEmpty()) { - // 背包放不下:剩余部分掉落脚下 - for (ItemStack drop : left.values()) { - p.getWorld().dropItemNaturally(p.getLocation(), drop); - } - } - returned += it.getAmount(); - if (names.length() > 0) names.append("§7、§e"); - names.append("§e").append(matname(it.getType())) - .append(" x").append(it.getAmount()); - } - if (returned > 0) { - p.sendMessage("§a§l[区域防护] §f你离开领地 §e" + areaName - + "§f,已退还没收物品: " + names); - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onEntityDamage( - org.bukkit.event.entity.EntityDamageByEntityEvent e) { - Entity entity = e.getEntity(); - String typeName = entity.getType().name(); - - // ★ 和平模式攻击免疫:敌对生物(含投射物)对玩家的攻击无效 - if (entity instanceof Player) { - boolean hostileAttack = false; - Entity damager = e.getDamager(); - if (isHostile(damager.getType().name())) { - hostileAttack = true; - } else if (damager instanceof org.bukkit.entity.Projectile) { - // 投射物(箭/三叉戟等):检查shooter是否敌对生物 - org.bukkit.entity.Projectile proj = (org.bukkit.entity.Projectile) damager; - if (proj.getShooter() instanceof Entity) { - hostileAttack = isHostile(((Entity) proj.getShooter()).getType().name()); - } - } - if (hostileAttack) { - Player victim = (Player) entity; - AreaConfig pac = getArea( - victim.getWorld().getName(), - victim.getLocation().getBlockX(), - victim.getLocation().getBlockY(), - victim.getLocation().getBlockZ()); - if (pac != null && pac.peaceMode) { - e.setCancelled(true); - return; - } - } - } - - if (typeName.equals("ITEM_FRAME") - || typeName.equals("GLOW_ITEM_FRAME")) { - if (!(e.getDamager() instanceof Player)) return; - Player p = (Player) e.getDamager(); - - // ★ 统一白名单检查(支持per-player权限) - AreaConfig ac = findFrameArea(entity); - if (ac == null) return; - // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (!getEffectiveDeny(p, ac, "denyItemFrame")) return; - - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - return; - } - - // ===== 攻击生物逻辑 ===== - if (e.getDamager() instanceof Player && !(e.getEntity() instanceof Player)) { - Player mobAttacker = (Player) e.getDamager(); - AreaConfig acMob = getArea( - mobAttacker.getWorld().getName(), - mobAttacker.getLocation().getBlockX(), - mobAttacker.getLocation().getBlockY(), - mobAttacker.getLocation().getBlockZ()); - if (acMob != null && getEffectiveDeny(mobAttacker, acMob, "denyMobAttack")) { - // ★ 公共建筑设施:访客允许攻击敌对生物 - if (!acMob.isPublicBuilding || samePlayer(mobAttacker.getName(), acMob.owner) || isAreaAdmin(mobAttacker)) { - e.setCancelled(true); - mobAttacker.sendMessage("§c§l[区域防护] §f此区域禁止攻击生物"); - return; - } - } - } - - // ===== PVP 逻辑 ===== - if (e.getDamager() instanceof Player && e.getEntity() instanceof Player) { - Player attacker = (Player) e.getDamager(); - Player victim = (Player) e.getEntity(); - - // 攻击者和受害者领地位置 - AreaConfig acAttacker = getArea( - attacker.getWorld().getName(), - attacker.getLocation().getBlockX(), - attacker.getLocation().getBlockY(), - attacker.getLocation().getBlockZ()); - AreaConfig acVictim = getArea( - victim.getWorld().getName(), - victim.getLocation().getBlockX(), - victim.getLocation().getBlockY(), - victim.getLocation().getBlockZ()); - - // ★ denyAllDamage:先检查(无论攻击者/受害者领地都拦) - // 受害者领地禁止所有伤害 → 阻断 - if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyAllDamage")) { - e.setCancelled(true); - attacker.sendMessage("§c§l[区域防护] §f目标领地为绝对安全区"); - return; - } - // 攻击者领地禁止所有伤害 → 阻断 - if (acAttacker != null && getEffectiveDeny(attacker, acAttacker, "denyAllDamage")) { - e.setCancelled(true); - attacker.sendMessage("§c§l[区域防护] §f此区域禁止一切伤害行为"); - return; - } - - // ★ denyPVP:只要任意一方领地开启PVP禁止 → 完全阻断 - // 受害者领地禁止PVP → 阻断 - if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyPVP")) { - e.setCancelled(true); - attacker.sendMessage("§c§l[区域防护] §f该领地禁止PVP"); - return; - } - // 攻击者领地禁止PVP → 阻断 - if (acAttacker != null && getEffectiveDeny(attacker, acAttacker, "denyPVP")) { - e.setCancelled(true); - attacker.sendMessage("§c§l[区域防护] §f此区域禁止PVP"); - return; - } - } - } - - // 展示框被点击(旋转/放入/取出) - @EventHandler - public void onItemFrameInteract( - PlayerInteractAtEntityEvent e) { - Entity clicked = e.getRightClicked(); - String typeName = clicked.getType().name(); - if (!typeName.equals("ITEM_FRAME") - && !typeName.equals("GLOW_ITEM_FRAME")) { - return; - } - - Player p = e.getPlayer(); - - // ★ 使用统一的权限检查方法(支持per-player权限) - AreaConfig ac = findFrameArea(clicked); - if (ac == null) return; - // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止交互实体"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止交互实体"); - return; - } - // ★ 25批订正倒反天罡:denyItemFrame/denyContainer 走 getEffectiveDeny 豁免链—— - // 领地主/授权成员/服务器管理员豁免,普通访客按领地默认拦截; - // 此前裸字段短路,连领地主交互自己领地的展示框都被拦。 - // 不提前 return——复用下方统一的 取消+下tick旋转恢复+消息 流程。 - if (!getEffectiveDeny(p, ac, "denyItemFrame") && !getEffectiveDeny(p, ac, "denyContainer")) return; - - // 非白名单 → 拦截 - // 记录当前旋转角度 - final Rotation savedRotation; - final Entity frame = clicked; - if (clicked instanceof org.bukkit.entity.ItemFrame) { - savedRotation = - ((org.bukkit.entity.ItemFrame) clicked) - .getRotation(); - } else { - savedRotation = null; - } - - e.setCancelled(true); - - // 下一tick强制恢复旋转 - if (savedRotation != null) { - final Rotation restore = savedRotation; - Bukkit.getScheduler().runTaskLater(plugin, () -> { - if (frame.isValid() - && frame instanceof org.bukkit.entity.ItemFrame) { - ((org.bukkit.entity.ItemFrame) frame) - .setRotation(restore); - } - }, 1L); - } - - p.sendMessage("§c§l[区域防护] §f禁止交互展示框"); - - // 驱逐远程敌对生物 - banRangedHostiles(p, ac); - } - - - - // 展示框保护:检查展示框本身+周围方块所属区域 - - // ===== 展示框攻击拦截(左键/投射物破坏)===== - @EventHandler(priority = EventPriority.HIGHEST) - public void onEntityDamageByEntity( - org.bukkit.event.entity.EntityDamageByEntityEvent e) { - // PVP逻辑在onEntityDamage()中统一处理,此处仅处理展示框攻击 - Entity entity = e.getEntity(); - String typeName = entity.getType().name(); - - if (typeName.equals("ITEM_FRAME") - || typeName.equals("GLOW_ITEM_FRAME")) { - // 展示框被攻击 - // ★ 防爆模式:非玩家 damager(苦力怕/凋零/闪电/投射物实体等)→ denyItemFrame 区域一律取消 - if (!(e.getDamager() instanceof Player)) { - AreaConfig guardAc = findFrameArea(entity); - if (guardAc != null && guardAc.denyItemFrame) { - e.setCancelled(true); - } - return; - } - Player p = (Player) e.getDamager(); - - AreaConfig ac = findFrameArea(entity); - if (ac == null) return; - - // ★ 25批订正倒反天罡:玩家攻击走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, - // 普通访客按领地默认拦截;非玩家 damager(苦力怕/凋零/闪电等)走上方裸字段硬保护(24批防爆)。 - if (getEffectiveDeny(p, ac, "denyItemFrame")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - return; - } - - // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - // ★ 三级:展示框同时受denyItemFrame和denyContainer控制 - if (!getEffectiveDeny(p, ac, "denyItemFrame") && !getEffectiveDeny(p, ac, "denyContainer")) return; - - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - return; - } - - // 船、矿车、盔甲架等实体破坏拦截(受 L1 破坏总开关 + L2 实体交互管控) - if (entity instanceof org.bukkit.entity.Vehicle - || entity instanceof org.bukkit.entity.ArmorStand) { - if (!(e.getDamager() instanceof Player)) return; - Player p = (Player) e.getDamager(); - // ★ 取区域方式与放置一致:优先玩家位置,实体位置兜底 - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - Location loc = entity.getLocation(); - ac = getArea( - p.getWorld().getName(), - loc.getBlockX(), - loc.getBlockY(), - loc.getBlockZ()); - } - if (ac == null) return; - // L1: 破坏总开关 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - // L2: 实体交互 - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - } - return; - } - } - - // ===== ★ 载具破坏拦截(船/矿车等):VehicleDestroyEvent 是破坏的权威事件 ===== - // 仅靠 EntityDamageByEntityEvent 取消伤害,在部分版本/路径下不足以阻止载具被移除并掉落, - // 此处直接拦截破坏事件本身,确保 L1破坏总开关 / L2实体交互 对载具稳定生效。 - @EventHandler(priority = EventPriority.HIGHEST) - public void onVehicleDestroy(org.bukkit.event.vehicle.VehicleDestroyEvent e) { - org.bukkit.entity.Vehicle v = e.getVehicle(); - Entity attacker = e.getAttacker(); - if (!(attacker instanceof Player)) return; // 仅拦截玩家主动破坏,环境破坏(如撞击)不在此处理 - Player p = (Player) attacker; - // ★ 取区域方式与放置一致:优先载具坐标,玩家坐标兜底 - AreaConfig ac = getArea( - v.getWorld().getName(), - v.getLocation().getBlockX(), - v.getLocation().getBlockY(), - v.getLocation().getBlockZ()); - if (ac == null) { - ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - } - if (ac == null) return; - // 层级:L1 破坏总开关 → L2 实体交互 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏载具"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏载具"); - } - } - - // ===== ★ 载具乘坐拦截:VehicleEnterEvent 是骑乘的权威事件 ===== - // 当玩家右键已放置的船/矿车进入时触发,需检查实体交互权限。 - @EventHandler(priority = EventPriority.HIGHEST) - public void onVehicleEnter(org.bukkit.event.vehicle.VehicleEnterEvent e) { - Entity entered = e.getEntered(); - if (!(entered instanceof Player)) return; // 只处理玩家乘坐 - Player p = (Player) entered; - org.bukkit.entity.Vehicle v = e.getVehicle(); - // ★ 区域取法与放置/破坏一致:优先载具坐标,玩家坐标兜底 - AreaConfig ac = getArea( - v.getWorld().getName(), - v.getLocation().getBlockX(), - v.getLocation().getBlockY(), - v.getLocation().getBlockZ()); - if (ac == null) { - ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - } - if (ac == null) return; - // 层级:L2 实体交互 → L3 骑乘 - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止乘坐载具"); - return; - } - if (getEffectiveDeny(p, ac, "denyMount")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止乘坐载具"); - } - } - - // ===== ★ 悬挂物破坏拦截(展示框/画等):HangingBreakEvent 是破坏的权威事件 ===== - // ★ 展示框防爆:denyItemFrame 开启 → 非个人破坏(苦力怕/TNT/凋零之首爆炸、闪电、 - // 非玩家投射物、环境等)一律硬拦截(24批防爆,无豁免链); - // 玩家破坏走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, - // 普通访客按领地默认拦截(25批订正「倒反天罡」:此前连领地主手动破坏都被拦)。 - @EventHandler(priority = EventPriority.HIGHEST) - public void onHangingBreak(org.bukkit.event.hanging.HangingBreakEvent e) { - org.bukkit.entity.Hanging hanging = e.getEntity(); - AreaConfig ac = findFrameArea(hanging); - if (ac == null) return; - // ★ HangingBreakEvent 本身无 getRemover(),玩家/实体破坏走子类 HangingBreakByEntityEvent - Entity remover = null; - if (e instanceof org.bukkit.event.hanging.HangingBreakByEntityEvent) { - remover = ((org.bukkit.event.hanging.HangingBreakByEntityEvent) e).getRemover(); - } - // ★ 防爆模式:非玩家破坏(爆炸/闪电/环境/非玩家投射物)→ denyItemFrame 区域一律取消 - if (!(remover instanceof Player)) { - if (ac.denyItemFrame) { - e.setCancelled(true); - } - return; // 玩家主动破坏;箭矢命中由 onProjectileHit 处理 - } - Player p = (Player) remover; - // ★ 25批订正倒反天罡:玩家破坏走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, - // 普通访客按领地默认拦截;非玩家破坏走上方裸字段硬保护(24批防爆)。 - if (getEffectiveDeny(p, ac, "denyItemFrame")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - return; - } - // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框 - if (getEffectiveDeny(p, ac, "denyBlockBreak")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (getEffectiveDeny(p, ac, "denyEntityInteract")) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (!getEffectiveDeny(p, ac, "denyItemFrame")) return; - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - } - - private AreaConfig findFrameArea(Entity frame) { - Location loc = frame.getLocation(); - int bx = loc.getBlockX(); - int by = loc.getBlockY(); - int bz = loc.getBlockZ(); - String world = loc.getWorld().getName(); - - // ★ 按位置返回区域(不再要求 denyItemFrame 为真,以支持 L1/L2 先行判定) - AreaConfig ac = getArea(world, bx, by, bz); - if (ac != null) return ac; - - int[][] offsets = { - {0, 0, 1}, {0, 0, -1}, - {1, 0, 0}, {-1, 0, 0}, - {0, 1, 0}, {0, -1, 0} - }; - for (int[] off : offsets) { - ac = getArea(world, - bx + off[0], by + off[1], bz + off[2]); - if (ac != null) return ac; - } - return null; - } - - // ===== 投射物击中展示框(弓箭/弩/发射器箭/骷髅箭等)===== - @EventHandler - public void onProjectileHit( - org.bukkit.event.entity.ProjectileHitEvent e) { - Entity hit = e.getHitEntity(); - if (hit == null) return; - String typeName = hit.getType().name(); - if (!typeName.equals("ITEM_FRAME") - && !typeName.equals("GLOW_ITEM_FRAME")) { - return; - } - - // ★ 使用统一的权限检查方法(支持per-player权限) - AreaConfig ac = findFrameArea(hit); - if (ac == null) return; - - // ★ 防爆模式:非玩家 shooter(发射器箭/骷髅箭等)→ denyItemFrame 区域一律取消 - if (!(e.getEntity().getShooter() instanceof Player)) { - if (ac.denyItemFrame) { - e.setCancelled(true); - } - return; - } - Player shooter = (Player) e.getEntity().getShooter(); - - // ★ 25批订正倒反天罡:玩家射击走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, - // 普通访客按领地默认拦截;非玩家 shooter(发射器箭/骷髅箭等)走上方裸字段硬保护(24批防爆)。 - if (getEffectiveDeny(shooter, ac, "denyItemFrame")) { - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - return; - } - // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 - if (getEffectiveDeny(shooter, ac, "denyBlockBreak")) { - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - if (getEffectiveDeny(shooter, ac, "denyEntityInteract")) { - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止破坏实体"); - return; - } - // ★ 三级:展示框同时受denyItemFrame和denyContainer控制 - if (!getEffectiveDeny(shooter, ac, "denyItemFrame") && !getEffectiveDeny(shooter, ac, "denyContainer")) return; - - e.setCancelled(true); - shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); - } - - - // ===== 区域外火焰不能蔓延进区域 ===== - @EventHandler - public void onBlockSpread(BlockSpreadEvent e) { - Block block = e.getBlock(); - AreaConfig tgtArea = getArea( - block.getWorld().getName(), - block.getX(), block.getY(), block.getZ()); - if (tgtArea != null && tgtArea.denyFireSpread) { - e.setCancelled(true); - } - } - - - // ===== 火焰弹/烈焰球拦截 ===== - @EventHandler - public void onProjectileHitFireball(ProjectileHitEvent e) { - if (!(e.getEntity() instanceof org.bukkit.entity.Fireball)) return; - org.bukkit.entity.Fireball fb = (org.bukkit.entity.Fireball) e.getEntity(); - AreaConfig ac = getArea(fb.getWorld().getName(), - fb.getLocation().getBlockX(), fb.getLocation().getBlockY(), fb.getLocation().getBlockZ()); - if (ac != null && ac.denyFire) { - fb.remove(); - // 如果射手是玩家,发消息 - if (fb.getShooter() instanceof Player) { - ((Player) fb.getShooter()).sendMessage("§c§l[区域防护] §f此区域禁止投掷火焰"); - } - } - } - - // ===== TNT自爆防漏网:EntityExplodeEvent ===== - @EventHandler - public void onEntityExplode(EntityExplodeEvent e) { - org.bukkit.entity.Entity ent = e.getEntity(); - if (ent != null) { - // 如果是TNT引爆,检查引爆位置 - AreaConfig ac = getArea(ent.getWorld().getName(), - (int) ent.getLocation().getBlockX(), - (int) ent.getLocation().getBlockY(), - (int) ent.getLocation().getBlockZ()); - // 如果爆炸源在禁止爆炸的区域,取消所有爆炸 - if (ac != null && ac.denyExplosion) { - e.blockList().clear(); - return; - } - } - // 检查每个被爆炸的方块是否在禁止爆炸的区域 - Iterator it = e.blockList().iterator(); - while (it.hasNext()) { - Block b = it.next(); - AreaConfig a = getArea(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()); - if (a != null && a.denyExplosion) { - it.remove(); - } - } - } - - // ===== 区域内方块着火(TNT、岩浆等)也阻止 ===== - @EventHandler - public void onBlockBurn(BlockBurnEvent e) { - Block block = e.getBlock(); - AreaConfig ac = getArea( - block.getWorld().getName(), - block.getX(), block.getY(), block.getZ()); - if (ac != null && ac.denyFireSpread) { - e.setCancelled(true); - } - } - - // ===== PlayerInteractEvent: 打火石/火焰弹右键使用拦截(带豁免) ===== - // 注:火床放置拦截在onBlockPlace事件中处理 - @EventHandler - public void onPlayerInteractFire(PlayerInteractEvent e) { - Player p = e.getPlayer(); - Action action = e.getAction(); - if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; - - Material mat = e.getItem() == null ? null : e.getItem().getType(); - if (mat != Material.FLINT_AND_STEEL && mat != Material.FIRE_CHARGE - && mat != Material.CAMPFIRE && mat != Material.SOUL_CAMPFIRE - && mat != Material.LAVA_BUCKET) { - return; - } - - AreaConfig srcArea = getArea(p.getWorld().getName(), - p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); - Block clickedBlock = e.getClickedBlock(); - AreaConfig tgtArea = null; - if (clickedBlock != null) { - tgtArea = getArea(clickedBlock.getWorld().getName(), - clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); - } - - // 使用统一的豁免逻辑(OWNER/ADMIN自动通过,visitor按领地默认权限) - boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); - boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); - if (srcDenied || tgtDenied) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); - } - } - - // ===== 火床放置拦截 ===== - @EventHandler - public void onPlaceCampfire(BlockPlaceEvent e) { - Material placed = e.getItemInHand().getType(); - if (placed != Material.CAMPFIRE && placed != Material.SOUL_CAMPFIRE) return; - - Player p = e.getPlayer(); - AreaConfig ac = getArea(p.getWorld().getName(), - p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); - - // OWNER/ADMIN 豁免 - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - // visitor: 用领地默认权限 - if (ac != null && ac.denyFire) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此区域禁止放置火床"); - } - } - - // ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 ===== - @EventHandler - public void onIgnite(BlockIgniteEvent e) { - Player p = e.getPlayer(); - Block tgtBlock = e.getBlock(); - - // 非玩家触发(如火蔓延、TNT引爆)-> 不拦截,由BlockBurnEvent处理 - if (p == null) return; - - AreaConfig srcArea = getArea(p.getWorld().getName(), - p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); - AreaConfig tgtArea = getArea(tgtBlock.getWorld().getName(), - tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); - - // 使用统一豁免 - boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); - boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); - if (srcDenied || tgtDenied) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); - } - } - - // ===== ★ 拴绳使用(给生物拴绳或拉拽)===== - @EventHandler - public void onLeash(PlayerInteractEntityEvent e) { - if (!(e.getPlayer() instanceof Player)) return; - Player p = e.getPlayer(); - Entity entity = e.getRightClicked(); - // 检查玩家所在位置或实体位置 - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - ac = getArea( - entity.getWorld().getName(), - entity.getLocation().getBlockX(), - entity.getLocation().getBlockY(), - entity.getLocation().getBlockZ()); - } - if (ac == null || !ac.denyLead) return; - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - // 只拦截拴绳操作(主手持拴绳) - ItemStack hand = p.getInventory().getItemInMainHand(); - if (hand != null && hand.getType() == Material.LEAD) { - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止在此区域使用拴绳"); - } - } - - // ===== ★ 投喂动物 ===== - @EventHandler - public void onPlayerFeedAnimal(EntityBreedEvent e) { - if (!(e.getBreeder() instanceof Player)) return; - Player p = (Player) e.getBreeder(); - Entity entity = e.getEntity(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null) { - ac = getArea( - entity.getWorld().getName(), - entity.getLocation().getBlockX(), - entity.getLocation().getBlockY(), - entity.getLocation().getBlockZ()); - } - if (ac == null || !ac.denyAnimalFeeding) return; - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f禁止在此区域繁殖动物"); - } - - // ===== ★ 袭击侦听(玩家触发袭击时检测)===== - @EventHandler - public void onRaidTrigger(org.bukkit.event.raid.RaidTriggerEvent e) { - Player p = e.getPlayer(); - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac == null || !ac.denyRaid) return; - if (hasPermission(p, ac, PermissionLevel.OWNER)) return; - e.setCancelled(true); - p.sendMessage("§c§l[区域防护] §f此区域禁止触发袭击事件"); - } - - // ===== ★ 玩家发光检测(通过定期扫描清除区域内发光效果)===== - // 在onEnable中注册repeating task,此处提供public方法供Main调用 - public void checkGlowingPlayers() { - for (Player p : Bukkit.getOnlinePlayers()) { - AreaConfig ac = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - if (ac != null && getEffectiveDeny(p, ac, "denyGlowing")) { - if (p.hasPotionEffect(PotionEffectType.GLOWING)) { - p.removePotionEffect(PotionEffectType.GLOWING); - p.sendMessage("§c§l[区域防护] §f此区域禁止使用发光效果"); - } - } - } - } - - - private void savePeaceWhitelist(AreaConfig ac) { - File file = new File(rootDir, ac.name + ".txt"); - if (!file.exists()) return; - - try { - List lines = new ArrayList<>(); - BufferedReader reader = new BufferedReader( - new FileReader(file, StandardCharsets.UTF_8)); - String line; - boolean found = false; - - while ((line = reader.readLine()) != null) { - String trimmed = line.trim(); - if (trimmed.contains("和平") - && trimmed.contains("白名单")) { - StringBuilder sb = new StringBuilder(); - sb.append("和平模式白名单:"); - boolean first = true; - for (String name : ac.peaceWhitelist) { - if (!first) sb.append(","); - sb.append(name); - first = false; - } - lines.add(sb.toString()); - found = true; - } else { - lines.add(line); - } - } - reader.close(); - - if (!found && !ac.peaceWhitelist.isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append("和平模式白名单:"); - boolean first = true; - for (String name : ac.peaceWhitelist) { - if (!first) sb.append(","); - sb.append(name); - first = false; - } - lines.add(sb.toString()); - } - - FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8); - for (String l : lines) { - writer.write(l + "\n"); - } - writer.close(); - } catch (IOException e) { - plugin.getLogger().warning( - "[防护] 保存和平白名单失败: " + e.getMessage()); - } - } - - - @EventHandler - public void onPaintingPlace( - org.bukkit.event.player.PlayerBucketEmptyEvent e) { - // 暂不处理 - } - - - // ==================== 区域配置文件 ==================== - - public static class LandConfig { - public int 创建价格 = 1000; - public int 默认高度范围最小 = 0; - public int 默认高度范围最大 = 255; - public int 默认和平模式持续秒数 = 5; - public int 权限商店最低价格 = 1000; - public int 权限商店默认时长秒 = 86400; - public int 权限商店最大时长秒 = 86400; - public String 进入提示 = "进入领地: {area}"; - public String 离开提示 = "离开领地: {area}"; - } - - private final LandConfig landConfig = new LandConfig(); - - public LandConfig getLandConfig() { return landConfig; } - - private void loadAreaConfig() { - // ★ 配置文件放在area/子目录,避免插件根目录凌乱 - File areaDir = new File(plugin.getDataFolder(), "area"); - if (!areaDir.exists()) { - areaDir.mkdirs(); - } - // ★ 自动迁移:如果根目录有旧配置文件,移到area/目录 - File oldConfigFile = new File(plugin.getDataFolder(), "区域防护配置.txt"); - File configFile = new File(areaDir, "区域防护配置.txt"); - if (!configFile.exists() && oldConfigFile.exists()) { - try { - oldConfigFile.renameTo(configFile); - plugin.getLogger().info("[防护] 已将区域防护配置迁移到 area/ 目录"); - } catch (Exception e) { - // 迁移失败,使用新位置 - } - } - if (!configFile.exists()) { - writeDefaultAreaConfig(configFile); - } - try (BufferedReader br = new BufferedReader( - new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) continue; - int eq = line.indexOf('='); - if (eq < 0) continue; - String key = line.substring(0, eq).trim(); - String value = line.substring(eq + 1).trim(); - switch (key) { - case "创建价格": landConfig.创建价格 = Integer.parseInt(value); break; - case "默认高度范围最小": landConfig.默认高度范围最小 = Integer.parseInt(value); break; - case "默认高度范围最大": landConfig.默认高度范围最大 = Integer.parseInt(value); break; - case "默认和平模式持续秒数": landConfig.默认和平模式持续秒数 = Integer.parseInt(value); break; - case "权限商店最低价格": landConfig.权限商店最低价格 = Integer.parseInt(value); break; - case "权限商店默认时长秒": landConfig.权限商店默认时长秒 = Integer.parseInt(value); break; - case "权限商店最大时长秒": landConfig.权限商店最大时长秒 = Integer.parseInt(value); break; - case "进入提示": landConfig.进入提示 = value; break; - case "离开提示": landConfig.离开提示 = value; break; - } - } - } catch (Exception e) { - plugin.getLogger().warning("[防护] 加载区域配置文件失败: " + e.getMessage()); - } - } - - private void writeDefaultAreaConfig(File configFile) { - try (PrintWriter pw = new PrintWriter( - new OutputStreamWriter(new FileOutputStream(configFile), StandardCharsets.UTF_8))) { - pw.println("# 区域防护配置文件"); - pw.println("创建价格=1000"); - pw.println("默认高度范围最小=0"); - pw.println("默认高度范围最大=255"); - pw.println("默认和平模式持续秒数=5"); - pw.println("权限商店最低价格=1000"); - pw.println("权限商店默认时长秒=86400"); - pw.println("权限商店最大时长秒=86400"); - pw.println("进入提示=进入领地: {area}"); - pw.println("离开提示=离开领地: {area}"); - } catch (Exception e) { - plugin.getLogger().warning("[防护] 写入默认区域配置失败: " + e.getMessage()); - } - } - - // ==================== 数据类 ==================== - - public static class AreaConfig { - public String name = ""; - public String owner = ""; // 领地所有者 - public String world = ""; - public int x1, z1, x2, z2; - public int yMin = 0, yMax = 255; - public List confiscateItems = new ArrayList<>(); - // ★ 违禁品没收计数:领地累计没收次数(2026-10-11 违禁品管理面板) - public int confiscateCount = 0; - // ★ 退出领地是否归还没收物(默认 false=不退,由领地主控制) - public boolean returnOnLeave = false; - public List denyUseItems = new ArrayList<>(); - public List clearEffects = new ArrayList<>(); // 清除单个效果名 - public boolean clearAllBadEffects = false; // 清除所有负面+中性 - public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] - /** - * ★ 负面效果独立列表(2026-10-04 用户要求从增益里拆出来) - * - *

结构同 {@link #giveEffects}:[效果名, 等级, 秒数]。原实现把所有效果 - * (含缓慢/中毒/凋零等负面)全塞在 {@code giveEffects} 里,GUI 上也只能一把选, - * 负面与正面混排。拆开后 GUI 分两个子菜单、CLI 分两套指令、上限口径也各归各。

- */ - public List badEffects = new ArrayList<>(); // [效果名, 等级, 秒数] - public List punishCommands = new ArrayList<>(); - public boolean denyBlockPlace = false; - public boolean denyBlockBreak = false; - public boolean denySignEdit = false; // ★ 独立告示牌编辑权限 - public boolean denyContainer = false; // ★ 独立容器管理权限 - public boolean denyPVP = false; - public boolean denyFallDamage = false; - public boolean denyHunger = false; - public boolean denyAllDamage = false; - public boolean denyMount = false; - public boolean allowDrop = false; - public boolean denyEnderPearl = false; - public boolean denyBow = false; - public boolean denyPotion = false; - public boolean denyExplosion = false; - public boolean denyMove = false; - public boolean allowPickup = false; - public boolean denyFire = false; - // ★ 新增投掷物权限(三叉戟、雪球、风蛋、箭) - public boolean denyThrownProjectiles = false; - // ★ 玩家发光控制 - public boolean denyGlowing = false; - // ★ 红石电路交互(按钮、压力板、中继器、比较器) - public boolean denyRedstoneInteraction = false; - // ★ 门禁交互(门/栅栏门) - public boolean denyDoorInteraction = false; - // ★ 音频交互(音符盒、唱片机) - public boolean denyNoteblockJukebox = false; - // ★ 拴绳使用 - public boolean denyLead = false; - // ★ 农作物状态检测(播种&收获) - public boolean denyCropHarvest = false; - // ★ 耕地破坏(玩家跳跃踩踏耕地 → 变为泥土) - public boolean denyFarmlandTrample = false; - // ★ 末影人传送控制 - public boolean denyEnderTeleport = false; - // ★ 采集羊毛和乐魂挽具 - public boolean denyWoolShear = false; - // ★ 投喂动物 - public boolean denyAnimalFeeding = false; - // ★ 生物蛋(右键刷怪蛋生成实体)——上级权限:放置方块(放置未开启时单独开启本项不生效) - public boolean denySpawnEgg = false; - // ★ 涂蜡/刮蜡(蜜脾涂蜡、斧头刮蜡)——独立一级权限(不受上级开关约束):开启后普通访客禁涂/禁刮,领地主/插件管理员/已授权玩家豁免 - public boolean denyWax = false; - // ★ 玩家攻击生物 - public boolean denyMobAttack = false; - // ★ 村民交易(右键村民开交易窗 / 交易窗内完成交易)——独立一级权限: - // 开启后普通访客禁止与领地内村民交易,领地主/插件管理员/已授权玩家豁免 - public boolean denyVillagerTrade = false; - public boolean enableAnnounce = false; - public String announceTemplate = ""; - public String txtContent = ""; - public String confiscateMsg = ""; - public String enterMsg = ""; - public String leaveMsg = ""; - public boolean denyRaid = false; - public boolean denyFireSpread = false; - public boolean denyAllEffects = false;; - public boolean peaceMode = false; - public int peaceModeDuration = 5000; // 和平模式生物保护期(毫秒),默认5秒,最大3600秒 - public boolean denyItemFrame = false; - // ★ 二级权限:实体交互(受denyBlockPlace/denyBlockBreak一级权限影响) - // 其下三级:denyItemFrame 等具体实体权限 - public boolean denyEntityInteract = false; - public Set peaceWhitelist = new HashSet<>(); - public String enforceGameMode = null; // 强制游戏模式 - public Set modeExempt = new HashSet<>(); // 模式排除名单 - // ★ 传送点坐标(玩家自定义,防止卡墙) - public double warpX = 0, warpY = 0, warpZ = 0; - public float warpYaw = 0, warpPitch = 0; - public String warpWorld = ""; - // ★ 公共建筑标记 - public boolean isPublicBuilding = false; - // ★ 访客传送权限(默认关闭:未授权访客不能传送进入领地) - public boolean allowVisitorTeleport = false; - // ★ 创建领地时实际扣款金额(删除领地按比例退费的基数;0=无扣款) - public int createCost = 0; - - - - - private static final Map EFFECT_MAP - = new HashMap<>(); - static { - // 负面效果 - EFFECT_MAP.put("缓慢", PotionEffectType.SLOWNESS); - EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); - EFFECT_MAP.put("瞬间伤害", PotionEffectType.INSTANT_DAMAGE); - EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); - EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); - EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); - EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); - EFFECT_MAP.put("中毒", PotionEffectType.POISON); - EFFECT_MAP.put("凋零", PotionEffectType.WITHER); - EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); - EFFECT_MAP.put("霉运", PotionEffectType.UNLUCK); - EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); - EFFECT_MAP.put("蓄风", PotionEffectType.WIND_CHARGED); - EFFECT_MAP.put("盘丝", PotionEffectType.WEAVING); - EFFECT_MAP.put("渗浆", PotionEffectType.OOZING); - EFFECT_MAP.put("寄生", PotionEffectType.INFESTED); - // 中性效果 - EFFECT_MAP.put("不祥之兆", PotionEffectType.BAD_OMEN); - EFFECT_MAP.put("不祥征兆", PotionEffectType.BAD_OMEN); - EFFECT_MAP.put("袭击之兆", PotionEffectType.RAID_OMEN); - EFFECT_MAP.put("袭击征兆", PotionEffectType.RAID_OMEN); - EFFECT_MAP.put("试炼之兆", PotionEffectType.TRIAL_OMEN); - EFFECT_MAP.put("试炼征兆", PotionEffectType.TRIAL_OMEN); - // 正面效果 - EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); - EFFECT_MAP.put("急迫", PotionEffectType.HASTE); - EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); - EFFECT_MAP.put("瞬间治疗", PotionEffectType.INSTANT_HEALTH); - EFFECT_MAP.put("跳跃提升", PotionEffectType.JUMP_BOOST); - EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); - EFFECT_MAP.put("抗性提升", PotionEffectType.RESISTANCE); - EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); - EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); - EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); - EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); - EFFECT_MAP.put("发光", PotionEffectType.GLOWING); - EFFECT_MAP.put("生命提升", PotionEffectType.HEALTH_BOOST); - EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); - EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); - EFFECT_MAP.put("幸运", PotionEffectType.LUCK); - EFFECT_MAP.put("村庄英雄", PotionEffectType.HERO_OF_THE_VILLAGE); - EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); - EFFECT_MAP.put("潮涌能量", PotionEffectType.CONDUIT_POWER); - EFFECT_MAP.put("海豚的恩惠", PotionEffectType.DOLPHINS_GRACE); - - - // 带效果二字 - // 补充带"效果"后缀的别名 - EFFECT_MAP.put("黑暗效果", PotionEffectType.DARKNESS); - EFFECT_MAP.put("不祥之兆效果", PotionEffectType.BAD_OMEN); - EFFECT_MAP.put("袭击之兆效果", PotionEffectType.RAID_OMEN); - EFFECT_MAP.put("试炼之兆效果", PotionEffectType.TRIAL_OMEN); - EFFECT_MAP.put("缓慢效果", PotionEffectType.SLOWNESS); - EFFECT_MAP.put("中毒效果", PotionEffectType.POISON); - EFFECT_MAP.put("凋零效果", PotionEffectType.WITHER); - EFFECT_MAP.put("失明效果", PotionEffectType.BLINDNESS); - EFFECT_MAP.put("饥饿效果", PotionEffectType.HUNGER); - EFFECT_MAP.put("虚弱效果", PotionEffectType.WEAKNESS); - EFFECT_MAP.put("反胃效果", PotionEffectType.NAUSEA); - EFFECT_MAP.put("飘浮效果", PotionEffectType.LEVITATION); - EFFECT_MAP.put("发光效果", PotionEffectType.GLOWING); - EFFECT_MAP.put("霉运效果", PotionEffectType.UNLUCK); - EFFECT_MAP.put("蓄风效果", PotionEffectType.WIND_CHARGED); - EFFECT_MAP.put("盘丝效果", PotionEffectType.WEAVING); - EFFECT_MAP.put("渗浆效果", PotionEffectType.OOZING); - EFFECT_MAP.put("寄生效果", PotionEffectType.INFESTED); - EFFECT_MAP.put("挖掘疲劳效果", PotionEffectType.MINING_FATIGUE); - EFFECT_MAP.put("瞬间伤害效果", PotionEffectType.INSTANT_DAMAGE); - EFFECT_MAP.put("村庄英雄效果", PotionEffectType.HERO_OF_THE_VILLAGE); - } - - - public int ruleCount() { - int c = 0; - if (!confiscateItems.isEmpty()) c++; - if (denyBlockPlace) c++; - if (denyBlockBreak) c++; - if (denyPVP) c++; - if (denyFallDamage) c++; - if (denyHunger) c++; - if (denyAllDamage) c++; - if (allowDrop) c++; - if (denyMount) c++; - if (denyEnderPearl) c++; - if (denyBow) c++; - if (denyRaid) c++; - if (denyPotion) c++; - if (denyExplosion) c++; - if (denyFireSpread) c++; - if (denyAllEffects) c++; - if (!clearEffects.isEmpty()) c++; - if (clearAllBadEffects) c++; - if (!giveEffects.isEmpty()) c++; - if (enableAnnounce) c++; - if (!punishCommands.isEmpty()) c++; - if (!denyUseItems.isEmpty()) c++; - if (peaceMode) c++; - if (denyItemFrame) c++; - if (denyEntityInteract) c++; - if (denyThrownProjectiles) c++; - if (denyGlowing) c++; - if (denyRedstoneInteraction) c++; - if (denyDoorInteraction) c++; - if (denyNoteblockJukebox) c++; - if (denyLead) c++; - if (denyCropHarvest) c++; - if (denyWoolShear) c++; - if (denyAnimalFeeding) c++; - if (denySpawnEgg) c++; - if (denyWax) c++; - if (denyVillagerTrade) c++; - if (!peaceWhitelist.isEmpty()) c++; - - return c; - } - } - - // ==================== 快速创建领地(无选点,以自身为中心) ==================== - - /** - * ★ 快速创建领地:以玩家为中心3×3,自动命名+冲突数字0~999 - * @return true成功创建,false失败 - */ - 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(); - int maxLands = (ugm != null) ? ugm.getPlayerMaxLands(p.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; - long landCount = areas.values().stream().filter(a -> samePlayer(p.getName(), a.owner)).count(); - // DB兜底 - if (landCount < maxLands) { - try { - java.sql.PreparedStatement cs = dbConnection.prepareStatement("SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); - cs.setString(1, p.getName()); - java.sql.ResultSet cr = cs.executeQuery(); - if (cr.next()) { long dbc = cr.getLong("cnt"); if (dbc > landCount) landCount = dbc; } - cr.close(); cs.close(); - } catch (Exception ignored) {} - } - if (landCount >= maxLands) { - p.sendMessage("§c§l[防护] §f已达领地上限!每人最多 §e" + maxLands + " §f个领地"); - return false; - } - - // ★ 以玩家为中心3×3 - int cx = p.getLocation().getBlockX(); - int cz = p.getLocation().getBlockZ(); - int x1 = cx - 1, z1 = cz - 1; - int x2 = cx + 1, z2 = cz + 1; - int area = 9; // 3×3 - - // ★ 检查这个区域是否与其他领地重叠 - for (AreaConfig other : areas.values()) { - if (!other.world.equalsIgnoreCase(p.getWorld().getName())) continue; - // AABB重叠检测 - if (x1 <= other.x2 && x2 >= other.x1 && z1 <= other.z2 && z2 >= other.z1) { - p.sendMessage("§c§l[防护] §f该区域与领地 §e" + other.name + " §f重叠,请走远点再试"); - return false; - } - } - - // ★ 计算费用 - UserGroupManager ug = plugin.getUserGroup(); - int pricePerSqm = (ug != null) ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; - int totalCost = area * pricePerSqm; - - // ★ 检查余额 - BondManager bm = plugin.getBonds(); - if (bm != null && totalCost > 0) { - int bal = bm.getBonds(p.getName()); - if (bal < totalCost) { - p.sendMessage("§c§l[防护] §f余额不足!需要 §e" + totalCost + " §f债券,当前 §a" + bal); - return false; - } - } - - // ★ 自动命名:玩家名,冲突加数字0~999(重名判定不区分大小写) - String autoName = p.getName(); - if (areaNameExists(autoName)) { - boolean found = false; - for (int i = 0; i <= 999; i++) { - String candidate = p.getName() + i; - if (!areaNameExists(candidate)) { autoName = candidate; found = true; break; } - } - if (!found) { - p.sendMessage("§c§l[防护] §f无法找到可用的领地名(已用尽0~999后缀)"); - return false; - } - } - - // ★ 扣费 - int chargedCost = 0; - if (bm != null && totalCost > 0) { - String src = (pricePerSqm != globalCreatePricePerSqm) ? "(用户组优惠价)" : ""; - if (!bm.deductBonds(p.getName(), totalCost, "land_create", p.getName(), p.getName(), - "快速创建领地: " + autoName + " (" + area + "㎡×" + pricePerSqm + ")")) { - p.sendMessage("§c§l[防护] §f扣费失败,请稍后重试"); - return false; - } - p.sendMessage("§a§l[防护] §f创建领地扣除 §e" + totalCost + " §f债券(" + area + "㎡×" + pricePerSqm + "/㎡)" + src); - chargedCost = totalCost; - } - - // ★ 创建领地 - AreaConfig ac = new AreaConfig(); - ac.name = autoName; - ac.owner = p.getName(); - ac.world = p.getWorld().getName(); - ac.x1 = x1; ac.z1 = z1; ac.x2 = x2; ac.z2 = z2; - ac.yMin = 0; ac.yMax = 255; - ac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数) - saveAreaToDb(ac); - areas.put(autoName, ac); - loadAllAreas(); - - p.sendMessage("§a§l[防护] §f区域 §e" + autoName + " §f已创建(" + area + "㎡,以你为中心3×3)"); - return true; - } +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockSpreadEvent; +import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Animals; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.block.BlockState; +import org.bukkit.block.Container; +import java.sql.*; +import org.bukkit.GameMode; +import org.bukkit.event.EventPriority; + + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.Bukkit; +import java.util.Iterator; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import java.util.Set; +import java.util.HashSet; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntitySpawnEvent; +import com.destroystokyo.paper.event.entity.EntityPathfindEvent; +import org.bukkit.entity.Item; +import org.bukkit.event.inventory.InventoryOpenEvent; + +public class AreaProtection implements Listener { + + // 三级权限枚举 + public enum PermissionLevel { + ADMIN, // 管理员:可以做所有事 + OWNER, // 领地所有者:可以管理自己的领地 + MEMBER, // 已授权成员(DB role='member'):不受领地全局默认权限限制,可被per-player单独收紧 + VISITOR // 访客:只能在被授权的领地内活动 + } + + private final Main plugin; + public List giveEffects = new ArrayList<>(); + private final Map> playerAreas + = new HashMap<>(); + private final Map areas = + new ConcurrentHashMap<>(); + private Connection dbConnection; + // ★ 防止"无法识别效果"刷屏:同一名称只警告一次 + private static final Set UNKNOWN_EFFECT_WARNINGS = ConcurrentHashMap.newKeySet(); + + // 全局白名单(玩家) + private final Set globalPlayerWhitelist = + ConcurrentHashMap.newKeySet(); + // 区域白名单 key=区域名 value=玩家名集合 + private final Map> areaPlayerWhitelist = + new ConcurrentHashMap<>(); + // 全局黑名单(物品ID) + private final Set globalItemBlacklist = + ConcurrentHashMap.newKeySet(); + // 区域黑名单 + private final Map> areaItemBlacklist = + new ConcurrentHashMap<>(); + + // ★ 领地删除延迟队列(1分钟冷却) + private static class PendingDelete { + final String areaName; + final String playerName; + final long startTime; + final BukkitTask task; + PendingDelete(String areaName, String playerName, long startTime, BukkitTask task) { + this.areaName = areaName; + this.playerName = playerName; + this.startTime = startTime; + this.task = task; + } + } + private final Map pendingDeletes = new ConcurrentHashMap<>(); + // ★ 圈地工具冷却:UUID → 上次获取时间(package-private供AreaGUIManager访问) + final Map wandCooldownMap = new ConcurrentHashMap<>(); + // ★ 管理员配置输入等待:UUID → 配置key(GUI点击后等待玩家聊天输入) + private final Map pendingConfigInput = new ConcurrentHashMap<>(); + // ★ 添加成员输入等待:UUID → 领地名 + private final Map pendingAddMemberInput = new ConcurrentHashMap<>(); + // ★ 效果管理待处理输入:UUID → [landName, inputType] + public final Map pendingEffectInput = new ConcurrentHashMap<>(); + // ★ 违禁品没收暂存(2026-10-11 违禁品管理面板):UUID → (领地名 → 被没收物品) + // 玩家进入领地被没收时暂存;退出领地时若 returnOnLeave=true 则归还,否则清空(不退) + public final Map>> confiscatedStore + = new ConcurrentHashMap<>(); + // ★ 过户领地待输入新所有者:UUID → [landName] + public final Map pendingTransferInput = new ConcurrentHashMap<>(); + // ★ 用户组配置编辑待输入:UUID → [groupName, field("price"|"maxlands"|"priority")] + public final Map pendingGroupEditInput = new ConcurrentHashMap<>(); + // ★ 领地改名冷却:UUID → 改名时间戳列表(自然滚动窗口,1小时限5次) + private final Map> renameCooldownMap = new ConcurrentHashMap<>(); + private static final int RENAME_LIMIT = 5; + private static final long RENAME_WINDOW_MS = 3600_000L; // 1小时 + + // ★ 全局配置默认值 + private int globalCreatePricePerSqm = 10; // 每㎡创建价格 + private int globalMaxLandsPerPlayer = 5; // 每人最多领地数 + private int globalMaxHomePerPlayer = 5; // 每人最多传送点(home)数量 + private int globalDefaultHeight = 255; // 默认高度 + private int globalRefundRatioPerCent = 0; // 删除领地退费比例(0=关闭退费,100=全额退款,其他=原价×比例%) + // ★ 面积限制(2026-09-29,PHP update_config → config_change 下发): + // 单次扩建/创建超过 free 面积,或边长超过 side 上限 → 转管理员现场审批 + private int globalMaxFreeAreaSqm = 3500; // 单次扩建(含创建选区)免审面积上限(㎡) + private int globalMaxLandSideBlocks = 7000; // 单领地免审边长上限(X/Z 各向,格) + + // 选地 + private static final Material WAND = Material.BLAZE_ROD; + + private final Map pos1 = + new ConcurrentHashMap<>(); + private final Map pos2 = + new ConcurrentHashMap<>(); + private final Set selecting = + ConcurrentHashMap.newKeySet(); + + public boolean hasPos1(UUID uuid) { return pos1.containsKey(uuid); } + public boolean hasPos2(UUID uuid) { return pos2.containsKey(uuid); } + public boolean denyRaid = false; + // ===== 统一文件夹 ===== + private final File rootDir; + private final File whitelistDir; + private final File globalWhitelistFile; + private final File oldDbFile; + private final File newDbFile; + + // 边框显示 + private final Map displayTaskIds = + new ConcurrentHashMap<>(); + private final Map> globalWhitelist + = new HashMap<>(); + private final Map> areaWhitelist + = new HashMap<>(); + private final Map protectedEntities + = new ConcurrentHashMap<>(); + // 区域给予的效果记录(贴标) + private final Map> playerAppliedEffects = new HashMap<>(); + // 延时清理任务 + private static final long MOVE_DEBOUNCE_MS = 100; + // 新:全部改为线程安全 + private final Map playerCurrentArea + = new ConcurrentHashMap<>(); + private final Map> playerMarkedEffects + = new ConcurrentHashMap<>(); + private final Map pendingClearTask + = new ConcurrentHashMap<>(); + private final Map pendingClearArea + = new ConcurrentHashMap<>(); + private final Map lastMoveProcess + = new ConcurrentHashMap<>(); + // ★ denyMove 豁免诊断日志节流(每玩家10秒) + private final Map denyExemptLogTs = new ConcurrentHashMap<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + // ★ 拾取消息节流(5~10秒随机间隔) + private final Map lastPickupMsgTime = new ConcurrentHashMap<>(); + // ★ 效果热更新节流(每2秒最多检查一次) + private final Map lastEffectHotUpdate = new ConcurrentHashMap<>(); + private static final long EFFECT_HOT_UPDATE_INTERVAL_MS = 2000; + private static final String[] PICKUP_MSGS = { + "§c§l[区域防护] §f禁止拾取物品", + "§c§l[区域防护] §f此区域不允许拾取", + "§c§l[区域防护] §f拾取功能已禁用", + "§c§l[区域防护] §f你无法在此区域拾取物品", + "§c§l[区域防护] §f请离开领地后再拾取" + }; + + + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + private final Set modeSwitching + = ConcurrentHashMap.newKeySet(); + @EventHandler(priority = EventPriority.HIGHEST) + public void onGameModeChange( + PlayerGameModeChangeEvent event) { + if (event.isCancelled()) return; + + Player p = event.getPlayer(); + UUID uid = p.getUniqueId(); + + if (modeSwitching.contains(uid)) return; + + String areaName = playerCurrentArea.get(uid); + if (areaName == null) return; + + AreaConfig ac = areas.get(areaName); + if (ac == null) return; + if (ac.enforceGameMode == null + || ac.enforceGameMode.isEmpty()) return; + if (isExemptFromGameMode(p, areaName)) return; + + event.setCancelled(true); + + modeSwitching.add(uid); + try { + GameMode target = GameMode.valueOf( + ac.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + } + } finally { + modeSwitching.remove(uid); + } + + // ★ 切换后发一次中文提示 + p.sendMessage(formatAreaMsg( + "&c该区域强制 " + ac.enforceGameMode + + " 模式,无法切换")); + } + + public Set getAlreadyForced() { + return alreadyForced; + } + + + // ==================== 定时强制游戏模式 ==================== + + private BukkitTask enforceTask; + + /** + * 启动定时强制任务 + * 每秒检查一次所有在线玩家 + */ + private BukkitTask enforceTask1; + + // 记录上次强制模式的玩家(避免重复发消息) + private final Set alreadyForced + = ConcurrentHashMap.newKeySet(); + + public void startEnforceTask() { + if (enforceTask != null) { + enforceTask.cancel(); + } + enforceTask = new BukkitRunnable() { + @Override + public void run() { + for (Player p : Bukkit.getOnlinePlayers()) { + UUID uid = p.getUniqueId(); + String areaName = + playerCurrentArea.get(uid); + if (areaName == null) { + // 不在任何区域,清除标记 + alreadyForced.remove(uid); + continue; + } + + AreaConfig ac = areas.get(areaName); + if (ac == null) continue; + if (ac.enforceGameMode == null + || ac.enforceGameMode + .isEmpty()) continue; + if (isExemptFromGameMode( + p, areaName)) { + alreadyForced.remove(uid); + continue; + } + + GameMode target = + GameMode.valueOf( + ac.enforceGameMode); + if (p.getGameMode() != target) { + modeSwitching.add(uid); + try { + p.setGameMode(target); + } finally { + modeSwitching.remove(uid); + } + + // ★ 只在玩家已被强制过时才发消息 + // 首次不发,后续才提醒 + if (alreadyForced.contains(uid)) { + p.sendMessage(formatAreaMsg( + "&c该区域强制 " + + ac.enforceGameMode + + " 模式,无法切换")); + } + alreadyForced.add(uid); + } else { + // 模式一致,清除标记 + alreadyForced.remove(uid); + } + } + } + }.runTaskTimer(plugin, 20L, 20L); + } + + public void stopEnforceTask() { + if (enforceTask != null) { + enforceTask.cancel(); + enforceTask = null; + } + } + + + + /** + * 检查玩家是否豁免于游戏模式强制 + * 豁免条件:全局白名单 / 区域白名单 / 模式排除名单 + */ + private boolean isExemptFromGameMode( + Player p, String areaName) { + String name = p.getName(); + + // 管理员豁免 + if (isAreaAdmin(p)) { + return true; + } + + // ★ 白名单判定统一走 whiteContains(2026-10-10 起为精确匹配) + if (whiteContains(globalPlayerWhitelist, name)) { + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (whiteContains(aw, name)) { + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + return true; + } + // 领地所有者豁免 + if (ac != null && ac.owner != null && ac.owner.equals(name)) { + return true; + } + return false; + } + + /** + * 玩家进入区域时,如果该区域强制游戏模式, + * 则立即设置(排除豁免玩家) + */ + private void applyEnforcedGameMode(Player p, + String areaName) { + AreaConfig ac = areas.get(areaName); + if (ac == null) { + /* plugin.getLogger().info( + "[防护-调试] applyMode: ac=null, area=" + + areaName);*/ + return; + } + if (ac.enforceGameMode == null + || ac.enforceGameMode.isEmpty()) { + /* plugin.getLogger().info( + "[防护-调试] applyMode: 未配置强制模式, area=" + + areaName);*/ + return; + } + + boolean exempt = isExemptFromGameMode( + p, areaName); + /* plugin.getLogger().info( + "[防护-调试] applyMode: 玩家=" + p.getName() + + " 区域=" + areaName + + " 强制=" + ac.enforceGameMode + + " 当前=" + p.getGameMode() + + " 豁免=" + exempt + + " OP=" + p.isOp() + + " 全白=" + globalPlayerWhitelist + .contains(p.getName()));*/ + + if (exempt) return; + + try { + GameMode target = GameMode.valueOf( + ac.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + /* plugin.getLogger().info( + "[防护] 强制切换 " + p.getName() + + " → " + ac.enforceGameMode);*/ + } + } catch (Exception e) { + /* plugin.getLogger().warning( + "[防护] 强制模式失败: " + + e.getMessage());*/ + } + } + + public AreaProtection(Main plugin) { + this.plugin = plugin; + + // 统一根目录 + rootDir = new File(plugin.getDataFolder(), "区域防护"); + if (!rootDir.exists()) rootDir.mkdirs(); + + // 白名单子目录 + whitelistDir = new File(rootDir, "whitelists"); + if (!whitelistDir.exists()) whitelistDir.mkdirs(); + globalWhitelistFile = new File( + whitelistDir, "全局白名单.txt"); + + // 数据库迁移 + oldDbFile = new File( + plugin.getDataFolder(), "area_effects.db"); + newDbFile = new File(rootDir, "area_effects.db"); + migrateDatabase(); + + // 初始化 + initDatabase(); + writeDefaultConfig(); + migrateTxtToDb(); // ★ 迁移txt文件到数据库 + loadAllAreas(); + loadWhitelists(); + recoverPendingEffects(); + // ★ 封禁/注销领主的领地自动清理(每5分钟扫描) + startBannedOwnerSweepTask(); + } + public void recoverPendingEffects() { + try { + // 1. 遍历所有在线玩家(而不是遍历DB记录) + for (Player p : Bukkit.getOnlinePlayers()) { + UUID uid = p.getUniqueId(); + String playerName = p.getName(); + + // 2. 获取玩家当前所在区域 + AreaConfig currentAc = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + + // 3. 获取该玩家在DB中的所有历史记录 + List recordedAreas = + getPlayerAreas(uid); + + // 4. 清理不在当前区域的旧记录 + for (String recordedArea : recordedAreas) { + if (currentAc == null + || !currentAc.name + .equals(recordedArea)) { + // 玩家不在这个区域了 → 清DB + removePlayerEffects( + uid, recordedArea); + plugin.getLogger().info( + "[防护] 重连: " + playerName + + " 不在" + recordedArea + + ", 清理旧记录"); + } + } + + // 5. 如果在区域中 → 恢复/给予效果 + if (currentAc != null) { + plugin.getLogger().info( + "[防护] 重连: " + playerName + + " 在" + currentAc.name + + ", 恢复效果"); + applyRegionEffects(p, uid, + currentAc.name, currentAc); + } else { + plugin.getLogger().info( + "[防护] 重连: " + playerName + + " 不在任何区域, 跳过"); + } + } + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 恢复效果失败: " + + e.getMessage()); + } + } + + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player p = event.getPlayer(); + UUID uid = p.getUniqueId(); + + // 取消延时清理任务 + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + pendingClearArea.remove(uid); + pendingClearEffects.remove(uid); + } + + // 清标记 + playerMarkedEffects.remove(uid); + playerCurrentArea.remove(uid); + } + + /** + * 玩家上线时恢复区域效果 + * 如果玩家在有效果的区域中,恢复药水效果 + */ + public void onPlayerJoin(Player p) { + if (p == null || !p.isOnline()) return; + UUID uid = p.getUniqueId(); + String playerName = p.getName(); + + // ★ 管理员上线提醒:有超限待审批提案(延迟60tick,等计分板标签恢复) + try { + org.bukkit.Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!p.isOnline() || !isAreaAdmin(p)) return; + pruneExpiredPending(); + if (pendingExpands.isEmpty()) return; + p.sendMessage("§6§l[领地审批] §f当前有 §e" + pendingExpands.size() + + " §f条面积超限待审批申请,用 §a/protect accept §f查看列表"); + }, 60L); + } catch (Exception ignored) {} + + // 获取玩家当前所在区域 + AreaConfig currentAc = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + + if (currentAc != null) { + plugin.getLogger().info( + "[防护] 上线: " + playerName + + " 在" + currentAc.name + + ", 恢复效果"); + applyRegionEffects(p, uid, + currentAc.name, currentAc); + } + } + + +// ==================== 数据库迁移 ==================== + + /** + * 如果旧位置有 db 文件,移到新位置 + * 如果新位置已有,不动 + * 如果旧位置也没有,在新位置创建 + */ + + private void migrateDatabase() { + if (newDbFile.exists()) { + if (oldDbFile.exists() + && !oldDbFile.equals(newDbFile)) { + oldDbFile.delete(); + } + return; + } + if (oldDbFile.exists() + && !oldDbFile.equals(newDbFile)) { + try { + java.nio.file.Files.move( + oldDbFile.toPath(), + newDbFile.toPath(), + java.nio.file.StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 迁移数据库失败: " + + e.getMessage()); + } + } + } + + + // 保存玩家进入区域时给予的效果 + private void savePlayerEffects(UUID uid, String playerName, + String areaName, List effects) { + try { + // 先删除该区域旧记录,避免重复 + PreparedStatement del = dbConnection.prepareStatement( + "DELETE FROM player_effects " + + "WHERE uuid = ? AND area_name = ?"); + del.setString(1, uid.toString()); + del.setString(2, areaName); + del.executeUpdate(); + del.close(); + + PreparedStatement ps = dbConnection.prepareStatement( + "INSERT INTO player_effects " + + "(uuid, player_name, area_name, " + + "effect_type, effect_level, " + + "effect_duration, enter_time) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)"); + long now = System.currentTimeMillis(); + for (PotionEffectType type : effects) { + ps.setString(1, uid.toString()); + ps.setString(2, playerName); + ps.setString(3, areaName); + ps.setString(4, type.getName()); + ps.setInt(5, 0); + ps.setInt(6, 999); + ps.setLong(7, now); + ps.addBatch(); + } + ps.executeBatch(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 保存效果失败: " + e.getMessage()); + } + } + + // 查询玩家在某个区域获得的效果 + private List getPlayerEffects( + UUID uid, String areaName) { + List list = new ArrayList<>(); + try { + PreparedStatement ps = dbConnection.prepareStatement( + "SELECT effect_type FROM player_effects " + + "WHERE uuid = ? AND area_name = ?"); + ps.setString(1, uid.toString()); + ps.setString(2, areaName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + PotionEffectType type = PotionEffectType + .getByName(rs.getString("effect_type")); + if (type != null) list.add(type); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 查询效果失败: " + e.getMessage()); + } + return list; + } + + // 删除玩家在某个区域的效果记录 + private void removePlayerEffects(UUID uid, String areaName) { + try { + PreparedStatement ps = dbConnection.prepareStatement( + "DELETE FROM player_effects " + + "WHERE uuid = ? AND area_name = ?"); + ps.setString(1, uid.toString()); + ps.setString(2, areaName); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 删除效果失败: " + e.getMessage()); + } + } + + // 删除玩家所有效果记录 + private void removeAllPlayerEffects(UUID uid) { + try { + PreparedStatement ps = dbConnection.prepareStatement( + "DELETE FROM player_effects " + + "WHERE uuid = ?"); + ps.setString(1, uid.toString()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 删除全部效果失败: " + e.getMessage()); + } + } + + // 获取玩家所有记录的区域 + private List getPlayerAreas(UUID uid) { + List list = new ArrayList<>(); + try { + PreparedStatement ps = dbConnection.prepareStatement( + "SELECT DISTINCT area_name " + + "FROM player_effects WHERE uuid = ?"); + ps.setString(1, uid.toString()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + list.add(rs.getString("area_name")); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 查询区域失败: " + e.getMessage()); + } + return list; + } + + // 查询所有待处理的效果(跨服重启也能恢复) + private List getAllPendingEffects() { + List list = new ArrayList<>(); + try { + Statement stmt = dbConnection.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT uuid, player_name, area_name, " + + "effect_type FROM player_effects"); + while (rs.next()) { + list.add(new String[]{ + rs.getString("uuid"), + rs.getString("player_name"), + rs.getString("area_name"), + rs.getString("effect_type")}); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 查询全部效果失败: " + e.getMessage()); + } + return list; + } + + + private void writeDefaultConfig() { + File f = new File(rootDir, "末地保护区.txt"); + if (f.exists()) return; + + String endWorld = "world_the_end"; + for (org.bukkit.World w : Bukkit.getWorlds()) { + if (w.getEnvironment() + == org.bukkit.World.Environment.THE_END) { + endWorld = w.getName(); + break; + } + } + + final String ew = endWorld; + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 末地保护区"); + pw.println("起点: " + ew + ",-30000000,-30000000"); + pw.println("终点: " + ew + ",30000000,30000000"); + pw.println("高度范围: 0-255"); + pw.println(); + pw.println("没收物品: BROWN_MUSHROOM,RED_MUSHROOM,MUSHROOM_STEW"); + pw.println("没收提示: 你携带了违禁品「蘑菇」,已被全部没收!"); + pw.println("通报批评: {player} 在末地违规携带蘑菇!"); + pw.println(); + pw.println("禁止放置方块"); + pw.println("禁止破坏方块"); + pw.println("禁止PVP"); + pw.println("禁止袭击"); + pw.println(); + pw.println("效果: 夜视 1 999"); + pw.println("进入提示: 末地保护区 - 请遵守规则"); + pw.println("离开提示: 已离开末地保护区"); + pw.close(); + plugin.getLogger().info("[防护] 生成末地配置, 世界: " + ew); + } catch (IOException ignored) { + } + } + + public void reload() { + loadAllAreas(); + loadWhitelists(); + // 强制输出醒目的中文日志 + plugin.getLogger().info("========================================"); + plugin.getLogger().info("[防护] 已重载 " + areas.size() + " 个区域"); + plugin.getLogger().info("[防护] 已重载白名单"); + plugin.getLogger().info("========================================"); + } + + /** + * ★ 从数据库重新加载area_config配置(Web同步用) + */ + public void reloadAreaConfigFromDb() { + if (dbConnection == null) return; + try { + ResultSet cfgRs = dbConnection.createStatement().executeQuery("SELECT key, value FROM area_config"); + while (cfgRs.next()) { + String key = cfgRs.getString("key"); + String value = cfgRs.getString("value"); + switch (key) { + case "create_price_per_sqm": /* 已在area_config表管理 */ break; + case "max_lands_per_player": break; + case "default_height": break; + case "peace_mode_max_duration": break; + } + } + cfgRs.close(); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 从DB重载配置失败: " + e.getMessage()); + } + } + +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + return formatAreaMsg(raw, null, null); + } + + /** + * 格式化区域消息(支持变量替换) + * 支持括号: () [] {} 【】 <> 《》 + */ + private String formatAreaMsg(String raw, String playerName, String landName) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // ★ 变量替换:支持多种括号格式(DOTALL确保跨行匹配) + // 支持 () [] {} 【】 <> 《》 六种括号 + if (playerName != null) { + java.util.regex.Pattern varPattern = java.util.regex.Pattern.compile( + "[((\\[【<《{]玩家[))\\]】>》}]", + java.util.regex.Pattern.DOTALL); + msg = varPattern.matcher(msg).replaceAll( + java.util.regex.Matcher.quoteReplacement(playerName)); + } + if (landName != null) { + // 同时支持"领地"和"领地名"两种写法 + java.util.regex.Pattern varPattern = java.util.regex.Pattern.compile( + "[((\\[【<《{]领地名?[))\\]】>》}]", + java.util.regex.Pattern.DOTALL); + msg = varPattern.matcher(msg).replaceAll( + java.util.regex.Matcher.quoteReplacement(landName)); + } + + // HTML实体解码(必须在&颜色代码之前,防止&a被误转为§amp;) + msg = decodeHtmlEntities(msg); + + // & → § + for (int i = 0; i <= 9; i++) { + msg = msg.replace("&" + i, "§" + i); + } + for (char c : "abcdefklnor".toCharArray()) { + msg = msg.replace("&" + c, "§" + c); + } + + // 换行 + msg = msg.replace("
", "\n"); + msg = msg.replace("
", "\n"); + msg = msg.replace("
", "\n"); + msg = msg.replace("\\n", "\n"); + + //

和 颜色标签预处理 — 必须在HTML标签转换之前 + msg = processColorTags(msg); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * HTML实体解码 + * 支持: & < > " ' ' '       + */ + private String decodeHtmlEntities(String msg) { + msg = msg.replace("&", "&"); + msg = msg.replace("<", "<"); + msg = msg.replace(">", ">"); + msg = msg.replace(""", "\""); + msg = msg.replace("'", "'"); + msg = msg.replace("'", "'"); + msg = msg.replace("'", "'"); + msg = msg.replace(" ", " "); + msg = msg.replace(" ", " "); + msg = msg.replace(" ", " "); + return msg; + } + + /** + * 处理HTML颜色标签:

和 + * 转换为Minecraft颜色代码 + */ + private String processColorTags(String msg) { + //

text

→ §color text§r + java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile( + "(.*?)

", + java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.DOTALL); + java.util.regex.Matcher pMatcher = pPattern.matcher(msg); + StringBuilder sb = new StringBuilder(); + while (pMatcher.find()) { + String color = pMatcher.group(1); + String text = pMatcher.group(2); + String mcColor = convertHtmlColorToMc(color); + pMatcher.appendReplacement(sb, mcColor + text + "§r"); + } + pMatcher.appendTail(sb); + msg = sb.toString(); + + // text → §color text§r + java.util.regex.Pattern sPattern = java.util.regex.Pattern.compile( + "(.*?)", + java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.DOTALL); + java.util.regex.Matcher sMatcher = sPattern.matcher(msg); + sb = new StringBuilder(); + while (sMatcher.find()) { + String color = sMatcher.group(1); + String text = sMatcher.group(2); + String mcColor = convertHtmlColorToMc(color); + sMatcher.appendReplacement(sb, mcColor + text + "§r"); + } + sMatcher.appendTail(sb); + msg = sb.toString(); + + // 清理残留的

和标签 + msg = msg.replaceAll("

(.*?)

", "$1"); + msg = msg.replaceAll("(.*?)", "$1"); + + return msg; + } + + /** + * HTML颜色值→Minecraft颜色代码 + * 支持: #RRGGBB, 颜色名(red/green/blue等), rgb() + */ + private String convertHtmlColorToMc(String color) { + if (color == null || color.isEmpty()) return "§f"; + String lower = color.toLowerCase().trim(); + + // rgb()函数 + if (lower.startsWith("rgb(")) { + try { + String content = color.substring(color.indexOf('(') + 1, color.lastIndexOf(')')); + String[] parts = content.split(","); + if (parts.length >= 3) { + int r = Integer.parseInt(parts[0].trim()); + int g = Integer.parseInt(parts[1].trim()); + int b = Integer.parseInt(parts[2].trim()); + return convertRgbToMc(r, g, b); + } + } catch (Exception ignored) {} + return "§f"; + } + + // 十六进制 + if (lower.startsWith("#")) { + String hex = lower.substring(1); + try { + if (hex.length() == 3) { + hex = "" + hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2); + } + int r = Integer.parseInt(hex.substring(0, 2), 16); + int g = Integer.parseInt(hex.substring(2, 4), 16); + int b = Integer.parseInt(hex.substring(4, 6), 16); + return convertRgbToMc(r, g, b); + } catch (Exception ignored) {} + return "§f"; + } + + // 颜色名映射 + switch (lower) { + case "red": return "§c"; + case "green": case "lime": return "§a"; + case "blue": return "§9"; + case "yellow": return "§e"; + case "white": return "§f"; + case "black": return "§0"; + case "gray": case "grey": return "§7"; + case "dark_gray": case "dark_grey": return "§8"; + case "orange": case "gold": return "§6"; + case "aqua": case "cyan": return "§b"; + case "pink": case "light_purple": return "§d"; + case "purple": case "dark_purple": return "§5"; + case "dark_red": return "§4"; + case "dark_green": return "§2"; + case "dark_blue": case "navy": return "§1"; + case "dark_aqua": case "dark_cyan": return "§3"; + default: return "§f"; + } + } + + /** + * RGB值→最近的Minecraft颜色代码 + */ + private String convertRgbToMc(int r, int g, int b) { + // MC 16色匹配(取色差最小的) + int[][] mcColors = { + {0, 0, 0}, // §0 black + {0, 0, 170}, // §1 dark_blue + {0, 170, 0}, // §2 dark_green + {0, 170, 170}, // §3 dark_aqua + {170, 0, 0}, // §4 dark_red + {170, 0, 170}, // §5 dark_purple + {255, 170, 0}, // §6 gold + {170, 170, 170}, // §7 gray + {85, 85, 85}, // §8 dark_gray + {85, 85, 255}, // §9 blue + {85, 255, 85}, // §a green + {85, 255, 255}, // §b aqua + {255, 85, 85}, // §c red + {255, 85, 255}, // §d light_purple + {255, 255, 85}, // §e yellow + {255, 255, 255}, // §f white + }; + char[] codes = "0123456789abcdef".toCharArray(); + int bestIdx = 15; + int bestDist = Integer.MAX_VALUE; + for (int i = 0; i < mcColors.length; i++) { + int dr = r - mcColors[i][0]; + int dg = g - mcColors[i][1]; + int db = b - mcColors[i][2]; + int dist = dr * dr + dg * dg + db * db; + if (dist < bestDist) { + bestDist = dist; + bestIdx = i; + } + } + return "§" + codes[bestIdx]; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + * ★ 支持颜色追踪:processColorTags设置的§c等颜色码在标签关闭§r后自动恢复 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + String activeColor = null; // 追踪processColorTags设置的颜色 + int i = 0; + + while (i < msg.length()) { + // ★ 追踪MC颜色码(§0-§f):当processColorTags插入颜色码时记录 + if (msg.charAt(i) == '§' && i + 1 < msg.length()) { + char next = Character.toLowerCase(msg.charAt(i + 1)); + if ("0123456789abcdef".indexOf(next) >= 0) { + activeColor = "" + msg.charAt(i) + next; + } + sb.append(msg.charAt(i)); + sb.append(msg.charAt(i + 1)); + i += 2; + continue; + } + + if (msg.charAt(i) == '<' + && i + 1 < msg.length()) { + if (msg.charAt(i + 1) == '/') { + // 关闭标签
+ int end = msg.indexOf('>', i); + if (end < 0) { + sb.append(msg.charAt(i)); + i++; + continue; + } + String tag = msg.substring(i + 2, end) + .trim().toLowerCase(); + char code = htmlToMc(tag); + if (code != 0 && !stack.isEmpty() + && stack.peek() == code) { + stack.pop(); + // ★ 重置后先恢复颜色,再重新应用外层格式 + sb.append("§r"); + if (activeColor != null) { + sb.append(activeColor); + } + for (char c : stack) { + sb.append("§").append(c); + } + } + i = end + 1; + } else { + // 开放标签 + int end = msg.indexOf('>', i); + if (end < 0) { + sb.append(msg.charAt(i)); + i++; + continue; + } + String tag = msg.substring(i + 1, end) + .trim().toLowerCase(); + char code = htmlToMc(tag); + if (code != 0) { + stack.push(code); + sb.append("§").append(code); + } + i = end + 1; + } + } else { + sb.append(msg.charAt(i)); + i++; + } + } + return sb.toString(); + } + + private char htmlToMc(String tag) { + switch (tag) { + case "b": case "strong": case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": return 'l'; + case "i": case "em": case "var": return 'o'; + case "u": case "ins": return 'n'; + case "s": case "del": case "strike": return 'm'; + case "center": case "p": case "span": case "div": case "font": return 0; // 颜色标签已在processColorTags处理,此处跳过 + default: return 0; + } + } + +// =============== 加载 ==================== + + public void loadAllAreas() { + areas.clear(); + + // ★ 优先从数据库加载 + loadAreasFromDb(); + + // ★ 自动迁移:确保 allow_visitor_teleport 列存在(2026-07-08 新增) + try { + dbConnection.createStatement().execute("ALTER TABLE area_lands ADD COLUMN allow_visitor_teleport INTEGER DEFAULT 0"); + plugin.getLogger().info("[防护] 已添加 allow_visitor_teleport 列"); + } catch (Exception ignored) { /* 列已存在则忽略 */ } + + // ★ 自动迁移:如果DB有数据但txt文件仍存在,迁移txt中的白名单数据到DB后删除txt + if (!areas.isEmpty()) { + migrateTxtFilesToDb(); + } + + // ★ 兼容:如果没有从DB加载到数据,回退到txt(未迁移场景)并自动导入DB + if (areas.isEmpty()) { + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files != null) { + for (File f : files) { + try { + String name = f.getName().replace(".txt", ""); + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + // ★ 自动写入数据库 + saveAreaToDb(ac); + plugin.getLogger().info("[防护] 自动迁移txt→DB: " + name); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 加载失败: " + f.getName()); + } + } + // ★ 迁移完成后删除所有txt文件 + if (!areas.isEmpty()) { + for (File f : files) { + if (f.delete()) { + plugin.getLogger().info("[防护] 已删除迁移后的txt: " + f.getName()); + } + } + plugin.getLogger().info("[防护] txt迁移完成,共导入 " + areas.size() + " 个领地"); + } + } + } + } + + /** + * 迁移txt文件到数据库(如果txt存在但DB已有数据,清理txt) + */ + private void migrateTxtFilesToDb() { + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files == null || files.length == 0) return; + + plugin.getLogger().info("[防护] 检测到 " + files.length + " 个残留txt文件,开始清理..."); + for (File f : files) { + String name = f.getName().replace(".txt", ""); + // 如果txt对应的领地不在DB中,先导入 + if (!areas.containsKey(name)) { + try { + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + saveAreaToDb(ac); + plugin.getLogger().info("[防护] 迁移txt→DB: " + name); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 迁移失败: " + f.getName()); + } + } + // 删除txt + if (f.delete()) { + plugin.getLogger().info("[防护] 已删除: " + f.getName()); + } + } + } + + /** + * 从数据库加载所有领地配置 + */ + private void loadAreasFromDb() { + if (dbConnection == null) return; + try { + Statement stmt = dbConnection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM area_lands"); + int count = 0; + while (rs.next()) { + AreaConfig ac = new AreaConfig(); + ac.name = rs.getString("name"); + ac.owner = rs.getString("owner"); + ac.world = rs.getString("world"); + ac.x1 = rs.getInt("x1"); + ac.z1 = rs.getInt("z1"); + ac.x2 = rs.getInt("x2"); + ac.z2 = rs.getInt("z2"); + ac.yMin = rs.getInt("y_min"); + ac.yMax = rs.getInt("y_max"); + ac.confiscateItems = splitToList(rs.getString("confiscate_items")); + ac.denyUseItems = splitToList(rs.getString("deny_use_items")); + ac.giveEffects = parseEffectsString(rs.getString("give_effects")); + ac.clearEffects = splitToList(rs.getString("clear_effects")); + ac.clearAllBadEffects = rs.getInt("clear_all_bad") == 1; + ac.punishCommands = splitToList(rs.getString("punish_commands")); + ac.denyBlockPlace = rs.getInt("deny_block_place") == 1; + ac.denyBlockBreak = rs.getInt("deny_block_break") == 1; + ac.denyPVP = rs.getInt("deny_pvp") == 1; + ac.denyFallDamage = rs.getInt("deny_fall_damage") == 1; + ac.denyHunger = rs.getInt("deny_hunger") == 1; + ac.denyAllDamage = rs.getInt("deny_all_damage") == 1; + ac.allowDrop = rs.getInt("deny_drop") == 1; + ac.denyMount = rs.getInt("deny_mount") == 1; + ac.denyEnderPearl = rs.getInt("deny_ender_pearl") == 1; + ac.denyBow = rs.getInt("deny_bow") == 1; + ac.denyPotion = rs.getInt("deny_potion") == 1; + ac.denyExplosion = rs.getInt("deny_explosion") == 1; + ac.denyRaid = rs.getInt("deny_raid") == 1; + ac.denyFireSpread = rs.getInt("deny_fire_spread") == 1; + ac.denyAllEffects = rs.getInt("deny_all_effects") == 1; + ac.denyItemFrame = rs.getInt("deny_item_frame") == 1; + try { ac.denyEntityInteract = rs.getInt("deny_entity_interact") == 1; } catch (Exception ignored) {} + try { ac.denyMove = rs.getInt("deny_move") == 1; } catch (Exception ignored) {} + try { ac.allowPickup = rs.getInt("deny_pickup") == 1; } catch (Exception ignored) {} + try { ac.denyFire = rs.getInt("deny_fire") == 1; } catch (Exception ignored) {} + try { ac.denyThrownProjectiles = rs.getInt("deny_thrown_projectiles") == 1; } catch (Exception ignored) {} + try { ac.denyGlowing = rs.getInt("deny_glowing") == 1; } catch (Exception ignored) {} + try { ac.denyRedstoneInteraction = rs.getInt("deny_redstone_interaction") == 1; } catch (Exception ignored) {} + try { ac.denyDoorInteraction = rs.getInt("deny_door_interaction") == 1; } catch (Exception ignored) {} + try { ac.denyNoteblockJukebox = rs.getInt("deny_noteblock_jukebox") == 1; } catch (Exception ignored) {} + try { ac.denyLead = rs.getInt("deny_lead") == 1; } catch (Exception ignored) {} + try { ac.denyCropHarvest = rs.getInt("deny_crop_harvest") == 1; } catch (Exception ignored) {} + try { ac.denyFarmlandTrample = rs.getInt("deny_farmland_trample") == 1; } catch (Exception ignored) {} + try { ac.denyEnderTeleport = rs.getInt("deny_ender_teleport") == 1; } catch (Exception ignored) {} + try { ac.denyWoolShear = rs.getInt("deny_wool_shear") == 1; } catch (Exception ignored) {} + try { ac.denyAnimalFeeding = rs.getInt("deny_animal_feeding") == 1; } catch (Exception ignored) {} + try { ac.denyContainer = rs.getInt("deny_container") == 1; } catch (Exception ignored) {} + try { ac.denyMobAttack = rs.getInt("deny_mob_attack") == 1; } catch (Exception ignored) {} + try { ac.denySignEdit = rs.getInt("deny_sign_edit") == 1; } catch (Exception ignored) {} + try { ac.denySpawnEgg = rs.getInt("deny_spawn_egg") == 1; } catch (Exception ignored) {} + try { ac.denyWax = rs.getInt("deny_wax") == 1; } catch (Exception ignored) {} + try { ac.denyVillagerTrade = rs.getInt("deny_villager_trade") == 1; } catch (Exception ignored) {} + try { ac.badEffects = parseEffectsString(rs.getString("bad_effects")); } catch (Exception ignored) { ac.badEffects = new ArrayList<>(); } + try { ac.confiscateCount = rs.getInt("confiscate_count"); } catch (Exception ignored) {} + try { ac.returnOnLeave = rs.getInt("return_on_leave") == 1; } catch (Exception ignored) {} + try { ac.parentId = rs.getInt("parent_id"); } catch (Exception ignored) { ac.parentId = 0; } // ★ 第57批续21 + try { ac.warpX = rs.getDouble("warp_x"); } catch (Exception ignored) {} + try { ac.warpY = rs.getDouble("warp_y"); } catch (Exception ignored) {} + try { ac.warpZ = rs.getDouble("warp_z"); } catch (Exception ignored) {} + try { ac.warpYaw = rs.getFloat("warp_yaw"); } catch (Exception ignored) {} + try { ac.warpPitch = rs.getFloat("warp_pitch"); } catch (Exception ignored) {} + try { ac.warpWorld = rs.getString("warp_world"); } catch (Exception ignored) {} + try { ac.isPublicBuilding = rs.getInt("is_public_building") == 1; } catch (Exception ignored) {} + try { ac.allowVisitorTeleport = rs.getInt("allow_visitor_teleport") == 1; } catch (Exception ignored) {} + ac.peaceMode = rs.getInt("peace_mode") == 1; + ac.peaceModeDuration = rs.getInt("peace_mode_duration") * 1000; // 转毫秒 + ac.peaceWhitelist = new HashSet<>(splitToList(rs.getString("peace_whitelist"))); + String gm = rs.getString("enforce_game_mode"); + ac.enforceGameMode = (gm != null && !gm.isEmpty()) ? gm : null; + ac.modeExempt = new HashSet<>(splitToList(rs.getString("mode_exempt"))); + ac.enterMsg = rs.getString("enter_msg"); + ac.leaveMsg = rs.getString("leave_msg"); + ac.confiscateMsg = rs.getString("confiscate_msg"); + ac.enableAnnounce = rs.getInt("enable_announce") == 1; + ac.announceTemplate = rs.getString("announce_template"); + ac.txtContent = rs.getString("txt_content"); + try { ac.createCost = rs.getInt("create_cost"); } catch (Exception ignored) {} + areas.put(ac.name, ac); + count++; + } + rs.close(); + stmt.close(); + if (count > 0) { + plugin.getLogger().info("[防护] 从数据库加载" + count + "个领地"); + } + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 从DB加载领地失败: " + e.getMessage()); + } + } + + /** + * 逗号分隔字符串或JSON数组 → List + * 兼容: "a,b,c" 或 ["a","b","c"] + */ + private List splitToList(String s) { + List result = new ArrayList<>(); + if (s == null || s.isEmpty()) return result; + String trimmed = s.trim(); + // ★ 支持JSON数组格式(PHP端存储格式),如 ["速度","急迫"] + // (PHP 端 JSON 存储时中文会被转义成 U+XXXX 形式,例如 "试" 对应 U+8BD5,此处展示的是 PHP 侧原样) + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + try { + String inner = trimmed.substring(1, trimmed.length() - 1); + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("\"(?:\\\\.|[^\"\\\\])*\"") + .matcher(inner); + while (m.find()) { + String raw = m.group(); + String cleaned = unescapeJsonString(raw.substring(1, raw.length() - 1)); + cleaned = cleaned.replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + if (!cleaned.isEmpty()) result.add(cleaned); + } + if (!result.isEmpty()) return result; + } catch (Exception ignored) { + // 回退到逗号分隔 + } + } + for (String item : s.split(",")) { + String itemTrimmed = item.trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + if (!itemTrimmed.isEmpty()) result.add(itemTrimmed); + } + return result; + } + + /** + * 简单JSON字符串unescape(支持 \\ \" \/ \b \f \n \r \t 以及反斜杠u开头的Unicode四位十六进制转义) + */ + private String unescapeJsonString(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i + 1 < s.length()) { + char next = s.charAt(i + 1); + switch (next) { + case '"': case '\\': case '/': sb.append(next); i++; break; + case 'b': sb.append('\b'); i++; break; + case 'f': sb.append('\f'); i++; break; + case 'n': sb.append('\n'); i++; break; + case 'r': sb.append('\r'); i++; break; + case 't': sb.append('\t'); i++; break; + case 'u': + if (i + 5 < s.length()) { + try { + int code = Integer.parseInt(s.substring(i + 2, i + 6), 16); + sb.append((char) code); + i += 5; + } catch (NumberFormatException e) { + sb.append(c); + } + } else { + sb.append(c); + } + break; + default: + sb.append(next); + i++; + break; + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 解析效果存储字符串 → List + * 格式: 管道格式 "效果名:等级:秒数|效果名:等级:秒数" + * 或 JSON格式 [["效果名",等级,秒数],...] + */ + private List parseEffectsString(String s) { + List result = new ArrayList<>(); + if (s == null || s.isEmpty()) return result; + + String trimmed = s.trim(); + // ★ 支持JSON格式(PHP端存储格式)[["效果名",等级,秒数],...] + if (trimmed.startsWith("[")) { + try { + String inner = trimmed.substring(1, trimmed.length() - 1); + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("\\[\\s*\"((?:\\\\.|[^\"\\\\])*)\"\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\]") + .matcher(inner); + while (m.find()) { + String name = unescapeJsonString(m.group(1)) + .replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + if (!name.isEmpty()) { + result.add(new String[]{name, m.group(2), m.group(3)}); + } + } + if (!result.isEmpty()) return result; + } catch (Exception ignored) { + // JSON解析失败,回退到管道格式 + } + } + + // ★ 管道格式(Java端GUI存储格式): 效果名:等级:秒数|效果名:等级:秒数 + for (String part : s.split("\\|")) { + String[] pieces = part.split(":"); + if (pieces.length >= 3) { + String name = pieces[0].trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + result.add(new String[]{name, pieces[1], pieces[2]}); + } + } + return result; + } + + + private void initDatabase() { + try { + dbConnection = DriverManager.getConnection( + "jdbc:sqlite:" + newDbFile.getAbsolutePath()); + Statement stmt = dbConnection.createStatement(); + + // 效果记录表(原有) + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS player_effects (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "uuid TEXT NOT NULL," + + "player_name TEXT," + + "area_name TEXT NOT NULL," + + "effect_type TEXT NOT NULL," + + "effect_level INTEGER DEFAULT 0," + + "effect_duration INTEGER DEFAULT 999," + + "enter_time INTEGER NOT NULL)"); + stmt.executeUpdate( + "CREATE INDEX IF NOT EXISTS idx_uuid " + + "ON player_effects(uuid)"); + + // ★ 领地主表(替代txt文件存储) + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS area_lands (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT UNIQUE NOT NULL," + + "owner TEXT NOT NULL DEFAULT ''," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "y_min INTEGER DEFAULT 0," + + "y_max INTEGER DEFAULT 255," + + "confiscate_items TEXT DEFAULT ''," + + "deny_use_items TEXT DEFAULT ''," + + "give_effects TEXT DEFAULT ''," + + "bad_effects TEXT DEFAULT ''," + + "clear_effects TEXT DEFAULT ''," + + "clear_all_bad INTEGER DEFAULT 0," + + "punish_commands TEXT DEFAULT ''," + + "deny_block_place INTEGER DEFAULT 0," + + "deny_block_break INTEGER DEFAULT 0," + + "deny_container INTEGER DEFAULT 0," + + "deny_pvp INTEGER DEFAULT 0," + + "deny_fall_damage INTEGER DEFAULT 0," + + "deny_hunger INTEGER DEFAULT 0," + + "deny_all_damage INTEGER DEFAULT 0," + + "deny_drop INTEGER DEFAULT 0," + + "deny_mount INTEGER DEFAULT 0," + + "deny_ender_pearl INTEGER DEFAULT 0," + + "deny_bow INTEGER DEFAULT 0," + + "deny_potion INTEGER DEFAULT 0," + + "deny_explosion INTEGER DEFAULT 0," + + "deny_raid INTEGER DEFAULT 0," + + "deny_fire_spread INTEGER DEFAULT 0," + + "deny_all_effects INTEGER DEFAULT 0," + + "deny_item_frame INTEGER DEFAULT 0," + + "deny_entity_interact INTEGER DEFAULT 0," + + "deny_move INTEGER DEFAULT 0," + + "deny_pickup INTEGER DEFAULT 0," + + "deny_fire INTEGER DEFAULT 0," + + "peace_mode INTEGER DEFAULT 0," + + "peace_mode_duration INTEGER DEFAULT 5," + + "peace_whitelist TEXT DEFAULT ''," + + "enforce_game_mode TEXT DEFAULT ''," + + "mode_exempt TEXT DEFAULT ''," + + "enter_msg TEXT DEFAULT ''," + + "leave_msg TEXT DEFAULT ''," + + "confiscate_msg TEXT DEFAULT ''," + + "enable_announce INTEGER DEFAULT 0," + + "announce_template TEXT DEFAULT ''," + + "txt_content TEXT DEFAULT ''," + + "created_at INTEGER NOT NULL," + + "parent_id INTEGER DEFAULT 0)"); + + // ★ 领地权限表(管理员/用户/访客三级权限) + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS area_land_permissions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "land_id INTEGER NOT NULL," + + "player_name TEXT NOT NULL," + + "role TEXT NOT NULL DEFAULT 'visitor'," + + "permissions TEXT DEFAULT ''," + + "granted_at INTEGER NOT NULL," + + "expires_at INTEGER DEFAULT 0," + + "UNIQUE(land_id, player_name))"); + stmt.executeUpdate( + "CREATE INDEX IF NOT EXISTS idx_land_id " + + "ON area_land_permissions(land_id)"); + + // ★ 权限商店表 + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS area_permission_shop (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "land_id INTEGER NOT NULL," + + "seller TEXT NOT NULL," + + "permission TEXT NOT NULL," + + "price INTEGER NOT NULL," + + "duration INTEGER NOT NULL DEFAULT 86400," + + "created_at INTEGER NOT NULL," + + "status TEXT DEFAULT 'active'," + + "buyer TEXT DEFAULT ''," + + "bought_at INTEGER DEFAULT 0)"); + + // ★ 全局配置表 + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS area_config (" + + "key TEXT PRIMARY KEY," + + "value TEXT NOT NULL)"); + + // 添加可能缺失的列(兼容旧数据库) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_move INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_pickup INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_fire INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN txt_content TEXT DEFAULT ''"); } catch (Exception ignored) {} + // ★ 新增权限列 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_thrown_projectiles INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_glowing INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_redstone_interaction INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_door_interaction INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_noteblock_jukebox INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_lead INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_crop_harvest INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_farmland_trample INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_ender_teleport INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wool_shear INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_animal_feeding INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 传送点列 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_x REAL DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_y REAL DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_z REAL DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_yaw REAL DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_pitch REAL DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN warp_world TEXT DEFAULT ''"); } catch (Exception ignored) {} + // ★ 容器管理权限列(旧表缺失会导致INSERT OR REPLACE失败) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_container INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 玩家攻击生物权限列 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_mob_attack INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 告示牌编辑权限列 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_sign_edit INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 公共建筑标记 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN is_public_building INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 流体阻止 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_fluid INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 二级权限:实体交互(受一级放置/破坏总开关影响) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_entity_interact INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN create_cost INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 生物蛋权限列(上级权限:放置方块) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_spawn_egg INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 涂蜡/刮蜡权限列(上级权限:破坏方块/放置方块) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wax INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 村民交易权限列(2026-10-10:领地主控制玩家能否与领地内村民交易) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_villager_trade INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN bad_effects TEXT DEFAULT ''"); } catch (Exception ignored) {} + // ★ 违禁品管理面板(2026-10-11):没收计数 + 退出是否归还 + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN confiscate_count INTEGER DEFAULT 0"); } catch (Exception ignored) {} + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN return_on_leave INTEGER DEFAULT 0"); } catch (Exception ignored) {} + // ★ 第57批续21 子领地列:旧库补列(已存在则忽略) + try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN parent_id INTEGER DEFAULT 0"); } catch (Exception ignored) {} + + // ★ 全局配置默认值 + try { + ResultSet checkRs = stmt.executeQuery("SELECT COUNT(*) FROM area_config"); + int configCount = 0; + if (checkRs.next()) configCount = checkRs.getInt(1); + checkRs.close(); + if (configCount == 0) { + stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('create_price_per_sqm', '10')"); + stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_lands_per_player', '5')"); + stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_home_per_player', '5')"); + stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('default_height', '255')"); + stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('peace_mode_max_duration', '3600')"); + } + // ★ 退费比例配置(新键,已存在则忽略) + try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('refund_ratio_per_cent', '0')"); } catch (Exception ignored) {} + // ★ 面积限制默认值(新键,已存在则忽略) + try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_free_area_sqm', '3500')"); } catch (Exception ignored) {} + try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('max_land_side_blocks', '7000')"); } catch (Exception ignored) {} + // 读取全局配置 + ResultSet cfgRs = stmt.executeQuery("SELECT key, value FROM area_config"); + while (cfgRs.next()) { + String k = cfgRs.getString("key"); + String v = cfgRs.getString("value"); + try { + switch (k) { + case "create_price_per_sqm": globalCreatePricePerSqm = Integer.parseInt(v); break; + case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(v); break; + case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(v); break; + case "default_height": globalDefaultHeight = Integer.parseInt(v); break; + case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(v); break; + case "max_free_area_sqm": globalMaxFreeAreaSqm = Integer.parseInt(v); break; + case "max_land_side_blocks": globalMaxLandSideBlocks = Integer.parseInt(v); break; + } + } catch (NumberFormatException ignored) {} + } + cfgRs.close(); + } catch (Exception ignored) {} + + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== txt→db 迁移 ==================== + + /** + * 将现有的txt文件区域配置迁移到数据库 + * 只在数据库中没有记录时执行(首次升级) + */ + private void migrateTxtToDb() { + try { + // 检查数据库是否已有领地数据 + Statement checkStmt = dbConnection.createStatement(); + ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM area_lands"); + int dbCount = 0; + if (rs.next()) dbCount = rs.getInt(1); + rs.close(); + checkStmt.close(); + + // 如果数据库已有数据,跳过迁移 + if (dbCount > 0) { + plugin.getLogger().info("[防护] 数据库已有" + dbCount + "个领地,跳过迁移"); + return; + } + + // 扫描txt文件 + File[] txtFiles = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (txtFiles == null || txtFiles.length == 0) { + plugin.getLogger().info("[防护] 无txt文件需要迁移"); + return; + } + + int migrated = 0; + PreparedStatement insertStmt = dbConnection.prepareStatement( + "INSERT INTO area_lands (name, owner, world, x1, z1, x2, z2, y_min, y_max, " + + "confiscate_items, deny_use_items, give_effects, clear_effects, clear_all_bad, " + + "punish_commands, deny_block_place, deny_block_break, deny_pvp, deny_fall_damage, " + + "deny_hunger, deny_all_damage, deny_drop, deny_mount, deny_ender_pearl, " + + "deny_bow, deny_potion, deny_explosion, deny_raid, deny_fire_spread, " + + "deny_all_effects, deny_item_frame, deny_entity_interact, deny_move, deny_pickup, deny_fire, " + + "peace_mode, peace_mode_duration, " + + "peace_whitelist, enforce_game_mode, mode_exempt, enter_msg, leave_msg, " + + "confiscate_msg, enable_announce, announce_template, txt_content, created_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + for (File f : txtFiles) { + try { + String name = f.getName().replace(".txt", ""); + // 读取原始txt内容 + String txtContent = new String( + java.nio.file.Files.readAllBytes(f.toPath()), + StandardCharsets.UTF_8); + + // 解析AreaConfig + AreaConfig ac = parseArea(name, f); + + // 写入数据库 + insertStmt.setString(1, name); + insertStmt.setString(2, ""); // owner(txt迁移无owner) + insertStmt.setString(3, ac.world); + insertStmt.setInt(4, ac.x1); + insertStmt.setInt(5, ac.z1); + insertStmt.setInt(6, ac.x2); + insertStmt.setInt(7, ac.z2); + insertStmt.setInt(8, ac.yMin); + insertStmt.setInt(9, ac.yMax); + insertStmt.setString(10, String.join(",", ac.confiscateItems)); + insertStmt.setString(11, String.join(",", ac.denyUseItems)); + insertStmt.setString(12, effectsToString(ac.giveEffects)); + insertStmt.setString(13, String.join(",", ac.clearEffects)); + insertStmt.setInt(14, ac.clearAllBadEffects ? 1 : 0); + insertStmt.setString(15, String.join("|", ac.punishCommands)); + insertStmt.setInt(16, ac.denyBlockPlace ? 1 : 0); + insertStmt.setInt(17, ac.denyBlockBreak ? 1 : 0); + insertStmt.setInt(18, ac.denyPVP ? 1 : 0); + insertStmt.setInt(19, ac.denyFallDamage ? 1 : 0); + insertStmt.setInt(20, ac.denyHunger ? 1 : 0); + insertStmt.setInt(21, ac.denyAllDamage ? 1 : 0); + insertStmt.setInt(22, ac.allowDrop ? 1 : 0); + insertStmt.setInt(23, ac.denyMount ? 1 : 0); + insertStmt.setInt(24, ac.denyEnderPearl ? 1 : 0); + insertStmt.setInt(25, ac.denyBow ? 1 : 0); + insertStmt.setInt(26, ac.denyPotion ? 1 : 0); + insertStmt.setInt(27, ac.denyExplosion ? 1 : 0); + insertStmt.setInt(28, ac.denyRaid ? 1 : 0); + insertStmt.setInt(29, ac.denyFireSpread ? 1 : 0); + insertStmt.setInt(30, ac.denyAllEffects ? 1 : 0); + insertStmt.setInt(31, ac.denyItemFrame ? 1 : 0); + insertStmt.setInt(32, ac.denyEntityInteract ? 1 : 0); + insertStmt.setInt(33, ac.denyMove ? 1 : 0); + insertStmt.setInt(34, ac.allowPickup ? 1 : 0); + insertStmt.setInt(35, ac.denyFire ? 1 : 0); + insertStmt.setInt(36, ac.peaceMode ? 1 : 0); + insertStmt.setInt(37, ac.peaceModeDuration / 1000); // 存秒 + insertStmt.setString(38, String.join(",", ac.peaceWhitelist)); + insertStmt.setString(39, ac.enforceGameMode != null ? ac.enforceGameMode : ""); + insertStmt.setString(40, String.join(",", ac.modeExempt)); + insertStmt.setString(41, ac.enterMsg); + insertStmt.setString(42, ac.leaveMsg); + insertStmt.setString(43, ac.confiscateMsg); + insertStmt.setInt(44, ac.enableAnnounce ? 1 : 0); + insertStmt.setString(45, ac.announceTemplate); + insertStmt.setString(46, txtContent); + insertStmt.setLong(47, System.currentTimeMillis() / 1000); + insertStmt.executeUpdate(); + migrated++; + plugin.getLogger().info("[防护] 迁移txt→db: " + name); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 迁移失败: " + f.getName() + " - " + e.getMessage()); + } + } + insertStmt.close(); + + if (migrated > 0) { + plugin.getLogger().info("[防护] ★ 迁移完成,共" + migrated + "个区域已写入数据库"); + // 创建迁移完成标记文件,防止重复迁移 + File marker = new File(rootDir, ".db_migrated"); + marker.createNewFile(); + // 创建owner待分配标记文件 + File ownerMarker = new File(rootDir, ".need_owner_assign"); + ownerMarker.createNewFile(); + } + } catch (Exception e) { + plugin.getLogger().warning("[防护] 迁移过程异常: " + e.getMessage()); + } + } + + /** + * 将效果列表转为存储字符串 + * 格式: 效果名:等级:秒数|效果名:等级:秒数 + */ + private String effectsToString(List effects) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < effects.size(); i++) { + String[] e = effects.get(i); + if (i > 0) sb.append("|"); + sb.append(e[0]).append(":").append(e[1]).append(":").append(e[2]); + } + return sb.toString(); + } + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 Bukkit 能识别的英文名 + */ + private String resolveEffectName(String name) { + if (name == null) return null; + String n = name.trim(); + // 如果已经是英文或命名空间格式,直接用 + if (n.contains(":") + || n.equals(n.toUpperCase())) { + return n; + } + // 中文 → 英文映射 + switch (n) { + case "伤害吸收": return "ABSORPTION"; + case "生命提升": return "HEALTH_BOOST"; + case "抗性提升": return "DAMAGE_RESISTANCE"; + case "抗火": return "FIRE_RESISTANCE"; + case "夜视": return "NIGHT_VISION"; + case "力量": return "INCREASE_DAMAGE"; + case "速度": return "SPEED"; + case "急迫": return "FAST_DIGGING"; + case "跳跃提升": return "JUMP"; + case "再生": return "REGENERATION"; + case "生命恢复": return "REGENERATION"; + case "治疗": return "HEAL"; + case "瞬间治疗": return "HEAL"; + case "伤害药水": return "HARM"; + case "瞬间伤害": return "HARM"; + case "虚弱": return "WEAKNESS"; + case "中毒": return "POISON"; + case "缓慢": return "SLOW"; + case "挖掘疲劳": return "SLOW_DIGGING"; + case "失明": return "BLINDNESS"; + case "隐身": return "INVISIBILITY"; + case "发光": return "GLOWING"; + // 村庄英雄 + case "村庄英雄": return "HERO_OF_THE_VILLAGE"; + case "幸运": return "LUCK"; + case "不幸": return "UNLUCK"; + case "水下呼吸": return "WATER_BREATHING"; + case "水下速掘": return "DOLPHINS_GRACE"; + case "缓降": return "LEVITATION"; + case "漂浮": return "LEVITATION"; + case "饱和": return "SATURATION"; + // 无效效果 + case "伤害": return "HARM"; + case "治疗药水": return "HEAL"; + default: + // 尝试直接用英文匹配 + return n; + } + } + + /** + * 全量容错:把全角字符统一转半角, + * 去掉零宽字符/不可见字符, + * 让用户怎么写都能正确解析。 + */ + private String normalizeLine(String input) { + if (input == null || input.isEmpty()) return ""; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + // 跳过零宽字符和不可见字符 + if (c == UnicodeChars.ZERO_WIDTH_SPACE // 零宽空格 + || c == UnicodeChars.ZERO_WIDTH_NON_JOINER // 零宽非连接 + || c == UnicodeChars.ZERO_WIDTH_JOINER // 零宽连接 + || c == UnicodeChars.BOM // BOM + || c == UnicodeChars.NBSP // 不间断空格 + || c == UnicodeChars.LINE_SEPARATOR // 行分隔符 + || c == UnicodeChars.PARAGRAPH_SEPARATOR // 段分隔符 + || c == UnicodeChars.LTR_MARK // LTR标记 + || c == UnicodeChars.RTL_MARK // RTL标记 + || c == UnicodeChars.WORD_JOINER // 字连接符 + || (c >= UnicodeChars.SPACE_U2000 && c <= UnicodeChars.SPACE_U200A)) { // 各种空格 + continue; + } + + // 全角→半角映射 + if (c >= '!' && c <= '~') { + c = (char) (c - 0xFEE0); + } + + // 全角空格→半角空格 + if (c == UnicodeChars.IDEOGRAPHIC_SPACE) { + c = ' '; + } + + sb.append(c); + } + + return sb.toString(); + } + + + private AreaConfig parseArea(String name, File f) + throws IOException { + AreaConfig ac = new AreaConfig(); + ac.name = name; + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + boolean inBlockComment = false; + boolean inHtmlComment = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; +// ===== 全量容错预处理:全角→半角,去零宽字符 ===== + line = normalizeLine(line); + if (line.isEmpty()) continue; +// ===== 暴力调试:输出所有包含关键词的行 ===== + if (line.indexOf("游戏") >= 0 + || line.indexOf("模式") >= 0 + || line.indexOf("生存") >= 0 + || line.indexOf("创造") >= 0 + || line.indexOf("排除") >= 0 + || line.indexOf("豁免") >= 0 + || line.indexOf("强制") >= 0 + || line.indexOf("GameMode") >= 0 + || line.indexOf("SURVIVAL") >= 0 + || line.indexOf("CREATIVE") >= 0) { + StringBuilder hex = new StringBuilder(); + for (int i = 0; i < line.length(); i++) { + hex.append(String.format("U+%04X ", (int) line.charAt(i))); + } + /* plugin.getLogger().info("[暴力调试] 行=[" + line + + "] 长度=" + line.length() + + " 字符码=" + hex.toString());*/ + } +// ===== 兜底扫描:确保和平白名单一定能解析 ===== + if (line.indexOf("白名单") >= 0 + || line.indexOf("排除名单") >= 0 + || line.indexOf("模式排除") >= 0 + || line.indexOf("模式豁免") >= 0) { + int ci = line.indexOf(':'); + if (ci < 0) ci = line.indexOf(':'); + if (ci < 0) ci = line.indexOf('='); + if (ci < 0) ci = line.indexOf(' '); + + // 和平白名单 + if (line.indexOf("和平") >= 0 + && line.indexOf("白名单") >= 0 + && ci >= 0) { + String rest = line.substring(ci + 1).trim(); + rest = rest.replaceAll("[,,;;|、/]", ","); + for (String s : rest.split(",")) { + String nm = s.trim(); + if (!nm.isEmpty()) ac.peaceWhitelist.add(nm); + } + plugin.getLogger().info("[解析兜底] 和平白名单=" + + ac.peaceWhitelist); + } + + // 排除名单 + if (line.indexOf("排除名单") >= 0 + || line.indexOf("模式排除") >= 0 + || line.indexOf("模式豁免") >= 0) { + if (ci >= 0) { + String rest = line.substring(ci + 1).trim(); + rest = rest.replaceAll("[,,;;|、/]", ","); + for (String s : rest.split(",")) { + String nm = s.trim(); + if (!nm.isEmpty()) ac.modeExempt.add(nm); + } + plugin.getLogger().info("[解析兜底] 排除名单=" + + ac.modeExempt); + } + } + } +// ===== 兜底结束 ===== + + if (inBlockComment) { + int endIdx = line.indexOf("*/"); + if (endIdx >= 0) { + inBlockComment = false; + line = line.substring(endIdx + 2).trim(); + if (line.isEmpty()) continue; + } else { + continue; + } + } + if (line.startsWith("/*")) { + int endIdx = line.indexOf("*/", 2); + if (endIdx >= 0) { + line = line.substring(endIdx + 2).trim(); + if (line.isEmpty()) continue; + } else { + inBlockComment = true; + continue; + } + } + if (inHtmlComment) { + int endIdx = line.indexOf("-->"); + if (endIdx >= 0) { + inHtmlComment = false; + line = line.substring(endIdx + 3).trim(); + if (line.isEmpty()) continue; + } else { + continue; + } + } + if (line.startsWith("", 4); + if (endIdx >= 0) { + line = line.substring(endIdx + 3).trim(); + if (line.isEmpty()) continue; + } else { + inHtmlComment = true; + continue; + } + } + if (line.startsWith("#") || line.startsWith("//")) { + continue; + } + int hashIdx = line.indexOf("#"); + if (hashIdx > 0) { + line = line.substring(0, hashIdx).trim(); + } + int slashIdx = line.indexOf("//"); + if (slashIdx > 0) { + line = line.substring(0, slashIdx).trim(); + } + if (line.isEmpty()) continue; + + if (line.startsWith("起点:")) { + String[] pp = line.substring("起点:".length()) + .trim().split(","); + if (pp.length == 3) { + ac.world = pp[0].trim(); + ac.x1 = Integer.parseInt(pp[1].trim()); + ac.z1 = Integer.parseInt(pp[2].trim()); + } + } else if (line.startsWith("终点:")) { + String[] pp = line.substring("终点:".length()) + .trim().split(","); + if (pp.length == 3) { + ac.world = pp[0].trim(); + ac.x2 = Integer.parseInt(pp[1].trim()); + ac.z2 = Integer.parseInt(pp[2].trim()); + } + } else if (line.startsWith("高度范围:")) { + String[] pp = line.substring("高度范围:".length()) + .trim().split("-"); + if (pp.length == 2) { + ac.yMin = Integer.parseInt(pp[0].trim()); + ac.yMax = Integer.parseInt(pp[1].trim()); + } + } else if (line.startsWith("没收物品:")) { + for (String s : line.substring("没收物品:".length()) + .trim().split("[,,]")) + ac.confiscateItems.add( + s.trim().toUpperCase()); + } else if (line.startsWith("禁止使用物品:")) { + for (String s : line.substring("禁止使用物品:".length()) + .trim().split("[,,]")) + ac.denyUseItems.add( + s.trim().toUpperCase()); + } else if (line.equals("禁止放置方块")) { + ac.denyBlockPlace = true; + } else if (line.equals("禁止破坏方块")) { + ac.denyBlockBreak = true; + } else if (line.equals("禁止PVP")) { + ac.denyPVP = true; + } else if (line.equals("禁止摔伤")) { + ac.denyFallDamage = true; + } else if (line.equals("禁止饥饿")) { + ac.denyHunger = true; + } else if (line.equals("禁止一切伤害")) { + ac.denyAllDamage = true; + } else if (line.equals("允许丢弃物品")) { + ac.allowDrop = true; + } else if (line.equals("禁止末影珍珠")) { + ac.denyEnderPearl = true; + } else if (line.equals("禁止使用弓箭")) { + ac.denyBow = true; + } else if (line.equals("禁止骑乘")) { + ac.denyMount = true; + } else if (line.equals("禁止袭击")) { + ac.denyRaid = true; + } else if (line.equals("禁止药水")) { + ac.denyPotion = true; + } else if (line.equals("禁止爆炸")) { + ac.denyExplosion = true; + } else if (line.equals("禁止燃烧") + || line.equals("禁止火焰蔓延")) { + ac.denyFireSpread = true; + } else if (line.equals("清除所有效果") + || line.equals("清理所有效果")) { + ac.clearAllBadEffects = true; + } else if (line.startsWith("清除") + || line.startsWith("清理")) { + String effName = line.substring(2).trim(); + if (effName.endsWith("效果")) + effName = effName.substring(0, + effName.length() - 2); + if (!effName.isEmpty()) + ac.clearEffects.add(effName); + } else if (line.contains("和平模式时间")) { + ac.peaceMode = true; // 自动开启和平模式 + int colonIdx = line.indexOf(':'); + if (colonIdx < 0) colonIdx = line.indexOf(':'); + if (colonIdx >= 0) { + try { + int seconds = Integer.parseInt(line.substring(colonIdx + 1).trim()); + // 最大1小时=3600秒,最小1秒 + seconds = Math.max(1, Math.min(3600, seconds)); + ac.peaceModeDuration = seconds * 1000; + plugin.getLogger().info("[解析] 和平模式时间=" + seconds + "秒"); + } catch (NumberFormatException ignored) {} + } + } else if (line.contains("和平模式") + && !line.contains("白名单") + && !line.contains("定时") + && !line.contains("开关") + && !line.contains("时间")) { + ac.peaceMode = true; + plugin.getLogger().info("[解析] 和平模式已识别"); + + } else if (line.startsWith("效果")) { + String rest = line.substring(2).trim(); + // 去掉前导冒号、空格 + while (rest.length() > 0 + && (rest.charAt(0) == ':' + || rest.charAt(0) == ':' + || rest.charAt(0) == ' ' + || rest.charAt(0) == '\t')) { + rest = rest.substring(1); + } + if (!rest.isEmpty()) { + String[] parts = rest.split("\\s+"); + // 第一个是效果名(可能是中文) + String effName = parts[0]; + int effLv = 1; + int effDur = 999; + if (parts.length >= 2) { + try { + effLv = Integer.parseInt(parts[1]); + } catch (NumberFormatException ignored) {} + } + if (parts.length >= 3) { + try { + effDur = Integer.parseInt(parts[2]); + } catch (NumberFormatException ignored) {} + } + + // ★ 用映射方法转为英文名 + String resolved = resolveEffectName(effName); + plugin.getLogger().info("[防护] 解析效果: " + + effName + " → " + resolved + + " 等级=" + effLv + + " 时长=" + effDur); + + ac.giveEffects.add(new String[]{ + resolved, + String.valueOf(effLv), + String.valueOf(effDur)}); + // ★ 加日志确认解析结果 + PotionEffectType checkType = + resolveEffectType(effName); + plugin.getLogger().info("[防护] 解析效果: " + + effName + " → type=" + + (checkType != null + ? checkType.getName() + : "NULL(失败)") + + " 等级=" + effLv + + " 时长=" + effDur); + + + } + if (!rest.isEmpty()) { + String[] parts = rest.split("\\s+"); + int effLv = 1; + int effDur = 999; + if (parts.length >= 2) { + try { + effLv = Integer.parseInt(parts[1]); + } catch (NumberFormatException ignored) { + } + } + if (parts.length >= 3) { + try { + effDur = Integer.parseInt(parts[2]); + } catch (NumberFormatException ignored) { + } + } + ac.giveEffects.add(new String[]{ + parts[0], + String.valueOf(effLv), + String.valueOf(effDur)}); + } + } else if (line.startsWith("通报批评:")) { + ac.announceTemplate = line.substring( + "通报批评:".length()).trim(); + ac.enableAnnounce = true; + } else if (line.startsWith("没收提示:")) { + ac.confiscateMsg = line.substring( + "没收提示:".length()).trim(); + } else if (line.startsWith("进入提示:")) { + String raw = line.substring("进入提示:".length()).trim(); + ac.enterMsg = raw; + + // ★ 调试:打印原文和Unicode + StringBuilder hex = new StringBuilder(); + for (int i = 0; i < raw.length(); i++) { + hex.append(String.format("U+%04X ", + (int) raw.charAt(i))); + } + plugin.getLogger().info( + "[防护-解析] ★★★进入提示原文=[" + raw + + "] 长度=" + raw.length() + + " Unicode=" + hex.toString()); + } else if (line.startsWith("离开提示:")) { + String raw = line.substring("离开提示:".length()).trim(); + ac.leaveMsg = raw; + + StringBuilder hex = new StringBuilder(); + for (int i = 0; i < raw.length(); i++) { + hex.append(String.format("U+%04X ", + (int) raw.charAt(i))); + } + plugin.getLogger().info( + "[防护-解析] ★★★离开提示原文=[" + raw + + "] 长度=" + raw.length() + + " Unicode=" + hex.toString()); + } else if (line.startsWith("惩罚命令:")) { + ac.punishCommands.add(line.substring( + "惩罚命令:".length()).trim()); + } else if (line.startsWith("强制游戏模式:") + || line.startsWith("游戏模式:")) { + String prefix = line.startsWith("强制游戏模式:") + ? "强制游戏模式:" : "游戏模式:"; + String modeStr = line.substring(prefix.length()) + .trim().toUpperCase(); + // 容错:支持多种写法 + if (modeStr.equals("CREATIVE") || modeStr.equals("创造") + || modeStr.equals("1")) { + ac.enforceGameMode = "CREATIVE"; + } else if (modeStr.equals("SURVIVAL") || modeStr.equals("生存") + || modeStr.equals("0")) { + ac.enforceGameMode = "SURVIVAL"; + } else if (modeStr.equals("ADVENTURE") || modeStr.equals("冒险") + || modeStr.equals("2")) { + ac.enforceGameMode = "ADVENTURE"; + } else if (modeStr.equals("SPECTATOR") || modeStr.equals("旁观") + || modeStr.equals("spectator") + || modeStr.equals("3")) { + ac.enforceGameMode = "SPECTATOR"; + } + } else if (line.contains("排除名单") + || line.contains("模式排除") + || line.contains("模式豁免")) { + int colonIdx = line.indexOf(':'); + if (colonIdx < 0) colonIdx = line.indexOf(':'); + if (colonIdx < 0) colonIdx = line.indexOf('='); + if (colonIdx >= 0) { + String rest = line.substring(colonIdx + 1).trim(); + String cleaned = rest.replaceAll("[,,;;|、/]", ","); + for (String s : cleaned.split(",")) { + String playerName = s.trim(); + if (!playerName.isEmpty()) { + ac.modeExempt.add(playerName); + } + } + } + + String rest = line.substring(/*idx*/ + 4).trim(); + while (rest.length() > 0) { + char c = rest.charAt(0); + if (c == ':' || c == ':' || c == '=' + || c == ' ' || c == '\t') { + rest = rest.substring(1); + } else { + break; + } + } + if (!rest.isEmpty()) { + String cleaned = rest.replaceAll("[,,;;|、/]", ","); + for (String s : cleaned.split(",")) { + String playername = s.trim(); + if (!name.isEmpty()) ac.modeExempt.add(name); + } + } + + while (rest.length() > 0) { + char c = rest.charAt(0); + if (c == ':' || c == ':' || c == '=' + || c == ' ' || c == '\t') { + rest = rest.substring(1); + } else { + break; + } + + } + if (!rest.isEmpty()) { + String cleaned = rest.replaceAll("[,,;;|、/]", ","); + for (String s : cleaned.split(",")) { + String wlName = s.trim(); + if (!wlName.isEmpty()) { + ac.peaceWhitelist.add(wlName); + } + } + } + + + + } else if (line.equals("禁止交互展示框") + || line.equals("禁止展示框")) { + ac.denyItemFrame = true; + + } + } + br.close(); + return ac; + } +// ===== 和平白名单操作 ===== + + // 添加生物名字到区域和平白名单 + public boolean addPeaceName(String areaName, String creatureName) { + AreaConfig ac = areas.get(areaName); + if (ac == null) return false; + ac.peaceWhitelist.add(creatureName); + savePeaceWhitelist(ac); + return true; + } + + // 从区域和平白名单移除生物名字 + public boolean removePeaceName(String areaName, String creatureName) { + AreaConfig ac = areas.get(areaName); + if (ac == null) return false; + ac.peaceWhitelist.remove(creatureName); + savePeaceWhitelist(ac); + return true; + } + +// ===== 模式排除名单操作 ===== + + + @EventHandler + public void onCreatureSpawn(CreatureSpawnEvent e) { + Entity entity = e.getEntity(); + if (!isHostile(entity.getType().name())) return; + + Location loc = entity.getLocation(); + AreaConfig ac = getArea( + loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), + loc.getBlockZ()); + + if (ac == null || !ac.peaceMode) return; + + protectedEntities.put( + entity.getUniqueId(), + System.currentTimeMillis() + ac.peaceModeDuration); + + /* plugin.getLogger().info("[和平] " + + entity.getType().name() + + " 获得5秒保护期" + + " 区域=" + ac.name + + " 白名单=" + ac.peaceWhitelist + + " 白名单数量=" + ac.peaceWhitelist.size());*/ + } + + + public void cleanupProtectedEntities() { + long now = System.currentTimeMillis(); + int cleaned = 0; + Iterator> it = + protectedEntities.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (now > entry.getValue()) { + it.remove(); + cleaned++; + } + } + if (cleaned > 0) { + /* plugin.getLogger().info("[和平模式] 清理过期保护: " + + cleaned + "个 剩余:" + + protectedEntities.size() + "个");*/ + } + } + + + public void banHostilesWithWhitelist(Player p, AreaConfig ac) { + long now = System.currentTimeMillis(); + List nearby = p.getNearbyEntities(48, 48, 48); + int scanned = 0; + int waiting = 0; + int saved = 0; + int banished = 0; + + for (Entity ent : nearby) { + if (!isHostile(ent.getType().name())) continue; + scanned++; + + UUID entUid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(entUid); + String customName = ent.getCustomName(); + + // 白名单命名生物:永久保留(含保护期内贴标的) + if (customName != null && !customName.isEmpty() && ac.peaceWhitelist.contains(customName)) { + if (protectUntil != null) { + protectedEntities.remove(entUid); // ★ 取消清理倒计时 + // ★ 保护期内贴标:立即确认 + if (protectUntil - now > 0) { + p.sendMessage("§a§l[和平模式] §f" + ent.getType().name() + " §a已被白名单保护,永久保留"); + } + } + saved++; + continue; + } + + // 有保护期记录 + if (protectUntil != null) { + long remain = protectUntil - now; + if (remain > 0) { + waiting++; + continue; + } + // 保护期到期 → 移除记录并检查白名单(最后一刻贴标也有效) + protectedEntities.remove(entUid); + if (customName != null && !customName.isEmpty() && ac.peaceWhitelist.contains(customName)) { + saved++; + continue; + } + banEntity(ent); + banished++; + continue; + } + + // 无保护期记录 → 未命名敌对生物,立即清理 + banEntity(ent); + banished++; + } + } + + /** + * 全图扫描清理和平模式下的未命名敌对生物 + * 由 Main 中的定时任务调用 + */ + public void scanAllLandsPeaceMode() { + long now = System.currentTimeMillis(); + int cleared = 0; + + // 遍历所有已加载的区域 + for (Map.Entry entry : areas.entrySet()) { + AreaConfig ac = entry.getValue(); + if (!ac.peaceMode) continue; + + // 对该区域边界内的实体扫描 + double centerX = (ac.x1 + ac.x2) / 2.0; + double centerZ = (ac.z1 + ac.z2) / 2.0; + World world = Bukkit.getWorld(ac.world); + if (world == null) continue; + + // ★ 根据领地实际大小计算扫描范围(取x和z范围的最大值,加边距) + int rangeX = Math.abs(ac.x2 - ac.x1) / 2 + 10; + int rangeZ = Math.abs(ac.z2 - ac.z1) / 2 + 10; + int range = Math.max(rangeX, rangeZ); + // 限制最大扫描范围避免性能问题 + range = Math.min(range, 200); + + // ★ 使用世界实际高度范围 + int minY = world.getMinHeight(); + int maxY = world.getMaxHeight(); + int heightRange = (maxY - minY) / 2; + + List nearby = new java.util.ArrayList<>(world.getNearbyEntities( + new Location(world, centerX, (minY + maxY) / 2.0, centerZ), range, heightRange, range)); + + for (Entity ent : nearby) { + if (!isHostile(ent.getType().name())) continue; + + // 检查是否在领地范围内 + AreaConfig landAc = getArea(world.getName(), + ent.getLocation().getBlockX(), + ent.getLocation().getBlockY(), + ent.getLocation().getBlockZ()); + if (landAc == null || !landAc.peaceMode) continue; + + // 白名单命名生物:跳过 + String cname = ent.getCustomName(); + if (cname != null && !cname.isEmpty() && landAc.peaceWhitelist.contains(cname)) continue; + + // 保护期内:跳过 + UUID uid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(uid); + if (protectUntil != null && (protectUntil - now) > 0) continue; + + // ★ 直接删除实体(传送可能卡住或重新刷新) + ent.remove(); + cleared++; + } + } + + if (cleared > 0) { + // plugin.getLogger().info("[和平模式扫描] 清理未命名敌对生物: " + cleared + " 个"); + } + } + + private void banEntity(Entity ent) { + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + if ("ENDERMAN".equals(ent.getType().name())) { + ent.remove(); + } else { + ent.teleport(voidLoc); + } + protectedEntities.remove(ent.getUniqueId()); + } + + private static final Set HOSTILE_TYPES = new HashSet<>(Arrays.asList( + "ZOMBIE", "ZOMBIE_VILLAGER", "HUSK", "DROWNED", + "SKELETON", "STRAY", "BOGGED", "WITHER_SKELETON", + "CREEPER", "SPIDER", "CAVE_SPIDER", "ENDERMITE", + "SILVERFISH", "BLAZE", "GHAST", "BIG_GHAST", + "MAGMA_CUBE", "SLIME", "WITCH", + "GUARDIAN", "ELDER_GUARDIAN", + "PILLAGER", "VINDICATOR", "EVOKER", "RAVAGER", + "ILLUSIONER", "VEX", + "PHANTOM", "SHULKER", "WARDEN", "BREEZE", + "PIGLIN", "PIGLIN_BRUTE", "ZOGLIN", + "ZOMBIFIED_PIGLIN", "ENDER_DRAGON", + "ENDERMAN", "WITHER" + )); + + private boolean isHostile(String typeName) { + return HOSTILE_TYPES.contains(typeName); + } + + // ==================== 白名单持久化 ==================== + + /** + * 保存白名单到文件 + * 路径:区域防护/whitelists/全局白名单.txt + * 路径:区域防护/whitelists/区域名.txt + */ + public void saveWhitelists() { + try { + // 1. 保存全局白名单 + PrintWriter pwGlobal = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(globalWhitelistFile), + StandardCharsets.UTF_8)); + for (String name : globalPlayerWhitelist) { + pwGlobal.println(name); + } + pwGlobal.close(); + + // 2. 删除旧的区域白名单文件(防止残留) + File[] oldFiles = whitelistDir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("全局白名单.txt")); + if (oldFiles != null) { + for (File f : oldFiles) f.delete(); + } + + // 3. 保存各区域白名单 + for (Map.Entry> entry + : areaPlayerWhitelist.entrySet()) { + String name = entry.getKey(); + File areaFile = new File( + whitelistDir, name + ".txt"); + PrintWriter pwArea = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(areaFile), + StandardCharsets.UTF_8)); + for (String playerName : entry.getValue()) { + pwArea.println(playerName); + } + pwArea.close(); + } + + plugin.getLogger().info("[防护] 白名单已保存"); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 保存白名单失败: " + + e.getMessage()); + } + } + + + + // ==================== 区域检测 ==================== + + public AreaConfig getArea(String worldName, + int x, int y, int z) { + for (AreaConfig ac : areas.values()) { + if (ac.world.equalsIgnoreCase(worldName) + && inRect(x, z, ac) + && y >= ac.yMin + && y <= ac.yMax) { + return ac; + } + } + return null; + } + + private boolean inRect(int x, int z, AreaConfig ac) { + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + return x >= minX && x <= maxX + && z >= minZ && z <= maxZ; + } + + /** + * ★ 第57批续21:查找【完全包含】指定矩形区域(世界+归一化坐标)的主领地(parentId==0)。 + * 子领地(有父)不视为"包含方"——部分重叠只算与父领地重叠,避免在子领地内再嵌套(仅1级)。 + * 多个主领地都完全包含时返回第一个(子领地完全包含于唯一主领地,正常不会多)。 + * @return 主领地的 AreaConfig;无则 null + */ + public AreaConfig findContainingMainLand(String world, int nx1, int nz1, int nx2, int nz2) { + if (world == null) return null; + int minX = Math.min(nx1, nx2), maxX = Math.max(nx1, nx2); + int minZ = Math.min(nz1, nz2), maxZ = Math.max(nz1, nz2); + for (AreaConfig other : areas.values()) { + if (other == null) continue; + if (other.parentId != 0) continue; // 只看主领地 + if (!other.world.equalsIgnoreCase(world)) continue; + int oMinX = Math.min(other.x1, other.x2), oMaxX = Math.max(other.x1, other.x2); + int oMinZ = Math.min(other.z1, other.z2), oMaxZ = Math.max(other.z1, other.z2); + // 完全包含:other ⊇ new + if (oMinX <= minX && oMaxX >= maxX && oMinZ <= minZ && oMaxZ >= maxZ) { + return other; + } + } + return null; + } + + /** + * ★ 第57批续21:创建子领地的身份闸门——判定玩家是否具备主领地的 + * 领地主 / 领地管理员 / 插件管理员 任一身份(授权管理员=该领地里 role='admin' 的行)。 + * 只有合格者才允许在主领地内创建子领地;不合格一律拒绝。 + */ + public boolean isQualifiedLandManager(Player p, AreaConfig mainLand) { + if (p == null || mainLand == null) return false; + if (isAreaAdmin(p)) return true; // 插件管理员(OP/ScoreboardTag) + if (samePlayer(p.getName(), mainLand.owner)) return true; // 领地主 + int lid = getLandIdFromDb(mainLand.name); + if (lid > 0) { + PermRow row = findPermRowExact(lid, p.getName()); + if (row != null && "admin".equalsIgnoreCase(row.role)) return true; // 授权领地管理员 + } + return false; + } + + /** + * 获取玩家在指定领地的权限级别 + */ + public PermissionLevel getPermissionLevel(Player player, AreaConfig ac) { + // 1. 全局管理员(ScoreboardTag) + if (isAreaAdmin(player)) { + return PermissionLevel.ADMIN; + } + + // 2. 领地所有者权限 + if (ac != null && ac.owner != null && !ac.owner.isEmpty()) { + if (samePlayer(player.getName(), ac.owner)) { + return PermissionLevel.OWNER; + } + } + + // 2.5) ★ 第57批续21 子领地继承:玩家对本领地无直接角色,但父领地里是 + // 领地主/授权管理员/插件管理员 → 在子领地内同样行使管理权(防越权绕过)。 + // 子领地 owner=主领地主,上面第2步已覆盖 owner;这里补授权管理员/插件管理员继承。 + if (ac != null && ac.parentId > 0 && dbConnection != null) { + String parentName = findLandNameById(ac.parentId); + if (parentName != null) { + AreaConfig parent = areas.get(parentName); + if (parent != null + && !samePlayer(player.getName(), parent.owner) + && isQualifiedLandManager(player, parent)) { + return PermissionLevel.ADMIN; + } + } + } + + // 3. 领地角色(DB,仅限该领地;2026-10-11 精确匹配,一次查询判 admin/member 两态) + // admin → ADMIN(/protect setadmin 或领地所有者GUI设置) + // member(未过期) → MEMBER:已授权成员,不受领地全局默认权限限制 + // ★ 必须在白名单/VISITOR 检查之前:成员多半同时在区域白名单里, + // 若先命中白名单只会拿到 VISITOR,denyMove 等权限按访客回退领地默认, + // 导致"已授权成员被当成路人、跟随全局权限一起被限制"(2026-10-11 修复)。 + // ★ 注意:严格按 ac.name 限定,绝不跨领地生效(封堵越权) + if (ac != null) { + int landId = getLandIdFromDb(ac.name); + if (landId > 0) { + PermRow row = findPermRowExact(landId, player.getName()); + if (row != null) { + if ("admin".equalsIgnoreCase(row.role)) { + return PermissionLevel.ADMIN; + } + if ("member".equalsIgnoreCase(row.role) + && (row.expiresAt == 0 || row.expiresAt > System.currentTimeMillis())) { + return PermissionLevel.MEMBER; + } + } + } + } + + // 4. 访客权限(白名单 + 数据库购买权限) + if (isPlayerWhitelisted(player.getName(), ac)) { + return PermissionLevel.VISITOR; + } + + // 5. 数据库购买的访客权限(未过期) + if (hasValidVisitorPermission(player, ac)) { + return PermissionLevel.VISITOR; + } + + // 6. 无权限 + return null; + } + + /** + * 检查玩家在领地是否有足够权限执行操作 + * @param requiredLevel 所需的最低权限级别 + * @return true如果有足够权限 + */ + public boolean hasPermission(Player player, AreaConfig ac, PermissionLevel requiredLevel) { + PermissionLevel playerLevel = getPermissionLevel(player, ac); + if (playerLevel == null) return false; + + // ADMIN拥有所有权限 + if (playerLevel == PermissionLevel.ADMIN) return true; + + // OWNER可以执行VISITOR能执行的操作 + if (playerLevel == PermissionLevel.OWNER) { + return requiredLevel != PermissionLevel.ADMIN; + } + + // VISITOR只能执行VISITOR级别的操作 + return requiredLevel == PermissionLevel.VISITOR; + } + + /** + * 领地传送权限检查: + * - 领地主 / 插件全局管理员 / 领地管理员 → 无条件允许传送 + * - 其他玩家 → 必须已获得该领地的"传送"授权(area_land_permissions.permissions 中 allowTeleport=true) + */ + public boolean canTeleportToLand(Player p, AreaConfig ac) { + if (ac == null || p == null) return false; + if (isAreaAdmin(p)) return true; + if (samePlayer(p.getName(), ac.owner)) return true; + if (isLandAdmin(ac.name, p.getName())) return true; + // ★ 公共设施连控:公共设施启用时,所有访客自动获得传送权限(优先于 per-player 检查) + if (ac.isPublicBuilding) return true; + // ★ 访客传送权限:领地级别允许则放行(优先于per-player检查) + if (ac.allowVisitorTeleport) return true; + int landId = getLandIdFromDb(ac.name); + if (landId <= 0) return false; // 无法验证则拒绝 + Map perms = getPlayerPermMapExact(landId, p.getName()); + return perms.getOrDefault("allowTeleport", false); + } + + // ===== Per-Player 独立权限系统 ===== + + /** + * 获取玩家在指定领地的独立权限JSON + * 格式: {"denyMove":true,"denyPVP":false,...} + * 空字符串或null表示没有自定义权限(使用领地默认) + */ + public String getPlayerPermJson(int landId, String playerName) { + PermRow row = findPermRow(landId, playerName); + if (row == null || row.permissions == null) return ""; + return row.permissions; + } + + /** area_land_permissions 行(Java 端匹配用) */ + private static final class PermRow { + int id; + String playerName; + String role; + String permissions; + long expiresAt; + } + + /** + * 按 land_id 取出该领地全部权限行,在 Java 端做玩家名匹配: + * 精确匹配优先,其次 NFKC+ROOT 规范化匹配(normName)。 + * 替代 SQL COLLATE NOCASE —— 后者只折叠 ASCII,İ/ẞ/K 等变种字符不生效, + * 且命中多条大小写重复行时返回顺序不确定(跨平台易读到错误记录)。 + */ + private List findPermRows(int landId, String playerName) { + List exact = new ArrayList<>(); + if (dbConnection == null || landId <= 0 || playerName == null || playerName.isEmpty()) + return exact; + String want = normName(playerName); + List fuzzy = new ArrayList<>(); + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT id, player_name, role, permissions, expires_at " + + "FROM area_land_permissions WHERE land_id = ?"); + stmt.setInt(1, landId); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + PermRow r = new PermRow(); + r.id = rs.getInt("id"); + r.playerName = rs.getString("player_name"); + r.role = rs.getString("role"); + r.permissions = rs.getString("permissions"); + r.expiresAt = rs.getLong("expires_at"); + if (playerName.equals(r.playerName)) exact.add(r); + else if (want.equals(normName(r.playerName))) fuzzy.add(r); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + return exact; + } + exact.addAll(fuzzy); + return exact; + } + + /** 取匹配度最高的一行(精确匹配优先) */ + private PermRow findPermRow(int landId, String playerName) { + List rows = findPermRows(landId, playerName); + return rows.isEmpty() ? null : rows.get(0); + } + + /** + * ★ 身份/授权判定专用:仅精确匹配(区分大小写,2026-10-10)。 + * 本服离线模式:名字大小写不同 => UUID 不同 => 不同玩家。 + * 旧版 findPermRow 的 normName 模糊兜底会让 YoupaishidIfu 命中 youpaishidifu 的 + * admin/visitor 行,从而获得 ADMIN/VISITOR 豁免、绕过全部 deny* 限制。 + * normName 模糊行仅保留在写路径(定位旧行做 UPDATE/DELETE),两者不可混用。 + */ + private PermRow findPermRowExact(int landId, String playerName) { + if (dbConnection == null || landId <= 0 || playerName == null || playerName.isEmpty()) + return null; + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT id, player_name, role, permissions, expires_at " + + "FROM area_land_permissions WHERE land_id = ? AND player_name = ?"); + stmt.setInt(1, landId); + stmt.setString(2, playerName); + ResultSet rs = stmt.executeQuery(); + PermRow r = null; + if (rs.next()) { + r = new PermRow(); + r.id = rs.getInt("id"); + r.playerName = rs.getString("player_name"); + r.role = rs.getString("role"); + r.permissions = rs.getString("permissions"); + r.expiresAt = rs.getLong("expires_at"); + } + rs.close(); + stmt.close(); + return r; + } catch (SQLException e) { + return null; + } + } + + /** + * ★ per-player 权限读取(授权判定专用):精确匹配版(2026-10-10)。 + * getEffectiveDeny / canTeleportToLand 等授权路径必须用本方法, + * 防止大小写变种玩家读到他人 per-player 放行配置而绕过领地默认 deny。 + */ + public Map getPlayerPermMapExact(int landId, String playerName) { + Map map = new HashMap<>(); + PermRow row = findPermRowExact(landId, playerName); + if (row == null || row.permissions == null || row.permissions.isEmpty()) return map; + String clean = row.permissions.trim(); + if (clean.startsWith("{")) clean = clean.substring(1); + if (clean.endsWith("}")) clean = clean.substring(0, clean.length() - 1); + if (clean.isEmpty()) return map; + for (String pair : clean.split(",")) { + String[] kv = pair.split(":"); + if (kv.length == 2) { + String key = kv[0].trim().replace("\"", ""); + boolean val = Boolean.parseBoolean(kv[1].trim().replace("\"", "")); + map.put(key, val); + } + } + return map; + } + + /** + * 设置玩家在指定领地的独立权限JSON + */ + public void setPlayerPermJson(int landId, String playerName, String permJson) { + if (dbConnection == null) return; + String json = permJson != null ? permJson : ""; + try { + // ★ 先按“精确优先 + normName 规范化”定位已有行,再决定 UPDATE / INSERT: + // - 已有行:只更新 permissions,保留原 role(admin/member/visitor) + // - 无行:插入新记录,role 默认 visitor(非 member,防止误降级管理员) + // 注意:UNIQUE(land_id, player_name) 是 BINARY,直接 INSERT ON CONFLICT + // 在大小写/变种字符不一致时会新增重复行,导致后续读到错误记录。 + PermRow existing = findPermRow(landId, playerName); + if (existing != null) { + PreparedStatement stmt = dbConnection.prepareStatement( + "UPDATE area_land_permissions SET permissions = ? WHERE id = ?"); + stmt.setString(1, json); + stmt.setInt(2, existing.id); + stmt.executeUpdate(); + stmt.close(); + } else { + PreparedStatement stmt = dbConnection.prepareStatement( + "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + + "VALUES (?, ?, 'visitor', ?, ?)"); + stmt.setInt(1, landId); + stmt.setString(2, playerName); + stmt.setString(3, json); + stmt.setLong(4, System.currentTimeMillis() / 1000); + stmt.executeUpdate(); + stmt.close(); + } + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 设置玩家权限失败: " + e.getMessage()); + } + } + + /** + * 获取玩家独立权限(解析JSON为Map) + */ + public Map getPlayerPermMap(int landId, String playerName) { + Map map = new HashMap<>(); + String json = getPlayerPermJson(landId, playerName); + if (json == null || json.isEmpty()) return map; + // 简单解析 {"key":true,"key2":false} + String clean = json.trim(); + if (clean.startsWith("{")) clean = clean.substring(1); + if (clean.endsWith("}")) clean = clean.substring(0, clean.length() - 1); + if (clean.isEmpty()) return map; + for (String pair : clean.split(",")) { + String[] kv = pair.split(":"); + if (kv.length == 2) { + String key = kv[0].trim().replace("\"", ""); + boolean val = Boolean.parseBoolean(kv[1].trim().replace("\"", "")); + map.put(key, val); + } + } + return map; + } + + /** + * 设置玩家独立权限中的单个权限项 + * @param permName 权限名(如 "denyMove") + * @param enabled true=启用限制(deny),false=取消限制(允许) + */ + public void setPlayerPerm(int landId, String playerName, String permName, boolean enabled) { + Map map = getPlayerPermMap(landId, playerName); + map.put(permName, enabled); + String json = mapToJson(map); + setPlayerPermJson(landId, playerName, json); + } + + /** + * 获取所有有独立权限的成员列表 + */ + public List> getLandMembersWithPerms(int landId) { + List> list = new ArrayList<>(); + if (dbConnection == null) return list; + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT player_name, role, permissions FROM area_land_permissions " + + "WHERE land_id = ?"); + stmt.setInt(1, landId); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Map entry = new HashMap<>(); + entry.put("player", rs.getString("player_name")); + entry.put("role", rs.getString("role")); + entry.put("permissions", rs.getString("permissions")); + list.add(entry); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + // 忽略 + } + return list; + } + + /** + * 获取所有领地的成员权限数据(用于同步到PHP端) + */ + public List> getAllPermsForSync() { + List> list = new ArrayList<>(); + if (dbConnection == null) return list; + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT p.id, p.land_id, l.name AS land_name, p.player_name, p.role, " + + "p.permissions, p.granted_at, p.expires_at " + + "FROM area_land_permissions p " + + "JOIN area_lands l ON p.land_id = l.id " + + "ORDER BY p.land_id, p.player_name"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Map entry = new HashMap<>(); + entry.put("land_id", rs.getInt("land_id")); + entry.put("land_name", rs.getString("land_name")); + entry.put("player_name", rs.getString("player_name")); + entry.put("role", rs.getString("role")); + entry.put("permissions", rs.getString("permissions")); + entry.put("granted_at", rs.getLong("granted_at")); + entry.put("expires_at", rs.getLong("expires_at")); + list.add(entry); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] getAllPermsForSync失败: " + e.getMessage()); + } + return list; + } + + /** + * 检查玩家是否是某领地的管理员(role='admin') + */ + public boolean isLandAdmin(String landName, String playerName) { + int landId = getLandIdFromDb(landName); + if (landId <= 0) return false; + // ★ 2026-10-10 精确匹配:normName 模糊会把大小写变种玩家判成本领地管理员而豁免全部 deny* + PermRow row = findPermRowExact(landId, playerName); + return row != null && "admin".equalsIgnoreCase(row.role); + } + + /** + * 设置/取消玩家的领地管理员身份 + */ + public void setLandAdmin(String landName, String playerName, boolean admin) { + int landId = getLandIdFromDb(landName); + if (landId <= 0) return; + if (dbConnection == null) return; + try { + String role = admin ? "admin" : "member"; + PermRow existing = findPermRow(landId, playerName); + if (existing != null) { + // 已有行:只改 role,保留 permissions + PreparedStatement stmt = dbConnection.prepareStatement( + "UPDATE area_land_permissions SET role = ? WHERE id = ?"); + stmt.setString(1, role); + stmt.setInt(2, existing.id); + stmt.executeUpdate(); + stmt.close(); + } else { + PreparedStatement stmt = dbConnection.prepareStatement( + "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + + "VALUES (?, ?, ?, '', ?)"); + stmt.setInt(1, landId); + stmt.setString(2, playerName); + stmt.setString(3, role); + stmt.setLong(4, System.currentTimeMillis() / 1000); + stmt.executeUpdate(); + stmt.close(); + } + } catch (SQLException e) { + // 忽略 + } + } + + /** + + * 拉黑时撤销该玩家在所有领地的管理员身份(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 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 + */ + public void insertLandPermission(String landName, String playerName, String role) { + int landId = getLandIdFromDb(landName); + if (landId <= 0 || dbConnection == null) return; + try { + // 已有任意大小写/变种形式的行则跳过,避免产生重复记录 + if (findPermRow(landId, playerName) != null) return; + PreparedStatement stmt = dbConnection.prepareStatement( + "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + + "VALUES (?, ?, ?, '', ?)"); + stmt.setInt(1, landId); + stmt.setString(2, playerName); + stmt.setString(3, role != null && !role.isEmpty() ? role : "visitor"); + stmt.setLong(4, System.currentTimeMillis() / 1000); + stmt.executeUpdate(); + stmt.close(); + } catch (SQLException e) { + // 忽略 + } + } + + /** + * ★ 核心方法:获取玩家的effective deny状态 + * 优先级:per-player权限 > 领地默认权限 + * @param permName 权限字段名(如 "denyMove") + * @return true=限制生效,false=允许 + */ + public boolean getEffectiveDeny(Player player, AreaConfig ac, String permName) { + // 如果ac为null(不在任何领地内),直接允许 + if (ac == null) return false; + + // ★ denyPVP / denyAllDamage 是全局安全权限,ADMIN/OWNER/VISITOR 也不例外 + // ★ denyWax(涂蜡/刮蜡)2026-09-29 起与其他 deny* 权限【对齐】,走下方四级判定: + // 领地主/插件管理员豁免 → per-player 授权 → 白名单访客豁免 → 普通访客回退领地默认。 + // 即"禁止涂蜡/刮蜡"只约束普通访客,已授权玩家/管理员/领地主不受影响(订正倒反天罡 bug)。 + boolean isSecurityPerm = "denyPVP".equals(permName) + || "denyAllDamage".equals(permName); + PermissionLevel level = isSecurityPerm ? null : getPermissionLevel(player, ac); + + // 1) ADMIN/OWNER 永远豁免 + if (level == PermissionLevel.ADMIN || level == PermissionLevel.OWNER) { + return false; + } + + // 2) per-player 显式权限【必须先于 VISITOR 豁免】: + // setPlayerPermJson 建行时 role 写死 'visitor'、expires_at 默认 0, + // 旧顺序会把"刚被关停权限的玩家"变成永久访客而直接豁免,关停形同虚设。 + int landId = getLandIdFromDb(ac.name); + if (landId > 0) { + // ★ 2026-10-10 精确匹配:防止大小写变种玩家读到他人 per-player 放行配置 + Map playerPerms = getPlayerPermMapExact(landId, player.getName()); + if (playerPerms.containsKey(permName)) { + return playerPerms.get(permName); + } + // ★ allowPickup/allowDrop 与 denyPickup/denyDrop 键名反转映射 + if ("denyPickup".equals(permName) && playerPerms.containsKey("allowPickup")) { + return !playerPerms.get("allowPickup"); + } + if ("denyDrop".equals(permName) && playerPerms.containsKey("allowDrop")) { + return !playerPerms.get("allowDrop"); + } + } + + // 2.5) 已授权成员(DB role='member')豁免领地默认权限 + // ★ 2026-10-11 修复严重bug:成员此前拿不到 ADMIN/OWNER/VISITOR 任一身份, + // getPermissionLevel 返回 null → 与纯路人一样回退领地默认 deny*, + // 被"跟随全局权限一起限制"(denyMove 传送、denyBlockBreak 拦截等全中)。 + // 成员是领地主显式授权的人,应豁免领地默认限制; + // per-player 显式配置仍在第 2 步优先命中,领地主可对单个成员单独收紧。 + if (level == PermissionLevel.MEMBER) { + return false; + } + + // 3) 访客豁免(该玩家没有任何 per-player 显式配置时才生效) + // ★ 2026-10-04 修复:denyMove 不再对访客豁免。 + // 原因:领地 GUI 的"移动"开关读的是 ac.denyMove(领地默认), + // 而访客在此处被无条件 return false,实际畅通无阻 —— 显示与行为脱节。 + // 访客若无 per-player 显式 denyMove(第 2 步未命中),就继续落到第 4 步, + // 按领地默认 denyMove 判定,与"移动 已禁用"的显示保持一致。 + // 其余 deny* 权限的访客豁免语义保持不变。 + if (level == PermissionLevel.VISITOR) { + if (!"denyMove".equals(permName)) { + return false; + } + } + + // 4) 回退到领地默认权限 + // ★ level==null(纯路人:无白名单、无权限行)本就走这里,行为正确,未改动。 + return getLandDefaultDeny(ac, permName); + } + + /** ★ 是否为刷怪蛋(生物蛋)物品 */ + private static boolean isSpawnEggItem(Material m) { + return m != null && m.name().endsWith("_SPAWN_EGG"); + } + + /** + * ★ 涂蜡/刮蜡权限是否生效: + * 2026-09-27 起改为【独立一级权限】:不再受"破坏方块/放置方块"上级开关约束。 + * 2026-09-29 订正:移出"安全权限"名单,与其他 deny* 权限【完全对齐】, + * 走 getEffectiveDeny 四级判定—— + * ① 领地主 / 插件管理员(含非OP领地管理员) 豁免; + * ② per-player 显式授权优先(已授权涂蜡刮蜡的普通玩家放行); + * ③ 白名单访客豁免; + * ④ 普通访客回退领地默认 denyWax("禁止涂蜡/刮蜡"只约束这一层)。 + * 此前按 denyPVP 处理导致 level 恒为 null、领地主和管理员也被拦(倒反天罡)。 + */ + private boolean isWaxDenied(Player player, AreaConfig ac) { + if (ac == null) return false; + return getEffectiveDeny(player, ac, "denyWax"); + } + + /** ★ 可涂蜡/刮蜡的方块(铜质方块、避雷针、告示牌) */ + private static boolean isWaxableBlock(Block b) { + if (b == null) return false; + String n = b.getType().name(); + if (n.contains("COPPER") && !n.contains("RAW_COPPER")) return true; + if (n.equals("LIGHTNING_ROD")) return true; + return n.endsWith("_SIGN"); + } + + /** + * 获取领地默认deny状态(按字段名映射) + */ + private boolean getLandDefaultDeny(AreaConfig ac, String permName) { + switch (permName) { + case "denyMove": return ac.denyMove; + case "denyBlockPlace": return ac.denyBlockPlace; + case "denyBlockBreak": return ac.denyBlockBreak; + case "denyContainer": return ac.denyContainer; + case "denyPVP": return ac.denyPVP; + case "denyFallDamage": return ac.denyFallDamage; + case "denyHunger": return ac.denyHunger; + case "denyAllDamage": return ac.denyAllDamage; + case "denyDrop": return !ac.allowDrop; + case "denyMount": return ac.denyMount; + case "denyEnderPearl": return ac.denyEnderPearl; + case "denyBow": return ac.denyBow; + case "denyPotion": return ac.denyPotion; + case "denyExplosion": return ac.denyExplosion; + case "denyRaid": return ac.denyRaid; + case "denyFireSpread": return ac.denyFireSpread; + case "denyAllEffects": return ac.denyAllEffects; + case "denyItemFrame": return ac.denyItemFrame; + case "denyEntityInteract": return ac.denyEntityInteract; + case "denyPickup": return !ac.allowPickup; + case "denyFire": return ac.denyFire; + case "denyThrownProjectiles": return ac.denyThrownProjectiles; + case "denyGlowing": return ac.denyGlowing; + case "denyRedstoneInteraction": return ac.denyRedstoneInteraction; + case "denyDoorInteraction": return ac.denyDoorInteraction; + case "denyNoteblockJukebox": return ac.denyNoteblockJukebox; + case "denyLead": return ac.denyLead; + case "denyCropHarvest": return ac.denyCropHarvest; + case "denyFarmlandTrample": return ac.denyFarmlandTrample; + case "denyEnderTeleport": return ac.denyEnderTeleport; + case "denyWoolShear": return ac.denyWoolShear; + case "denyAnimalFeeding": return ac.denyAnimalFeeding; + case "denyMobAttack": return ac.denyMobAttack; + case "denySignEdit": return ac.denySignEdit; + case "denySpawnEgg": return ac.denySpawnEgg; + case "denyVillagerTrade": return ac.denyVillagerTrade; + case "denyWax": return ac.denyWax; + case "peaceMode": return ac.peaceMode; + default: return false; + } + } + + /** + * Map转简单JSON + */ + private String mapToJson(Map map) { + if (map.isEmpty()) return ""; + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry e : map.entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(e.getKey()).append("\":").append(e.getValue()); + first = false; + } + sb.append("}"); + return sb.toString(); + } + + /** + * 获取指定领地的成员列表 + */ + public Set getAreaMembers(String areaName) { + Set members = areaPlayerWhitelist.get(areaName); + return members != null ? new HashSet<>(members) : new HashSet<>(); + } + + // ===== 统一白名单检查(供所有事件处理器使用)===== + + /** + * 检查玩家是否在任意白名单中(全局白名单 + 区域白名单) + * 这是所有白名单判断的唯一入口 + */ + // ===== 统一玩家名规范化(修复大小写变种字符在 Windows/Linux 下比对结果不一致) ===== + + /** + * 统一玩家名规范化:NFKC 兼容归一 + Locale.ROOT 小写。 + * 规则固定,不受操作系统默认 Locale / JDK Unicode 版本影响, + * 保证同一玩家名在 Windows 开发环境与 Linux 面板服上归一结果完全相同。 + * 例:İ→i、ẞ→ß、K(Kelvin)→k、全角→半角。 + */ + public static String normName(String s) { + if (s == null || s.isEmpty()) return ""; + return java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFKC) + .toLowerCase(java.util.Locale.ROOT); + } + + /** + * 玩家身份比对(领地主 / 成员 / 上架人等“是不是同一个人”的判定): + * 精确匹配、区分大小写,禁止归一化。 + * + * 背景:本服为离线模式,名字大小写不同 => UUID 不同 => 是不同玩家。 + * 旧代码用 equalsIgnoreCase(相当于按小写归一化)比对领地主, + * 导致 YoUpaishidIfu 被判成领地主 youpaishidifu, + * 进而在 getPermissionLevel 里拿到 OWNER 豁免、直接跳过 denyMove 等限制(越权绕过)。 + * 与 8069070「取消归一化,按人头配置权限」保持一致。 + * + * 注意:normName 仍用于**定位数据库里那条权限行**(历史数据大小写不一致), + * 但**是否授予身份/豁免**必须走这里的精确比对,两者不可混用。 + */ + public static boolean samePlayer(String a, String b) { + return a != null && b != null && a.equals(b); + } + + /** + * 白名单查询:★ 精确匹配(2026-10-10 封堵大小写变种绕过)。 + * 离线模式大小写不同 => UUID 不同 => 不同玩家; + * 旧 normName 归一比对会让 YoupaishidIfu 命中 youpaishidifu 的白名单/访客身份, + * 从而豁免 denyBlockBreak 等全部限制。历史小写条目由 loadWhitelists 的 + * canonicalNameFromDb 规范名迁移兜底(用 DB 里记录的精确拼写回写)。 + */ + private static boolean whiteContains(Set set, String name) { + if (set == null || name == null || name.isEmpty()) return false; + return set.contains(name); + } + + private boolean isPlayerWhitelisted(String player, + AreaConfig ac) { + // 1. 全局白名单 + if (whiteContains(globalPlayerWhitelist, player)) + return true; + // 2. 区域白名单 + if (ac != null && whiteContains(areaPlayerWhitelist.get(ac.name), player)) + return true; + return false; + } + + /** + * 快速检查:仅全局白名单(不带 AreaConfig) + */ + private boolean isGlobalWhite(String player) { + return whiteContains(globalPlayerWhitelist, player); + } + + /** + * 快速检查:仅区域白名单 + */ + private boolean isAreaWhite(String player, String areaName) { + return whiteContains(areaPlayerWhitelist.get(areaName), player); + } + + /** + * 检查玩家是否为区域防护管理员 + * 与 Main.isAdmin() 使用相同的 ScoreboardTag 机制 + */ + public boolean isAreaAdmin(Player player) { + // ★ OP 即为全局领地管理员(封堵前仅认ScoreboardTag,导致非OP也能跨领地越权) + if (player.isOp()) return true; + + ConfigManager cfg = plugin.getConfigMgr(); + if (cfg == null) return false; + + String tag = cfg.areaProtectAdminTag; + if (tag == null || tag.isEmpty()) return false; + + // 使用ScoreboardTag验证(与Main.isAdmin()一致,仅作为OP之外的补充) + try { + return player.getScoreboardTags().contains(tag); + } catch (Exception e) { + return false; + } + } + + /** + * 检查CommandSender是否为区域防护管理员(支持控制台) + */ + public boolean isAreaAdmin(CommandSender sender) { + if (sender instanceof Player) { + return isAreaAdmin((Player) sender); + } + // 控制台视为管理员 + return true; + } + + /** + * 自动将无owner的txt迁移领地分配给第一个操作的管理员 + */ + private void assignUnownedLandsToAdmin(Player admin) { + File ownerMarker = new File(rootDir, ".need_owner_assign"); + if (!ownerMarker.exists()) return; + + String adminName = admin.getName(); + int assigned = 0; + + for (AreaConfig ac : areas.values()) { + if (ac.owner == null || ac.owner.isEmpty()) { + ac.owner = adminName; + saveAreaToDb(ac); + assigned++; + } + } + + // 删除标记文件 + ownerMarker.delete(); + + if (assigned > 0) { + admin.sendMessage("§a§l[防护] 已将 " + assigned + " 个无主领地分配给你"); + plugin.getLogger().info("[防护] txt迁移领地已分配给管理员: " + adminName + " 共" + assigned + "个"); + } + } + + private boolean isItemBlacklisted(String itemId, + AreaConfig ac) { + if (globalItemBlacklist.contains( + itemId.toUpperCase())) + return true; + Set areaList = + areaItemBlacklist.get(ac.name); + return areaList != null + && areaList.contains(itemId.toUpperCase()); + } + + // ==================== 没收 ==================== + + private int confiscateItem(Player p, String itemTypeId) { + Material mat = Material.matchMaterial(itemTypeId); + if (mat == null) return 0; + ItemStack[] contents = p.getInventory().getContents(); + int removed = 0; + for (int i = 0; i < contents.length; i++) { + if (contents[i] != null + && contents[i].getType() == mat) { + removed += contents[i].getAmount(); + contents[i] = null; + } + } + p.getInventory().setContents(contents); + return removed; + } + + // ==================== 边框显示 ==================== + private void showBorder(Player p, AreaConfig ac, boolean temp) { + cancelBorder(p); + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + int edgeLenX = maxX - minX + 1; + int edgeLenZ = maxZ - minZ + 1; + // 超大领地跳过3D边框粒子 + if (edgeLenX > 200 || edgeLenZ > 200) return; + int perimeter = 2 * (edgeLenX + edgeLenZ); + // 动态步长:保证每条边最多~25个粒子点 + final int step = Math.max(4, Math.min(10, perimeter / 100)); + // 动态层数:大领地减少层数(从6层降到2层) + final int dyStep = perimeter > 200 ? 10 : 5; + final int maxParticlesPerTick = 80; + + int taskId = Bukkit.getScheduler() + .runTaskTimer(plugin, new Runnable() { + @Override + public void run() { + if (!p.isOnline()) return; + Location loc = p.getLocation(); + int py = loc.getBlockY(); + int count = 0; + for (int dy = 5; dy <= 30 && count < maxParticlesPerTick; dy += dyStep) { + for (int x = minX; x <= maxX && count < maxParticlesPerTick; x += step) { + if (x == minX || x == maxX) { + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, minZ + 0.5, + 1, 0, 0, 0, 0); + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, maxZ + 0.5, + 1, 0, 0, 0, 0); + count += 2; + } + } + for (int z = minZ + step; z < maxZ && count < maxParticlesPerTick; z += step) { + p.spawnParticle(Particle.END_ROD, + minX + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + p.spawnParticle(Particle.END_ROD, + maxX + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + count += 2; + } + } + } + }, 0L, 20L).getTaskId(); + displayTaskIds.put(p.getUniqueId(), taskId); // taskId 是 int + if (temp) { + Bukkit.getScheduler().runTaskLater(plugin, + () -> cancelBorder(p), 300L); + } + } + + + private void cancelBorder(Player p) { + Integer taskId = displayTaskIds.remove(p.getUniqueId()); + if (taskId != null) { + Bukkit.getScheduler().cancelTask(taskId); + } + } + + public void onPlayerQuit(String playerName) { + playerCurrentArea.remove(playerName); + } + + public void clearAllPlayerAreas() { + playerCurrentArea.clear(); + } + private void saveModeExempt(AreaConfig ac) { + File file = new File(rootDir, ac.name + ".txt"); + if (!file.exists()) return; + + try { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader( + new FileReader(file, StandardCharsets.UTF_8)); + String line; + boolean found = false; + + while ((line = reader.readLine()) != null) { + String trimmed = line.trim(); + if (trimmed.contains("排除名单") + || trimmed.contains("模式排除") + || trimmed.contains("模式豁免")) { + StringBuilder sb = new StringBuilder(); + sb.append("排除名单:"); + boolean first = true; + for (String name : ac.modeExempt) { + if (!first) sb.append(","); + sb.append(name); + first = false; + } + lines.add(sb.toString()); + found = true; + } else { + lines.add(line); + } + } + reader.close(); + + if (!found && !ac.modeExempt.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("排除名单:"); + boolean first = true; + for (String name : ac.modeExempt) { + if (!first) sb.append(","); + sb.append(name); + first = false; + } + lines.add(sb.toString()); + } + + FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8); + for (String l : lines) { + writer.write(l + "\n"); + } + writer.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[防护] 保存模式排除名单失败: " + e.getMessage()); + } + } + + // ==================== 事件监听 ==================== + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + Player p = event.getPlayer(); + UUID uid = p.getUniqueId(); + Location to = event.getTo(); + if (to == null) return; + + // ★ 未登录(冻结)玩家跳过领地移动判定: + // 登录系统冻结期间玩家本就无法移动,领地系统不应在此期间 + // 对其传送/施加效果;登录成功后由 onPlayerJoin 恢复区域效果。 + // (消除登录系统与领地系统在冻结态下的交叉干扰) + if (plugin.isFrozen(p)) return; + + Location from = event.getFrom(); + // X/Z debounce removed to allow denyMove teleport for small movements + + // 防抖 + Long lastTime = lastMoveProcess.get(uid); + long now = System.currentTimeMillis(); + if (lastTime != null + && now - lastTime < MOVE_DEBOUNCE_MS) { + return; + } + lastMoveProcess.put(uid, now); + + // 检测区域 + String oldArea = playerCurrentArea.get(uid); + String newArea = null; + String forcedMode = null; + + for (Map.Entry entry + : areas.entrySet()) { + AreaConfig ac = entry.getValue(); + if (!ac.world.equals(to.getWorld().getName())) + continue; + int px = to.getBlockX(); + int pz = to.getBlockZ(); + int py = to.getBlockY(); + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + if (px >= minX && px <= maxX + && pz >= minZ && pz <= maxZ + && py >= ac.yMin && py <= ac.yMax) { + newArea = entry.getKey(); + forcedMode = ac.enforceGameMode; + break; + } + } + + /* plugin.getLogger().info( + "[防护-移动] " + p.getName() + + " 旧=[" + oldArea + + "] 新=[" + newArea + "]" + + " areas.size=" + areas.size() + + " world=" + to.getWorld().getName());*/ + + // ===== 2. 区域没变 ===== + if (Objects.equals(oldArea, newArea)) { + if (newArea != null) { + AreaConfig ac = areas.get(newArea); + if (ac != null) { + // ★ denyMove检查:支持per-player独立权限 + // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免, + // 对无权限玩家则走领地默认 denyMove 配置 + if (getEffectiveDeny(p, ac, "denyMove")) { + Location safeLoc = findSafeExitLocation(p.getLocation(), ac); + if (safeLoc != null) { + p.teleport(safeLoc); + p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限,已被传送出去"); + } else { + p.teleport(p.getWorld().getSpawnLocation()); + p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限"); + } + return; + } else if (ac.denyMove) { + // 领地已开启移动限制但该玩家未被拦下 → 记录命中原因(10秒节流) + logDenyExempt(p, ac, "原地停留"); + } + + // ★ 每次移动都清除指定效果 + clearBadEffects(p, ac); + + // ★ 效果热更新:实时增删效果(2秒冷却) + hotUpdateRegionEffects(p, uid, newArea, ac); + + // ★ 每次移动都强制游戏模式 + if (ac.enforceGameMode != null + && !ac.enforceGameMode.isEmpty() + && !isExemptFromGameMode(p, newArea)) { + GameMode target = + GameMode.valueOf(ac.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + } + } + } + } + return; + } + + // 离开旧区域 + if (oldArea != null) { + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★开始离开处理: + + oldArea);*/ + + AreaConfig oldAc = areas.get(oldArea); + + // 离开提示(默认消息:感谢(玩家)光临(领地)) + String leaveMsg = (oldAc != null && oldAc.leaveMsg != null && !oldAc.leaveMsg.isEmpty()) + ? oldAc.leaveMsg : "感谢(玩家)光临(领地)"; + if (oldAc != null) { + p.sendMessage(formatAreaMsg(leaveMsg, p.getName(), oldArea)); + } + + // ★ 离开领地时展示边框粒子(3秒) + if (oldAc != null) { + showBorderBrief(p, oldAc); + } + + // ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还 + handleConfiscateReturn(p, oldArea); + + // 收标 + List marked = + playerMarkedEffects.remove(uid); + + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★收标: " + + (marked != null ? marked.size() : "null"));*/ + + if (marked != null) { + for (PotionEffectType t : marked) { + plugin.getLogger().info( + "[防护-移动-离开] 标记项: " + t.getName()); + } + } + + // 取消旧延时 + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + pendingClearArea.remove(uid); + pendingClearEffects.remove(uid); + plugin.getLogger().info( + "[防护-移动-离开] 取消旧延时"); + } + + // ★★★ 离开到野外(无新区域):立即清除领地给予的效果 ★★★ + if (newArea == null && oldAc != null + && oldAc.giveEffects != null && !oldAc.giveEffects.isEmpty()) { + plugin.getLogger().info( + "[防护-移动-离开野外] ★★★立即清除效果: 玩家=" + p.getName() + + " 区域=" + oldArea); + int cleared = 0; + for (String[] eff : oldAc.giveEffects) { + PotionEffectType type = resolveEffectType(eff[0]); + if (type != null) { + PotionEffect pe = p.getPotionEffect(type); + if (pe != null) { + p.removePotionEffect(type); + cleared++; + plugin.getLogger().info( + "[防护-移动-离开野外] ★清除: " + eff[0]); + } + } + } + removePlayerEffects(uid, oldArea); + plugin.getLogger().info( + "[防护-移动-离开野外] ★完成: 清除" + + cleared + "/" + oldAc.giveEffects.size() + "个效果"); + } + + // 创建延时清理(领地→领地跨区域差量清理) + if (marked != null && !marked.isEmpty() && newArea != null) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] cleared " + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 100L); // 5秒 = 100 tick + pendingClearTask.put(uid, task); + } + } + + + // 进入新区域 + if (newArea != null) { + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + String oldP = pendingClearArea.remove(uid); + if (oldP != null) { + diffEffects(p, uid, oldP, newArea); + } + } + + AreaConfig newAc = areas.get(newArea); + if (newAc != null) { + // ★ denyMove检查:支持per-player独立权限 + // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免 + if (getEffectiveDeny(p, newAc, "denyMove")) { + Location safeLoc = findSafeExitLocation(event.getFrom(), newAc); + if (safeLoc != null) { + p.teleport(safeLoc); + p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限,已被传送出去"); + } else { + p.teleport(p.getWorld().getSpawnLocation()); + p.sendMessage("§c§l[区域防护] §f你不具备此领地的移动权限"); + } + return; + } else if (newAc.denyMove) { + // 领地已开启移动限制但该玩家未被拦下 → 记录命中原因(10秒节流) + logDenyExempt(p, newAc, "进入区域"); + } + + // 进入提示(默认消息:欢迎(玩家)来到(领地)) + String enterMsg = (newAc.enterMsg != null && !newAc.enterMsg.isEmpty()) + ? newAc.enterMsg : "欢迎(玩家)来到(领地)"; + p.sendMessage(formatAreaMsg(enterMsg, p.getName(), newArea)); + // ★ 进入领地时展示边框粒子(3秒) + showBorderBrief(p, newAc); + // ★ 公共建筑设施:访客自动获得传送、免疫伤害、攻击敌对生物权限 + if (newAc.isPublicBuilding && !samePlayer(p.getName(), newAc.owner) && !isAreaAdmin(p)) { + p.sendMessage("§a§l[公共建筑] §f欢迎来到公共设施: §e" + newAc.name); + p.sendMessage("§7你在此区域享有: 传送、免疫伤害、攻击敌对生物权限"); + // 暂时移除伤害限制:给玩家5秒免疫效果 + p.addPotionEffect(new org.bukkit.potion.PotionEffect( + org.bukkit.potion.PotionEffectType.RESISTANCE, 100, 255, false, false, true)); + } + applyRegionEffects(p, uid, newArea, newAc); + // 清除效果 + clearBadEffects(p, newAc); + + } +// ===== 清除指定效果 ===== + if (newAc.clearEffects != null + && !newAc.clearEffects.isEmpty()) { + for (String effName : newAc.clearEffects) { + PotionEffectType type = + resolveEffectType(effName); + if (type != null && p.getPotionEffect(type) != null) { + p.removePotionEffect(type); + plugin.getLogger().info( + "[防护] ★清除效果: " + effName + + " 玩家=" + p.getName()); + } + } + } + +// ===== 清除负面效果(clearBadEffects 已在上方处理) ===== + // clearAllBadEffects 仅清除负面+中性效果,不清除正面效果 + // 此处不再单独处理,统一由上方 clearBadEffects() 负责 + + if (forcedMode != null + && !isExemptFromGameMode(p, newArea)) { + GameMode target = + GameMode.valueOf(forcedMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + } + } + } + + if (newArea != null) { + playerCurrentArea.put(uid, newArea); + } else { + playerCurrentArea.remove(uid); + } + } + + /** + * 诊断日志:领地已开启 denyMove,但该玩家判定为"允许移动"时, + * 打印命中的权限级别、白名单状态与权限行内容(每玩家10秒节流)。 + * 用于 Linux 面板服现场定位"关停权限后仍可移动"到底命中了哪条豁免分支。 + */ + private void logDenyExempt(Player p, AreaConfig ac, String scene) { + try { + long now = System.currentTimeMillis(); + Long last = denyExemptLogTs.get(p.getUniqueId()); + if (last != null && now - last < 10000L) return; + denyExemptLogTs.put(p.getUniqueId(), now); + + PermissionLevel lvl = getPermissionLevel(p, ac); + int landId = getLandIdFromDb(ac.name); + PermRow row = landId > 0 ? findPermRow(landId, p.getName()) : null; + String name = p.getName(); + plugin.getLogger().info("[防护][移动豁免诊断] 场景=" + scene + + " 领地=" + ac.name + + " 玩家=" + name + + " level=" + lvl + + " 全局管理员=" + isAreaAdmin(p) + + " 领地主=" + (ac.owner != null && samePlayer(name, ac.owner)) + + " 白名单=" + isPlayerWhitelisted(name, ac) + + " 访客行=" + (row == null ? "无" + : (row.playerName + "/" + row.role + "/exp=" + row.expiresAt + "/" + row.permissions)) + + " 领地默认denyMove=" + ac.denyMove); + } catch (Exception ignored) { + } + } + + /** + * 查找安全的领地外部传送位置 + */ + private Location findSafeExitLocation(Location loc, AreaConfig ac) { + int px = loc.getBlockX(), pz = loc.getBlockZ(); + int minX = Math.min(ac.x1, ac.x2), maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2), maxZ = Math.max(ac.z1, ac.z2); + + int distLeft = px - minX; + int distRight = maxX - px; + int distTop = pz - minZ; + int distBottom = maxZ - pz; + + int minDist = Math.min(Math.min(distLeft, distRight), Math.min(distTop, distBottom)); + + int exitX = px, exitZ = pz; + if (minDist == distLeft) exitX = minX - 3; + else if (minDist == distRight) exitX = maxX + 3; + else if (minDist == distTop) exitZ = minZ - 3; + else exitZ = maxZ + 3; + + // ★ 安全判定:头顶≥2格空气、脚下实体方块、距边界≥3格 + World world = loc.getWorld(); + if (world == null) return null; + for (int y = Math.min(ac.yMax, world.getMaxHeight() - 3); y >= Math.max(ac.yMin, world.getMinHeight() + 2); y--) { + Location check = new Location(world, exitX, y, exitZ); + Block feet = check.getBlock(); + Block head = world.getBlockAt(exitX, y + 1, exitZ); + Block head2 = world.getBlockAt(exitX, y + 2, exitZ); + // 脚下必须是实体方块 + if (!feet.getType().isSolid()) continue; + // 头顶2格必须是空气(可通过) + if (head.getType() != Material.AIR && head.getType() != Material.CAVE_AIR && head.getType() != Material.VOID_AIR) continue; + if (head2.getType() != Material.AIR && head2.getType() != Material.CAVE_AIR && head2.getType() != Material.VOID_AIR) continue; + // ★ 确保出口不在领地范围内(再往外推3格) + int finalX = exitX; + int finalZ = exitZ; + // 如果出口坐标还在领地内,继续往外推 + if (finalX >= minX && finalX <= maxX && finalZ >= minZ && finalZ <= maxZ) { + // 在minDist方向继续推 + if (minDist == distLeft) finalX = minX - 3; + else if (minDist == distRight) finalX = maxX + 3; + else if (minDist == distTop) finalZ = minZ - 3; + else finalZ = maxZ + 3; + } + return new Location(world, finalX + 0.5, y + 1, finalZ + 0.5); + } + // ★ 不满足条件,传送到世界出生点 + return world.getSpawnLocation().clone(); + } + +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + private static final Set RAID_MOBS = new HashSet<>(Arrays.asList( + "VEX", + "EVOKER", + "PILLAGER", + "VINDICATOR", + "WITCH", + "RAVAGER", + "PILLAGER" + )); + + @EventHandler + public void onEntitySpawn(EntitySpawnEvent event) { + Entity entity = event.getEntity(); + Location loc = entity.getLocation(); + + // 检查是否在防护区域内 + AreaConfig ac = getArea( + loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), + loc.getBlockZ()); + if (ac == null) return; + + // 禁止袭击:取消袭击事件 + if (ac.denyRaid) { + // 清理袭击生物 + String typeName = entity.getType().name(); + if (RAID_MOBS.contains(typeName)) { + event.setCancelled(true); + plugin.getLogger().info( + "[防护] ★清理袭击生物: " + typeName + + " 区域=" + ac.name); + } + } + + // 没收物品检测 + if (ac.confiscateItems != null + && !ac.confiscateItems.isEmpty()) { + if (entity instanceof Item) { + Item item = (Item) entity; + String itemType = item.getItemStack() + .getType().name(); + if (ac.confiscateItems.contains(itemType)) { + event.setCancelled(true); + plugin.getLogger().info( + "[防护] 没收违禁掉落物: " + + itemType); + } + } + } + } + + + /** + * 进入区域时发放效果 + * 流程:查DB → 有记录跳过写入 → 无记录写入DB + * → 给效果 → 贴标 + */ + private void applyRegionEffects(Player p, UUID uid, + String areaName, AreaConfig ac) { + /* plugin.getLogger().info( + "[防护-贴标] ★★★方法被调用! 玩家=" + + p.getName() + " 区域=" + areaName + + " ac=" + (ac != null) + + " 效果配置数=" + + (ac != null ? ac.giveEffects.size() : "null"));*/ + + if (ac == null) return; + // ★ 2026-10-04:负面配置拆出后,不能再只判 giveEffects 为空就返回 + boolean hasGive = ac.giveEffects != null && !ac.giveEffects.isEmpty(); + boolean hasBad = ac.badEffects != null && !ac.badEffects.isEmpty(); + if (!hasGive && !hasBad) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + List allForDb = new ArrayList<>(ac.giveEffects); + if (ac.badEffects != null) allForDb.addAll(ac.badEffects); + for (String[] eff : allForDb) { + PotionEffectType t = + resolveEffectType(eff[0]); + if (t == null) continue; + saveSingleEffect(uid, p.getName(), + areaName, t.getName()); + } + plugin.getLogger().info( + "[防护] DB写入, 玩家=" + p.getName() + + " 区域=" + areaName); + } else { + plugin.getLogger().info( + "[防护] DB已有记录, 跳过写入, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + + // 给效果 + 贴标 + // ★ 2026-10-04:负面配置已拆到 badEffects,两边都要给 + List allCfg = new ArrayList<>(ac.giveEffects); + if (ac.badEffects != null) allCfg.addAll(ac.badEffects); + List applied = new ArrayList<>(); + for (String[] eff : allCfg) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 + int lv = clampEffectLevel(Integer.parseInt(eff[1]), eff[0], ac.owner) - 1; + int dur = Integer.parseInt(eff[2]) * 20; + + PotionEffect existing = + p.getPotionEffect(type); + if (existing != null + && existing.getAmplifier() >= lv) { + continue; + } + p.addPotionEffect( + new PotionEffect(type, dur, lv)); + applied.add(type); + } catch (Exception ignored) { + } + } + + // ★ 成就防作弊贴标:这批是插件给的效果,必须登记 + markPluginEffects(uid, applied); + + // 贴标 + // 新代码(合并,不覆盖): + List existingMarks = + playerMarkedEffects.get(uid); + if (existingMarks != null && !existingMarks.isEmpty()) { + // 已有标记,合并新增的 + for (PotionEffectType t : applied) { + if (!existingMarks.contains(t)) { + existingMarks.add(t); + } + } + /* plugin.getLogger().info( + "[防护-贴标] 合并标记: " + + existingMarks.size() + + " 个, 玩家=" + p.getName());*/ + } else { + // 无标记,新建 + playerMarkedEffects.put(uid, applied); + /* plugin.getLogger().info( + "[防护-贴标] 新建标记: " + + applied.size() + + " 个, 玩家=" + p.getName());*/ + } + } + + /** + * 效果热更新:玩家在领地内移动时,实时对比领地效果配置, + * 增删缺失/多余的效果(2秒冷却防止频繁计算) + */ + private void hotUpdateRegionEffects(Player p, UUID uid, + String areaName, AreaConfig ac) { + if (ac == null) return; + + // 冷却检查 + Long lastTime = lastEffectHotUpdate.get(uid); + long now = System.currentTimeMillis(); + if (lastTime != null && now - lastTime < EFFECT_HOT_UPDATE_INTERVAL_MS) { + return; + } + lastEffectHotUpdate.put(uid, now); + + // 领地无效果配置 → 跳过 + // ★ 2026-10-04:负面配置拆出后,不能只判 giveEffects + boolean hasGive = ac.giveEffects != null && !ac.giveEffects.isEmpty(); + boolean hasBad = ac.badEffects != null && !ac.badEffects.isEmpty(); + if (!hasGive && !hasBad) return; + + // 1. 构建领地当前期望的效果集合(增益 + 负面) + Map expectedEffects = new LinkedHashMap<>(); + List allForHot = new ArrayList<>(); + if (ac.giveEffects != null) allForHot.addAll(ac.giveEffects); + if (ac.badEffects != null) allForHot.addAll(ac.badEffects); + for (String[] eff : allForHot) { + PotionEffectType type = resolveEffectType(eff[0]); + if (type == null) continue; + try { + // ★ 药效等级封顶(2026-10-04 任务4),与首次给予同一口径 + int lv = clampEffectLevel(Integer.parseInt(eff[1]), eff[0], ac.owner) - 1; // MC amplifier从0开始 + int dur = Integer.parseInt(eff[2]) * 20; // 秒→tick + expectedEffects.put(type, new int[]{lv, dur}); + } catch (NumberFormatException ignored) {} + } + + // 2. 获取当前标记的效果(即本领地给予的效果) + List marked = playerMarkedEffects.get(uid); + Set currentMarked = (marked != null) + ? new HashSet<>(marked) : new HashSet<>(); + + // 3. 添加缺失的效果 + List newMarked = new ArrayList<>(currentMarked); + List hotApplied = new ArrayList<>(); + for (Map.Entry entry : expectedEffects.entrySet()) { + PotionEffectType type = entry.getKey(); + int lv = entry.getValue()[0]; + int dur = entry.getValue()[1]; + + if (currentMarked.contains(type)) { + // 已标记 → 检查等级是否需要更新 + PotionEffect existing = p.getPotionEffect(type); + if (existing != null && existing.getAmplifier() < lv) { + // 等级提升 → 重新给效果 + p.addPotionEffect(new PotionEffect(type, dur, lv)); + hotApplied.add(type); + plugin.getLogger().info("[防护-热更新] 等级提升: " + + type.getName() + " Lv" + (lv + 1) + + " 玩家=" + p.getName()); + } + continue; + } + + // 新效果 → 添加 + PotionEffect existing = p.getPotionEffect(type); + if (existing == null || existing.getAmplifier() < lv) { + p.addPotionEffect(new PotionEffect(type, dur, lv)); + newMarked.add(type); + hotApplied.add(type); + plugin.getLogger().info("[防护-热更新] 新增效果: " + + type.getName() + " Lv" + (lv + 1) + " " + dur / 20 + "s" + + " 玩家=" + p.getName()); + } + } + + // ★ 成就防作弊贴标:热更新补上的效果同样是插件给的 + markPluginEffects(uid, hotApplied); + + // 4. 移除多余的效果(领地不再给予的) + List toRemove = new ArrayList<>(); + for (PotionEffectType type : currentMarked) { + if (!expectedEffects.containsKey(type)) { + // 该效果不在领地配置中 → 移除 + PotionEffect existing = p.getPotionEffect(type); + if (existing != null) { + p.removePotionEffect(type); + toRemove.add(type); + plugin.getLogger().info("[防护-热更新] 移除效果: " + + type.getName() + + " 玩家=" + p.getName()); + } + } + } + + // 5. 更新标记 + if (!toRemove.isEmpty() || !newMarked.equals(currentMarked)) { + List updatedMarked = new ArrayList<>(newMarked); + updatedMarked.removeAll(toRemove); + if (updatedMarked.isEmpty()) { + playerMarkedEffects.remove(uid); + } else { + playerMarkedEffects.put(uid, updatedMarked); + } + } + } + + /** + * 离开区域时,延时5秒清理效果 + * 流程:收标 → 5秒后清效果 → 确认清空 → 清DB + */ + private void scheduleEffectClear(Player p, UUID uid, + String areaName) { + // 收标 + List marked = + playerAppliedEffects.remove(uid); + + plugin.getLogger().info( + "[防护] 收标 " + (marked != null ? marked.size() : 0) + + " 个, 玩家=" + p.getName() + + " 区域=" + areaName); + + // 启动5秒延时 + pendingClearArea.put(uid, areaName); + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + pendingClearTask.remove(uid); + pendingClearArea.remove(uid); + pendingClearEffects.remove(uid); + + // 清除效果 + if (marked != null) { + for (PotionEffectType type : marked) { + p.removePotionEffect(type); + } + } + + // ★ 成就防作弊:离开领地后插件不再持有这些效果,标记要一并撤掉, + // 否则玩家之后自己喝药水凑齐 34 种时会被误判成作弊。 + if (plugin.achievementEffectGuard != null) { + plugin.achievementEffectGuard.clearPluginEffects(uid, null); + } + + // 确认清空后清DB + removePlayerEffects(uid, areaName); + plugin.getLogger().info( + "[防护] 5秒已到, 清除 " + + (marked != null ? marked.size() : 0) + + " 个效果 + DB, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + }.runTaskLater(plugin, 100L); // 5秒 = 100 tick + + pendingClearTask.put(uid, task); + } + public Set getAllAreaNames() { + return areas.keySet(); + } + + /** + * 5秒内进了新区域: + * 清除旧区域有、新区域没有的效果 + */ + private void diffEffects(Player p, UUID uid, + String oldAreaName, String newAreaName) { + AreaConfig oldAc = areas.get(oldAreaName); + AreaConfig newAc = areas.get(newAreaName); + if (oldAc == null || newAc == null) return; + + // ★ 从 pendingClearEffects 获取旧标记 + List oldMarked = + pendingClearEffects.remove(uid); + + if (oldMarked == null || oldMarked.isEmpty()) { + plugin.getLogger().info( + "[防护-跨区域] 无待清理效果"); + return; + } + + // 新区域的效果集合 + Set newTypes = new HashSet<>(); + if (newAc != null) { + if (newAc.giveEffects != null) { + for (String[] eff : newAc.giveEffects) { + PotionEffectType t = resolveEffectType(eff[0]); + if (t != null) newTypes.add(t); + } + } + // ★ 2026-10-04:负面配置拆到 badEffects,跨区域比对必须一起算 + if (newAc.badEffects != null) { + for (String[] eff : newAc.badEffects) { + PotionEffectType t = resolveEffectType(eff[0]); + if (t != null) newTypes.add(t); + } + } + } + + int cleared = 0; + for (PotionEffectType type : oldMarked) { + if (!newTypes.contains(type)) { + p.removePotionEffect(type); + cleared++; + plugin.getLogger().info( + "[防护-跨区域] 清除: " + + type.getName()); + } else { + plugin.getLogger().info( + "[防护-跨区域] 保留: " + + type.getName() + + " (新区域也有)"); + } + } + + removePlayerEffects(uid, oldAreaName); + plugin.getLogger().info( + "[防护-跨区域] 完成: 清除" + + cleared + "/" + oldMarked.size() + + " 个, " + oldAreaName + + " → " + newAreaName); + } + +// ===== DB 辅助方法(简化版)===== + + /** + * 查询玩家在某个区域的效果记录(返回英文名列表) + */ + private List getPlayerEffectNames( + UUID uid, String areaName) { + List list = new ArrayList<>(); + try { + PreparedStatement ps = dbConnection.prepareStatement( + "SELECT effect_type FROM player_effects " + + "WHERE uuid=? AND area_name=?"); + ps.setString(1, uid.toString()); + ps.setString(2, areaName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + list.add(rs.getString("effect_type")); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 查询效果失败: " + e.getMessage()); + } + return list; + } + + /** + * 写入单条效果记录 + */ + private void saveSingleEffect(UUID uid, + String playerName, String areaName, + String effectType) { + try { + PreparedStatement ps = dbConnection.prepareStatement( + "INSERT INTO player_effects " + + "(uuid, player_name, area_name, " + + "effect_type, effect_level, " + + "effect_duration, enter_time) " + + "VALUES (?,?,?,?,?,?,?)"); + ps.setString(1, uid.toString()); + ps.setString(2, playerName); + ps.setString(3, areaName); + ps.setString(4, effectType); + ps.setInt(5, 0); + ps.setInt(6, 999); + ps.setLong(7, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + plugin.getLogger().warning( + "[防护] 写入效果失败: " + e.getMessage()); + } + } + + // ==================== 违禁品没收 ==================== + + /** + * 检查玩家是否豁免于没收 + * 全局白名单 / 区域白名单 / modeExempt / OP 都跳过 + */ + public boolean isExemptFromConfiscation( + Player p, String areaName) { + AreaConfig ac = areas.get(areaName); + // 管理员和领地所有者免检 + if (hasPermission(p, ac, PermissionLevel.OWNER)) return true; + // modeExempt名单也免检 + if (ac != null && ac.modeExempt.contains(p.getName())) + return true; + return false; + } + + + private String getChineseGameMode(GameMode mode) { + switch (mode) { + case SURVIVAL: return "生存模式"; + case CREATIVE: return "创造模式"; + case ADVENTURE: return "冒险模式"; + case SPECTATOR: return "旁观模式"; + default: return mode.name(); + } + } + + /** + * ★ 贴标:把「这批效果是插件给的」登记到成就防作弊守卫。 + * + *

2026-10-04 新增。原先只有 {@code playerMarkedEffects} 这份内部标记 + * (用于离场清理),现在额外同步一份到 {@link AchievementEffectGuard}, + * 后者据此撤销 potion 类原版成就(all_potions / all_effects)。

+ */ + private void markPluginEffects(UUID uid, List types) { + if (plugin.achievementEffectGuard == null || uid == null || types == null || types.isEmpty()) return; + try { + plugin.achievementEffectGuard.markPluginEffects(uid, types); + } catch (Throwable ignored) { } + } + + /** + * 给予领地配置的全部效果(增益 giveEffects + 负面 badEffects) + * + *

2026-10-04:负面效果从 {@code giveEffects} 拆到 {@code badEffects}, + * 这里是两个列表的唯一合流点,改任何一处施加点都要记得带上 {@code badEffects}。

+ * + * @return 实际施加成功(已解析出类型)的效果类型列表,供贴标使用 + */ + private List giveRegionEffects(Player p, AreaConfig ac) { + List given = new ArrayList<>(); + if (ac == null) return given; + if (ac.giveEffects != null) given.addAll(applyEffectList(p, ac, ac.giveEffects)); + if (ac.badEffects != null) given.addAll(applyEffectList(p, ac, ac.badEffects)); + return given; + } + + /** + * 施加一份效果列表([名字, 等级, 秒数]),统一走等级封顶 + */ + private List applyEffectList(Player p, AreaConfig ac, List list) { + List applied = new ArrayList<>(); + if (list == null) return applied; + for (String[] parts : list) { + if (parts == null || parts.length < 1 || parts[0] == null || parts[0].isEmpty()) continue; + String effName = parts[0]; + int level = 1; + int duration = 999; + try { level = Integer.parseInt(parts[1]); } + catch (Exception ignored) {} + try { duration = Integer.parseInt(parts[2]); } + catch (Exception ignored) {} + // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 + level = clampEffectLevel(level, effName, ac.owner); + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + applied.add(type); + } + } + return applied; + } + + private void handleEnter(Player p, AreaConfig ac) { + UUID uid = p.getUniqueId(); + + // ★ 2026-10-04 关键顺序修正:负面配置拆到 badEffects 后, + // clearBadEffects 会把 badEffects 里配的负面一并清掉。 + // 所以「先给效果、再按配置清负面」——否则新增的负面效果子菜单等于白配。 + List given = giveRegionEffects(p, ac); + + // 清除负面效果(clearAllBadEffects / peaceMode / clearEffects / denyRaid) + clearBadEffects(p, ac); + + // ★ 贴标:这些效果是插件给的,不是玩家自己喝出来的。 + // 成就防作弊(撤销 nether/all_effects 等)完全依赖这份标记。 + markPluginEffects(uid, given); + + removePlayerEffects(uid, ac.name); + + // 白名单检查(最先执行) + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + + // 游戏模式切换(只对非白名单玩家) + if (ac.enforceGameMode != null) { + boolean exempt = isPlayerExemptFromModeChange(p, ac); + if (!exempt) { + GameMode target = GameMode.valueOf(ac.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + p.sendMessage("§e§l[区域防护] §f游戏模式已切换为: " + + getChineseGameMode(target)); + } + } + } + + // 和平模式/袭击检查 + if (ac.peaceMode) { + banHostilesWithWhitelist(p, ac); + } else if (ac.denyRaid) { + banRaidMobs(p, ac); + } + + handleConfiscate(p, ac); + } + + private boolean isPlayerExemptFromModeChange( + Player p, AreaConfig ac) { + // if (p.isOp()) return true; + // 1. 全局白名单(★ 精确匹配,2026-10-10) + if (whiteContains(globalPlayerWhitelist, p.getName())) { + return true; + } + // 2. 区域白名单 + Set areaWl = areaPlayerWhitelist.get(ac.name); + if (whiteContains(areaWl, p.getName())) { + return true; + } + if (ac.modeExempt.contains(p.getName())) { + return true; + } + return false; + } + + private void banRangedHostiles(Player p, AreaConfig ac) { + for (Entity ent : p.getNearbyEntities(48, 48, 48)) { + if (isRangedHostile(ent.getType().name())) { + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + } + } + } + + /** + * 按配置清除效果 + * denyRaid → 清除袭击相关 + * denyAllEffects → 清除所有负面+中性 + * denyAllEffects > denyRaid(全清优先) + */ + private void clearBadEffects(Player p, AreaConfig ac) { + // 和平模式 > 清除所有效果 > 禁止单个 + if (ac.peaceMode || ac.clearAllBadEffects) { + PotionEffectType[] allBad = { + PotionEffectType.SLOWNESS, + PotionEffectType.MINING_FATIGUE, + PotionEffectType.INSTANT_DAMAGE, + PotionEffectType.NAUSEA, + PotionEffectType.BLINDNESS, + PotionEffectType.HUNGER, + PotionEffectType.WEAKNESS, + PotionEffectType.POISON, + PotionEffectType.WITHER, + PotionEffectType.LEVITATION, + PotionEffectType.UNLUCK, + PotionEffectType.DARKNESS, + PotionEffectType.WIND_CHARGED, + PotionEffectType.WEAVING, + PotionEffectType.OOZING, + PotionEffectType.INFESTED, + PotionEffectType.BAD_OMEN, + PotionEffectType.TRIAL_OMEN, + PotionEffectType.RAID_OMEN + }; + for (PotionEffectType type : allBad) { + p.removePotionEffect(type); + } + return; + } + + // 禁止袭击 → 自动关联清除 + if (ac.denyRaid) { + p.removePotionEffect(PotionEffectType.BAD_OMEN); + p.removePotionEffect(PotionEffectType.RAID_OMEN); + } + + // 配置文件单个清除 + for (String effName : ac.clearEffects) { + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.removePotionEffect(type); + } + } + } + + + private PotionEffectType resolveEffectType(String name) { + if (name == null || name.isEmpty()) return null; + + // 去除首尾空白、引号、反斜杠等残留转义字符(PHP JSON存储偶尔带入的\前缀) + String clean = name.trim().replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + if (clean.isEmpty()) return null; + + // 去掉"效果"后缀 + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + + // ★ 直接用 PotionEffectType 常量映射(不依赖 getByName,兼容所有 MC 版本) + // 中文名、英文ID(大写/小写/snake_case)全部在此处理 + switch (clean) { + // ========== 正面 ========== + // 速度 + case "迅捷": case "速度": + case "speed": case "SPEED": + return PotionEffectType.SPEED; + // 急迫 + case "急迫": + case "haste": case "HASTE": + return PotionEffectType.HASTE; + // 力量 + case "力量": + case "strength": case "STRENGTH": + case "INCREASE_DAMAGE": + return PotionEffectType.STRENGTH; + // 瞬间治疗 + case "瞬间治疗": case "治疗": + case "instant_health": case "INSTANT_HEALTH": + case "HEAL": + return PotionEffectType.INSTANT_HEALTH; + // 跳跃提升 + case "跳跃提升": + case "jump_boost": case "JUMP_BOOST": + case "JUMP": + return PotionEffectType.JUMP_BOOST; + // 生命恢复/再生/回复 + case "生命恢复": case "再生": case "回复": + case "regeneration": case "REGENERATION": + return PotionEffectType.REGENERATION; + // 抗性提升 + case "抗性提升": + case "resistance": case "RESISTANCE": + case "DAMAGE_RESISTANCE": + return PotionEffectType.RESISTANCE; + // 抗火 + case "抗火": + case "fire_resistance": case "FIRE_RESISTANCE": + return PotionEffectType.FIRE_RESISTANCE; + // 水下呼吸 + case "水下呼吸": + case "water_breathing": case "WATER_BREATHING": + return PotionEffectType.WATER_BREATHING; + // 隐身 + case "隐身": + case "invisibility": case "INVISIBILITY": + return PotionEffectType.INVISIBILITY; + // 夜视 + case "夜视": + case "night_vision": case "NIGHT_VISION": + return PotionEffectType.NIGHT_VISION; + // 发光 + case "发光": + case "glowing": case "GLOWING": + return PotionEffectType.GLOWING; + // 生命提升 + case "生命提升": + case "health_boost": case "HEALTH_BOOST": + return PotionEffectType.HEALTH_BOOST; + // 伤害吸收 + case "伤害吸收": case "吸收": + case "absorption": case "ABSORPTION": + return PotionEffectType.ABSORPTION; + // 饱和 + case "饱和": + case "saturation": case "SATURATION": + return PotionEffectType.SATURATION; + // 幸运 + case "幸运": + case "luck": case "LUCK": + return PotionEffectType.LUCK; + // 村庄英雄 + case "村庄英雄": + case "hero_of_the_village": case "HERO_OF_THE_VILLAGE": + return PotionEffectType.HERO_OF_THE_VILLAGE; + // 缓降 + case "缓降": + case "slow_falling": case "SLOW_FALLING": + return PotionEffectType.SLOW_FALLING; + // 潮涌能量 + case "潮涌能量": + case "conduit_power": case "CONDUIT_POWER": + return PotionEffectType.CONDUIT_POWER; + // 海豚的恩惠 + case "海豚的恩惠": + case "dolphins_grace": case "DOLPHINS_GRACE": + return PotionEffectType.DOLPHINS_GRACE; + // ========== 负面 ========== + // 缓慢 + case "缓慢": + case "slowness": case "SLOWNESS": + case "SLOW": + return PotionEffectType.SLOWNESS; + // 挖掘疲劳 + case "挖掘疲劳": + case "mining_fatigue": case "MINING_FATIGUE": + case "SLOW_DIGGING": + return PotionEffectType.MINING_FATIGUE; + // 瞬间伤害 + case "瞬间伤害": case "伤害药水": + case "instant_damage": case "INSTANT_DAMAGE": + case "HARM": + return PotionEffectType.INSTANT_DAMAGE; + // 反胃 + case "反胃": + case "nausea": case "NAUSEA": + return PotionEffectType.NAUSEA; + // 失明 + case "失明": + case "blindness": case "BLINDNESS": + return PotionEffectType.BLINDNESS; + // 饥饿 + case "饥饿": + case "hunger": case "HUNGER": + return PotionEffectType.HUNGER; + // 虚弱 + case "虚弱": + case "weakness": case "WEAKNESS": + return PotionEffectType.WEAKNESS; + // 中毒/致命中毒 + case "中毒": case "致命中毒": + case "poison": case "POISON": + case "poisonous": + return PotionEffectType.POISON; + // 凋零 + case "凋零": + case "wither": case "WITHER": + return PotionEffectType.WITHER; + // 飘浮/漂浮 + case "飘浮": case "漂浮": + case "levitation": case "LEVITATION": + return PotionEffectType.LEVITATION; + // 霉运/厄运 + case "霉运": case "厄运": case "不幸": + case "unluck": case "UNLUCK": + return PotionEffectType.UNLUCK; + // 黑暗 + case "黑暗": + case "darkness": case "DARKNESS": + return PotionEffectType.DARKNESS; + // 蓄风 + case "蓄风": + case "wind_charged": case "WIND_CHARGED": + return PotionEffectType.WIND_CHARGED; + // 盘丝 + case "盘丝": + case "weaving": case "WEAVING": + return PotionEffectType.WEAVING; + // 渗浆 + case "渗浆": + case "oozing": case "OOZING": + return PotionEffectType.OOZING; + // 寄生 + case "寄生": + case "infested": case "INFESTED": + return PotionEffectType.INFESTED; + // ========== 中性 ========== + // 不祥之兆 + case "不祥之兆": case "不祥征兆": + case "bad_omen": case "BAD_OMEN": + return PotionEffectType.BAD_OMEN; + // 袭击之兆 + case "袭击之兆": case "袭击征兆": + case "raid_omen": case "RAID_OMEN": + return PotionEffectType.RAID_OMEN; + // 试炼之兆 + case "试炼之兆": case "试炼征兆": + case "trial_omen": case "TRIAL_OMEN": + return PotionEffectType.TRIAL_OMEN; + default: + break; + } + + // ★ 终极兜底:用原版 getByName(兼容未来新增效果) + PotionEffectType fallback = PotionEffectType.getByName(clean.toLowerCase()); + if (fallback != null) return fallback; + + // 同一名称只警告一次,避免控制台刷屏 + if (UNKNOWN_EFFECT_WARNINGS.add(name)) { + plugin.getLogger().warning("[防护] 无法识别效果: " + name + " (clean=" + clean + "),已抑制后续相同警告"); + } + return null; + + } + + + private static final Map EFFECT_MAP + = new HashMap<>(); + + + private void clearRaidEffects(Player p) { + boolean cleared = false; + try { + if (p.hasPotionEffect(PotionEffectType.BAD_OMEN)) { + p.removePotionEffect(PotionEffectType.BAD_OMEN); + cleared = true; + } + } catch (Exception ignored) { + } + try { + if (p.hasPotionEffect(PotionEffectType.RAID_OMEN)) { + p.removePotionEffect(PotionEffectType.RAID_OMEN); + cleared = true; + } + } catch (Exception ignored) { + } + if (cleared) { + p.sendMessage("§c§l[区域防护] §f已清除袭击相关效果"); + } + } + + public Map> + getPendingClearEffects() { + return pendingClearEffects; + } + + public void onPlayerOffline(UUID uid, String playerName, + String areaName) { + // 清除该玩家在该区域的所有效果 + List given = getPlayerEffects(uid, areaName); + for (PotionEffectType type : given) { + // 只清除我们给予的效果 + Player p = Bukkit.getPlayer(uid); + if (p != null) { + p.removePotionEffect(type); + } + } + // 清除DB记录 + removePlayerEffects(uid, areaName); + plugin.getLogger().info("[防护] " + playerName + + " 下线,已清除" + given.size() + + "个效果并清理DB"); + } + + @EventHandler + public void onTeleport(PlayerTeleportEvent e) { + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + + if (from.getWorld() == to.getWorld() + && from.distance(to) < 5) return; + + Player p = e.getPlayer(); + UUID uid = p.getUniqueId(); + + // 标记传送中 + teleporting.add(uid); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + teleporting.remove(uid); + }, 5L); + + AreaConfig fromArea = getArea( + from.getWorld().getName(), + from.getBlockX(), from.getBlockY(), + from.getBlockZ()); + AreaConfig toArea = getArea( + to.getWorld().getName(), + to.getBlockX(), to.getBlockY(), + to.getBlockZ()); + + String fromName = fromArea != null + ? fromArea.name : null; + String toName = toArea != null + ? toArea.name : null; + + /* plugin.getLogger().info( + "[防护-传送] ★★★进入onTeleport: " + + p.getName() + + " [" + fromName + "] → [" + toName + "]" + + " teleporting=" + teleporting.contains(uid));*/ + + if (Objects.equals(fromName, toName)) { + /* plugin.getLogger().info( + "[防护-传送] 区域相同, 跳过");*/ + return; + } + + // ===== 离开旧区域 ===== + if (fromName != null) { + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★开始离开处理: " + + fromName);*/ + + // 收标 + List marked = + playerMarkedEffects.remove(uid); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★收标结果: " + + (marked != null ? marked.size() : "null"));*/ + + if (marked != null) { + for (PotionEffectType t : marked) { + /* plugin.getLogger().info( + "[防护-传送-离开] 标记项: " + + t.getName());*/ + } + } else { + /* plugin.getLogger().info( + "[防护-传送-离开] ★★标记为null! " + + "尝试其他key查找...");*/ + // 遍历查找可能的标记 + for (Map.Entry> entry + : playerMarkedEffects.entrySet()) { + if (entry.getKey().equals(uid)) { + /* plugin.getLogger().info( + "[防护-传送-离开] 找到标记! 效果数=" + + entry.getValue().size());*/ + } + } + /* plugin.getLogger().info( + "[防护-传送-离开] playerMarkedEffects大小=" + + playerMarkedEffects.size());*/ + } + + // 取消旧延时 + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + pendingClearArea.remove(uid); + pendingClearEffects.remove(uid); + } + + // ★★★ 传送到野外(无目标区域):立即清除领地效果 ★★★ + if (toName == null && fromArea != null + && fromArea.giveEffects != null && !fromArea.giveEffects.isEmpty()) { + plugin.getLogger().info( + "[防护-传送-离开野外] ★★★立即清除效果: 玩家=" + p.getName() + + " 区域=" + fromName); + int cleared = 0; + for (String[] eff : fromArea.giveEffects) { + PotionEffectType type = resolveEffectType(eff[0]); + if (type != null) { + PotionEffect pe = p.getPotionEffect(type); + if (pe != null) { + p.removePotionEffect(type); + cleared++; + plugin.getLogger().info( + "[防护-传送-离开野外] ★清除: " + eff[0]); + } + } + } + removePlayerEffects(uid, fromName); + plugin.getLogger().info( + "[防护-传送-离开野外] ★完成: 清除" + + cleared + "/" + fromArea.giveEffects.size() + "个效果"); + } + + // 创建5秒延时清理(传送跨领地差量清理) + if (marked != null && !marked.isEmpty() && toName != null) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + if (online == null + || !online.isOnline()) { + plugin.getLogger().info( + "[防护-传送-清理] 玩家不在线, 清DB"); + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] cleared " + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 100L); // 5秒 = 100 tick + pendingClearTask.put(uid, task); + + } + + // 离开提示(默认消息:感谢(玩家)光临(领地)) + String leaveMsg2 = (fromArea != null && fromArea.leaveMsg != null && !fromArea.leaveMsg.isEmpty()) + ? fromArea.leaveMsg : "感谢(玩家)光临(领地)"; + if (fromArea != null) { + p.sendMessage(formatAreaMsg(leaveMsg2, p.getName(), fromName)); + } + // ★ 离开领地时展示边框粒子(3秒) + if (fromArea != null) { + showBorderBrief(p, fromArea); + } + // ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还 + handleConfiscateReturn(p, fromName); + } + + // ===== 进入新区域 ===== + if (toName != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] ★★★开始进入处理: " + + toName);*/ + + // 取消旧延时 + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + String oldP = pendingClearArea.remove(uid); + plugin.getLogger().info( + "[防护-传送-进入] 取消旧延时, 旧区域=" + + oldP); + if (oldP != null) { + diffEffects(p, uid, oldP, toName); + } + } + + // 进入提示(默认消息:欢迎(玩家)来到(领地)) + String enterMsg2 = (toArea != null && toArea.enterMsg != null && !toArea.enterMsg.isEmpty()) + ? toArea.enterMsg : "欢迎(玩家)来到(领地)"; + p.sendMessage(formatAreaMsg(enterMsg2, p.getName(), toName)); + + // ★ 进入领地时展示边框粒子(3秒) + if (toArea != null) { + showBorderBrief(p, toArea); + } + + // 发放效果 + if (toArea != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] 调用applyRegionEffects");*/ + applyRegionEffects(p, uid, toName, toArea); +// 清除效果 + clearBadEffects(p, toArea); + + // ★ 没收违禁品(使用统一的handleConfiscate) + if (!isExemptFromConfiscation(p, toName)) { + handleConfiscate(p, toArea); + } + } + + // 强制模式 + if (toArea != null && toArea.enforceGameMode != null + && !toArea.enforceGameMode.isEmpty() + && !isExemptFromGameMode(p, toName)) { + GameMode target = GameMode.valueOf( + toArea.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + } + } + } + + // 更新记录 + if (toName != null) { + playerCurrentArea.put(uid, toName); + } else { + playerCurrentArea.remove(uid); + } + } + + + public Map getPendingClearTasks() { + return pendingClearTask; + } + + public Map> + getPlayerMarkedEffects() { + return playerMarkedEffects; + } + + public void handleInside(Player p, AreaConfig ac) { + clearBadEffects(p, ac); + + if (ac.peaceMode) { + banHostilesWithWhitelist(p, ac); + } else if (ac.denyRaid) { + banRaidMobs(p, ac); + } + + // 所有者或管理员 → 跳过所有限制性规则(包括没收) + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + + handleConfiscate(p, ac); + } + + public void cleanupExpiredProtections() { + protectedEntities.entrySet().removeIf( + entry -> System.currentTimeMillis() > entry.getValue()); + } + + // 禁止袭击:只驱逐袭击怪物 + private void banRaidMobs(Player p, AreaConfig ac) { + for (Entity ent : p.getNearbyEntities(32, 32, 32)) { + String t = ent.getType().name(); + if (t.equals("PILLAGER") + || t.equals("VINDICATOR") + || t.equals("EVOKER") + || t.equals("RAVAGER") + || t.equals("VEX") + || t.equals("WITCH") + || t.equals("ILLUSIONER") + || t.equals("ZOMBIE_VILLAGER")) { + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(voidLoc.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + } + } + } + + public Set getAreaNames() { + return areas.keySet(); + } + + // ==================== 领地边框粒子展示 ==================== + + /** + * 向玩家展示领地边框粒子特效(只展示给指定玩家) + * @param ac 领地配置 + * @param durationTicks 持续tick数(20tick=1秒) + */ + public void showBorderParticles(Player p, AreaConfig ac, int durationTicks) { + if (ac == null) return; + World world = Bukkit.getWorld(ac.world); + if (world == null) return; + + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + + // === 性能优化:动态密度控制 === + int edgeLenX = maxX - minX + 1; + int edgeLenZ = maxZ - minZ + 1; + int perimeter = 2 * (edgeLenX + edgeLenZ); + + // 超大领地直接跳过粒子(>200格边长) + if (edgeLenX > 200 || edgeLenZ > 200) { + return; + } + + // 动态步长:保证每条边最多~30个粒子点 + int step = Math.max(2, Math.min(6, perimeter / 120)); + // 每轮粒子总量上限60(不含四角) + int maxParticlesPerRound = 60; + + Particle.DustTransition dust = new Particle.DustTransition(org.bukkit.Color.fromRGB(0, 255, 255), org.bukkit.Color.fromRGB(255, 255, 0), 1.2f); + + // 每20tick刷新一次粒子(共durationTicks/20次) + int rounds = Math.max(1, durationTicks / 20); + for (int i = 0; i < rounds; i++) { + final int round = i; + final int fStep = step; + new org.bukkit.scheduler.BukkitRunnable() { + @Override + public void run() { + if (!p.isOnline()) { cancel(); return; } + + // 在玩家Y层展示四条边 + int playerY = p.getLocation().getBlockY(); + int y = Math.max(ac.yMin, Math.min(playerY, ac.yMax)); + + int count = 0; + // 四条边 + for (int x = minX; x <= maxX && count < maxParticlesPerRound; x += fStep) { + p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, x + 0.5, y, minZ + 0.5), 1, dust); + p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, x + 0.5, y, maxZ + 0.5), 1, dust); + count += 2; + } + for (int z = minZ; z <= maxZ && count < maxParticlesPerRound; z += fStep) { + p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, minX + 0.5, y, z + 0.5), 1, dust); + p.spawnParticle(Particle.DUST_COLOR_TRANSITION, new Location(world, maxX + 0.5, y, z + 0.5), 1, dust); + count += 2; + } + // 四个角高亮(各1个粒子,减少ProtocolLib包量) + p.spawnParticle(Particle.END_ROD, new Location(world, minX + 0.5, y, minZ + 0.5), 1); + p.spawnParticle(Particle.END_ROD, new Location(world, maxX + 0.5, y, minZ + 0.5), 1); + p.spawnParticle(Particle.END_ROD, new Location(world, minX + 0.5, y, maxZ + 0.5), 1); + p.spawnParticle(Particle.END_ROD, new Location(world, maxX + 0.5, y, maxZ + 0.5), 1); + } + }.runTaskLater(plugin, round * 20L); + } + } + + /** + * 短时间展示领地边框(进入/离开时,3秒) + */ + public void showBorderBrief(Player p, AreaConfig ac) { + showBorderParticles(p, ac, 60); // 3秒 + } + + /** 获取所有用户组名(tab补全用) */ + /** + * ★ 领地增益效果数量上限(2026-10-04 任务3)。 + * 普通玩家 5 个;用户组配置 max_effects 可覆盖(<=0=不限)。 + * @return >0=上限数量;Integer.MAX_VALUE=不限 + */ + public int getMaxGiveEffects(String ownerName) { + try { + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm != null) { + return ugm.getPlayerMaxEffects(ownerName, 5); + } + } catch (Exception ignored) {} + return 5; + } + + /** + * ★ 领地增益【药效等级上限】(2026-10-04 任务4)。 + * 普通玩家(无用户组)严格按原版最大药效强度 256 封顶; + * 用户组玩家按组配置 max_effect_level,组里配多少就是多少。 + * + * @param ownerName 领地所有者(上限跟随领地所有者所属用户组,与数量上限一致口径) + * @return 药效强度上限,恒在 1~256 之间 + */ + public int getMaxGiveEffectLevel(String ownerName) { + try { + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm != null) { + return ugm.getPlayerMaxEffectLevel(ownerName, + UserGroupManager.DEFAULT_MAX_EFFECT_LEVEL); + } + } catch (Exception ignored) {} + return UserGroupManager.DEFAULT_MAX_EFFECT_LEVEL; + } + + /** + * 把领地配置里的药效等级夹到该领地的合法上限内。 + * 所有写入/应用入口都必须过这一刀。 + * + * @param rawLevel 配置里写的原始药效强度 + * @param ownerName 领地所有者 + * @return 夹紧后的药效强度 + */ + public int clampGiveEffectLevel(int rawLevel, String ownerName) { + int cap = getMaxGiveEffectLevel(ownerName); + if (rawLevel < 1) return 1; + return rawLevel > cap ? cap : rawLevel; + } + + /** + * ★ 生存模式可获取的最大等级(2026-10-04 任务4 用户纠正)。 + * + *

口径:不是 /effect 命令的作弊上限(amplifier 255 = 强度 256), + * 而是生存模式下正常探索、合成、建造能拿到的最大等级。 + * 例:抗性提升靠信标最高 2 级;海豚的恩惠只有潮涌能量块一个来源,最高 1 级。

+ * + *

取值来源:Minecraft 中文维基「状态效果」「信标」逐项核对, + * 效果清单与 paper-api 的 39 个 PotionEffectType 完全对齐。

+ * + *

默认返回 1:未知效果一律按最低档处理,宁可少给也不超发。

+ */ + private static int getSurvivalMaxLevel(String effectName) { + if (effectName == null) return 1; + String clean = effectName.trim() + .replaceAll("^[\\s\"'\\\\]+|[\\s\"'\\\\]+$", ""); + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + if (clean.isEmpty()) return 1; + switch (clean) { + // ===== 信标类主效果:4级金字塔可选「提升至II」→ 2;无药水的固定 1 ===== + case "迅捷": case "速度": case "speed": case "SPEED": + return 2; // 迅捷药水(增强版) II;信标 I+II + case "急迫": case "haste": case "HASTE": + return 2; // 无药水,仅信标;4级金字塔可升 II + case "抗性提升": case "抗性": case "resistance": case "RESISTANCE": + return 2; // 抗性药水 II;信标 I+II + case "跳跃提升": case "跳跃": case "jump_boost": case "JUMP_BOOST": case "JUMP": + return 2; // 跳跃药水(增强版) II;信标 I+II + case "力量": case "strength": case "STRENGTH": case "INCREASE_DAMAGE": + return 2; // 力量药水 II;信标 I+II + // ===== 有药水且可升 II 的效果 ===== + case "瞬间治疗": case "治疗": case "instant_health": case "INSTANT_HEALTH": case "HEAL": + case "瞬间伤害": case "伤害": case "instant_damage": case "INSTANT_DAMAGE": case "HARM": + case "缓慢": case "slowness": case "SLOWNESS": + case "中毒": case "poison": case "POISON": + case "生命恢复": case "再生": case "回复": case "regeneration": case "REGENERATION": + return 2; // 各药水均可用发光石粉升至 II + case "挖掘疲劳": case "mining_fatigue": case "MINING_FATIGUE": + return 3; // 无药水;远古守卫者固定给 III(Wiki:natural effect level is III) + // ===== 生存只有单一来源、等级恒为 1 ===== + case "海豚的恩惠": case "海豚恩惠": case "dolphins_grace": case "DOLPHINS_GRACE": + return 1; // 仅潮涌能量块 / 海豚馈赠,无等级区分 + case "潮涌能量": case "conduit_power": case "CONDUIT_POWER": + return 1; + case "抗火": case "fire_resistance": case "FIRE_RESISTANCE": + return 1; // 抗火药水无升级配方 + case "水下呼吸": case "water_breathing": case "WATER_BREATHING": + return 1; // 水肺药水无升级配方 + case "隐身": case "invisibility": case "INVISIBILITY": + return 1; // 隐身药水无升级配方 + case "夜视": case "night_vision": case "NIGHT_VISION": + return 1; // 夜视药水无升级配方 + case "缓降": case "slow_falling": case "SLOW_FALLING": + return 1; // 缓降药水无升级配方 + case "虚弱": case "weakness": case "WEAKNESS": + return 1; // 虚弱药水无升级配方 + case "反胃": case "恶心": case "nausea": case "NAUSEA": + return 1; // 河豚等食物,固定 I + case "饥饿": case "hunger": case "HUNGER": + return 1; // 生肉/腐肉,固定 I + case "凋零": case "wither": case "WITHER": + return 1; // 凋灵之首施加,固定 I + case "失明": case "blindness": case "BLINDNESS": + return 1; + case "黑暗": case "darkness": case "DARKNESS": + return 1; // 监守者等施加,固定 I + case "发光": case "glowing": case "GLOWING": + return 1; // 光灵箭,固定 I + case "飘浮": case "悬浮": case "levitation": case "LEVITATION": + return 1; // 潜影弹,固定 I + case "不祥之兆": case "bad_omen": case "BAD_OMEN": + return 5; // 不祥之兆瓶 I–V(1.16 起上限 5),进村转袭击之兆时等级保留 + case "试炼之兆": case "trial_omen": case "TRIAL_OMEN": + case "袭击之兆": case "raid_omen": case "RAID_OMEN": + return 5; // 均由不祥之兆转化而来,等级沿用,故同为 5 + case "村庄英雄": case "hero_of_the_village": case "HERO_OF_THE_VILLAGE": + return 5; // JE 袭击全歼后按袭击等级给 I–V(Wiki:potency I–V) + case "蓄风": case "wind_charged": case "WIND_CHARGED": + case "盘丝": case "weaving": case "WEAVING": + case "渗浆": case "oozing": case "OOZING": + case "寄生": case "infested": case "INFESTED": + return 1; // 均为事件触发,无等级区分 + case "鹦鹉螺之息": case "breath_of_the_nautilus": case "BREATH_OF_THE_NAUTILUS": + return 1; + // ===== 生存无来源,仅命令/插件可得 ===== + case "生命提升": case "health_boost": case "HEALTH_BOOST": + return 1; // Wiki:Health Boost 仅能靠 /effect 获得 + case "伤害吸收": case "absorption": case "ABSORPTION": + return 4; // 附魔金苹果给 IV(金苹果/图腾只给 I/II),Wiki 1.9 起 IV + case "饱和": case "saturation": case "SATURATION": + case "幸运": case "luck": case "LUCK": + case "霉运": case "unluck": case "UNLUCK": + return 1; + default: + return 1; // 未知效果:宁可少给 + } + } + + /** + * ★ 单个效果的最终封顶:min(生存可获取上限, 该领地用户组配置的全局上限)。 + * + *

两层含义同时生效,缺一不可: + *

    + *
  1. 生存上限(硬顶):该效果在正常玩法里根本拿不到更高等级, + * 例如海豚的恩惠最多 1 级,配 255 也只给 1 级。
  2. + *
  3. 用户组上限(软顶):组里配多少就是多少, + * 但同样不得突破上面那条生存上限。
  4. + *

+ * + * @param effectName 效果名(支持中文名与英文 ID) + * @param ownerName 领地所有者(决定用户组上限) + * @return 药效强度上限,恒 >=1 + */ + public int getEffectLevelCap(String effectName, String ownerName) { + int survival = getSurvivalMaxLevel(effectName); + int group = getMaxGiveEffectLevel(ownerName); + if (group < survival) return group < 1 ? 1 : group; + return survival; + } + + /** + * 按效果名封顶(各应用点统一走这里)。 + */ + public int clampEffectLevel(int rawLevel, String effectName, String ownerName) { + int cap = getEffectLevelCap(effectName, ownerName); + if (rawLevel < 1) return 1; + return rawLevel > cap ? cap : rawLevel; + } + + public List getUserGroupNames() { + List names = new ArrayList<>(); + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm != null) { + for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) { + names.add(cfg.name); + } + } + return names; + } + + /** + * 根据名称获取领地配置 + */ + public AreaConfig getLand(String name) { + return areas.get(name); + } + + /** + * 获取所有领地(只读) + */ + public Map getAllLands() { + return Collections.unmodifiableMap(areas); + } + + /** + * 检查领地是否有待删除请求 + */ + public boolean hasPendingDelete(String areaName) { + return pendingDeletes.containsKey(areaName); + } + + /** + * 获取指定玩家拥有的所有领地 + */ + public List getLandsByOwner(String ownerName) { + List result = new ArrayList<>(); + for (AreaConfig ac : areas.values()) { + if (samePlayer(ac.owner, ownerName)) { + result.add(ac); + } + } + return result; + } + + /** + * 删除领地 + */ + public void deleteLand(String name) { + areas.remove(name); + // 删除数据库记录 + try { + if (dbConnection != null && !dbConnection.isClosed()) { + PreparedStatement ps = dbConnection.prepareStatement( + "DELETE FROM area_lands WHERE name = ?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 删除领地数据库失败: " + e.getMessage()); + } + } + + /** + * 获取领地成员列表(白名单) + */ + public Set getLandMembers(String landName) { + Set members = areaPlayerWhitelist.get(landName); + return members != null ? new HashSet<>(members) : new HashSet<>(); + } + + /** + * 添加领地成员(领地主自动跳过,不允许作为成员入库) + */ + public boolean addLandMember(String landName, String playerName) { + AreaConfig ac = getLand(landName); + if (ac != null && ac.owner != null && samePlayer(ac.owner, playerName)) { + return false; // 领地主不能作为成员添加 + } + Set members = areaPlayerWhitelist.computeIfAbsent( + landName, k -> ConcurrentHashMap.newKeySet()); + members.add(normName(playerName)); + saveWhitelists(); + return true; + } + + /** + * 移除领地成员 + */ + public void removeLandMember(String landName, String playerName) { + Set members = areaPlayerWhitelist.get(landName); + if (members != null) { + // ★ 精确 / 旧版默认Locale小写 / normName 三种格式全部尝试移除 + members.remove(playerName); + members.remove(playerName.toLowerCase()); + members.remove(normName(playerName)); + saveWhitelists(); + } + // ★ 同步删除 area_land_permissions 记录(确保 admin 身份一并清除) + removePlayerFromAreaWhitelist(landName, playerName); + } + + /** + * 获取玩家当前所在区域名 + */ + public String getPlayerArea(Player p) { + return playerCurrentArea.get(p.getUniqueId()); + } + + public Collection getPendingTasks() { + return pendingClearTask.values(); + } + + + // 判断是否为远程攻击敌对生物 + private boolean isRangedHostile(String typeName) { + switch (typeName) { + // ===== 骷髅系 ===== + case "SKELETON": // 骷髅(弓箭) + case "STRAY": // 流浪者(迟缓之箭) + case "BOGGED": // 虫蚀骷髅(毒箭) + case "WITHER_SKELETON": // 凋灵骷髅(近战但算灾厄) + + // ===== 灾厄村民 ===== + case "PILLAGER": // 掠夺者(弩) + case "ILLUSIONER": // 幻术师(弓箭+失明) + case "EVOKER": // 幻魔者(唤魔) + case "VINDICATOR": // 卫道士(斧头近战) + case "VEX": // 恼鬼(剑近战,但会穿墙飞行) + + // ===== 法术/投射 ===== + case "BLAZE": // 烈焰人(火球) + case "WITCH": // 女巫(药水) + + // ===== 飞行/爆炸 ===== + case "GHAST": // 恶魂(火球) + case "PHANTOM": // 幻翼(俯冲) + case "BIG_GHAST": // 大恶魂(1.21新增) + + // ===== 水下 ===== + case "GUARDIAN": // 守卫者(激光) + case "ELDER_GUARDIAN": // 远古守卫者(激光) + + // ===== 远程特殊 ===== + case "SHULKER": // 潜影贝(潜影弹) + case "BREEZE": // 旋风人(风弹) + + // ===== 蜘蛛系(远程毒液攻击) ===== + case "SPIDER": // 蜘蛛(白天远程吐丝) + case "CAVE_SPIDER": // 洞穴蜘蛛(毒) + + // ===== 1.21新增 ===== + case "WIND_CHARGED": // 蓄风怪(风弹) + case "WEAVING": // 盘丝怪(吐丝) + case "OOZING": // 渗浆怪(史莱姆) + case "INFESTED": // 寄生怪(蠹虫) + + // ===== 盔甲/变种(部分版本) ===== + case "ARMORED_SKELETON": // 盔甲骷髅 + case "ARCHER_SKELETON": // 弓箭骷髅(部分版本) + case "SNOW_GOLEM": // 雪傀儡(不是敌对但远程) + + // ===== 亡灵骑乘 ===== + case "HUSK": // 尸壳(近战但属于灾厄系) + case "DROWNED": // 溺尸(三叉戟远程) + case "ZOMBIE_VILLAGER": // 僵尸村民 + case "ZOMBIE_PIGMAN": // 僵尸猪灵 + case "ZOMBIFIED_PIGLIN": // 僵尸猪灵(1.16+) + case "PIGLIN_BRUTE": // 猪灵蛮兵 + + // ===== 猪灵(弓箭) ===== + case "PIGLIN": // 猪灵(金弩远程) + + // ===== 末影系 ===== + case "ENDER_DRAGON": // 末影龙(远程吐息) + case "WARDEN": // 监守者(远程音波) + + // ===== 其他 ===== + case "CREPER": // 苦力怕(爆炸不算远程但危险) + case "CREEPER": // 苦力怕 + + return true; + default: + return false; + } + } + + + + private void applyEffects(Player p, AreaConfig ac) { + // ★ 2026-10-04:负面效果从 giveEffects 拆到 badEffects,两边都要给 + List all = new ArrayList<>(ac.giveEffects); + if (ac.badEffects != null) all.addAll(ac.badEffects); + for (String[] parts : all) { + String effName = parts[0]; + int level = 1; + int duration = 999; + try { level = Integer.parseInt(parts[1]); } + catch (NumberFormatException ignored) {} + try { duration = Integer.parseInt(parts[2]); } + catch (NumberFormatException ignored) {} + // ★ 药效等级封顶(2026-10-04 任务4):按该效果的生存可获取上限 + 用户组配置 + level = clampEffectLevel(level, effName, ac.owner); + + PotionEffectType type = resolveEffectType(effName); + plugin.getLogger().info("[防护调试] 给予: " + + effName + " -> " + type); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + plugin.getLogger().info("[防护调试] 已给予: " + + effName + " 等级" + level + + " 持续" + duration + "秒"); + } + } + } + + + @EventHandler + public void onBlockPlace(BlockPlaceEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getBlock().getX(), + e.getBlock().getY(), + e.getBlock().getZ()); + if (ac == null) return; + + Material placedMat = e.getBlock().getType(); + + // ★ 农作物种植权限分层:种子/农作物由denyCropHarvest独立控制 + if (isCropSeed(placedMat)) { + if (getEffectiveDeny(p, ac, "denyCropHarvest")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止种植农作物"); + } + return; // 农作物种子不走denyBlockPlace + } + + // 非农作物:正常denyBlockPlace检查 + if (getEffectiveDeny(p, ac, "denyBlockPlace")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置方块"); + } + // ★ 领地记录:记录成功放置的方块(事件未被取消才记录) + if (!e.isCancelled()) { + plugin.landRecordManager.recordBlockPlace(ac, p, e.getBlock()); + } + } + + @EventHandler + public void onEntityPlace(org.bukkit.event.entity.EntityPlaceEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = e.getPlayer(); + Location loc = e.getEntity().getLocation(); + AreaConfig ac = getArea( + p.getWorld().getName(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ()); + if (ac == null) return; + + // ★ 生物蛋权限:上级权限=放置方块(放置未开启时,单独开启生物蛋不生效) + if (isSpawnEggItem(p.getInventory().getItemInMainHand().getType()) + && getEffectiveDeny(p, ac, "denyBlockPlace") + && getEffectiveDeny(p, ac, "denySpawnEgg")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋"); + return; + } + + // 层级:denyBlockPlace (一级) → denyEntityInteract (二级) → denyItemFrame (三级) + if (getEffectiveDeny(p, ac, "denyBlockPlace")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置方块或实体"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置实体"); + return; + } + // 三级:展示框特定 + String typeName = e.getEntity().getType().name(); + if ((typeName.equals("ITEM_FRAME") || typeName.equals("GLOW_ITEM_FRAME")) + && getEffectiveDeny(p, ac, "denyItemFrame")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置展示框"); + } + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getBlock().getX(), + e.getBlock().getY(), + e.getBlock().getZ()); + if (ac == null) return; + + Material blockMat = e.getBlock().getType(); + + // ★ 农作物权限分层:农作物由denyCropHarvest独立控制,不受denyBlockBreak影响 + if (isCropBlock(blockMat)) { + if (getEffectiveDeny(p, ac, "denyCropHarvest")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止收获农作物"); + } + return; // 农作物不走denyBlockBreak + } + + // 非农作物:正常denyBlockBreak检查 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏方块"); + } + // ★ 领地记录:记录成功破坏的方块(事件未被取消才记录) + if (!e.isCancelled()) { + plugin.landRecordManager.recordBlockBreak(ac, p, e.getBlock()); + } + } + + @EventHandler + public void onSignChange(SignChangeEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getBlock().getX(), + e.getBlock().getY(), + e.getBlock().getZ()); + if (ac == null) return; + + // ★ 记录告示牌放置/编辑(权限检查前记录,被取消也要记录尝试) + // 检测是编辑还是放置:如果该位置已有告示牌方块数据 + boolean isEdit = e.getBlock().getState() instanceof org.bukkit.block.Sign; + String[] lines = e.getLines(); + // 延迟记录(等事件处理完后,内容已写入) + final boolean editFinal = isEdit; + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + plugin.landRecordManager.recordSignEdit(ac, p, e.getBlock(), + lines, editFinal); + }, 1L); + + if (p.hasPermission("sdf1.admin")) return; // 管理员跳过 + if (samePlayer(p.getName(), ac.owner)) return; // 领地主免检 + if (isAreaAdmin(p)) return; // 插件管理员免检 + // ★ 授权成员(白名单内玩家)也豁免denySignEdit + if (isPlayerWhitelisted(p.getName(), ac)) return; + + if (getEffectiveDeny(p, ac, "denySignEdit")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止编辑告示牌"); + } + } + + + @EventHandler + public void onDamage(EntityDamageEvent e) { + if (!(e.getEntity() instanceof Player)) return; + Player p = (Player) e.getEntity(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + // 保护性规则:支持per-player独立权限 + if (getEffectiveDeny(p, ac, "denyAllDamage")) { e.setCancelled(true); return; } + if (getEffectiveDeny(p, ac, "denyFallDamage") + && e.getCause() == EntityDamageEvent.DamageCause.FALL) + e.setCancelled(true); + } + + + @EventHandler + public void onFood(FoodLevelChangeEvent e) { + if (!(e.getEntity() instanceof Player)) return; + Player p = (Player) e.getEntity(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null && getEffectiveDeny(p, ac, "denyHunger")) + e.setCancelled(true); + } + + + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + // 使用getEffectiveDeny,OWNER/Admin自动豁免,visitor走领地默认 + if (getEffectiveDeny(p, ac, "denyDrop")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); + } + } + + // ★ 拾取限制:allowPickup=false时禁止拾取(denyPickup映射到allowPickup) + @EventHandler + public void onPlayerPickup(PlayerPickupItemEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + // 使用getEffectiveDeny,OWNER/Admin自动豁免,visitor走领地默认 + if (getEffectiveDeny(p, ac, "denyPickup")) { + e.setCancelled(true); + // 节流:5~10秒随机间隔 + long now = System.currentTimeMillis(); + Long last = lastPickupMsgTime.get(p.getUniqueId()); + long cooldown = 5000 + new java.util.Random().nextLong(5000); // 5~10秒 + if (last == null || now - last > cooldown) { + lastPickupMsgTime.put(p.getUniqueId(), now); + p.sendMessage(PICKUP_MSGS[new java.util.Random().nextInt(PICKUP_MSGS.length)]); + } + } + } + + // ===== 涂蜡/刮蜡权限(denyWax)—— 独立处理器 ===== + // 为什么单独开一个方法: + // ① onInteract 开头有选区工具等分支会提前 return,合并写会被短路跳过; + // ② 用 e.getItem()/e.getHand() 取"实际使用的那只手",副手蜜脾/副手斧同样拦截; + // ③ 同时 DENY useInteractedBlock + useItemInHand,确保原版"刮蜡/涂蜡"流程(1.20.5+ 在 + // ServerPlayerGameMode.useItemOn 中先查 useItemInHand 再继续 item.useOn)被彻底切断; + // ④ HIGHEST 优先级,避免被其它监听器的默认处理覆盖。 + @EventHandler(priority = EventPriority.HIGHEST) + public void onWaxInteract(PlayerInteractEvent e) { + Action act = e.getAction(); + if (act != Action.RIGHT_CLICK_BLOCK && act != Action.RIGHT_CLICK_AIR) return; + // 副手事件会与主手重复触发一次,只处理主手 + if (e.getHand() == org.bukkit.inventory.EquipmentSlot.OFF_HAND) return; + + Player p = e.getPlayer(); + ItemStack used = e.getItem(); + if (used == null || used.getType().isAir()) { + used = p.getInventory().getItemInMainHand(); + } + Material mat = used.getType(); + boolean waxOn = mat == Material.HONEYCOMB; + boolean waxOff = mat.name().endsWith("_AXE"); // 木/石/铜/铁/金/钻/下界合金斧全部匹配 + if (!waxOn && !waxOff) return; + + Block waxBlock = e.getClickedBlock(); + if (waxBlock == null) { + try { waxBlock = p.getTargetBlockExact(6); } catch (Exception ignored) {} + } + if (waxBlock == null || !isWaxableBlock(waxBlock)) return; + + AreaConfig waxAc = getArea(waxBlock.getWorld().getName(), + waxBlock.getX(), waxBlock.getY(), waxBlock.getZ()); + if (waxAc == null) return; // 不在任何领地内 → 不干预 + + if (isWaxDenied(p, waxAc)) { + e.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY); + e.setUseItemInHand(org.bukkit.event.Event.Result.DENY); + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止" + (waxOn ? "涂蜡" : "刮蜡")); + return; + } + + // ★ 诊断日志:领地已开启 denyWax 却放行时,把放行原因打进控制台, + // 方便定位"为什么没拦住"(所有者/管理员/白名单访客豁免是最常见原因)。 + if (waxAc.denyWax) { + plugin.getLogger().info("[防护] 放行" + (waxOn ? "涂蜡" : "刮蜡") + + ": 玩家=" + p.getName() + " 领地=" + waxAc.name + + " 原因=" + waxAllowReason(p, waxAc)); + } + } + + /** 诊断用:denyWax 已开却放行的原因(2026-09-29 对齐后放行路径与普通 deny* 一致) */ + private String waxAllowReason(Player p, AreaConfig ac) { + try { + PermissionLevel lvl = getPermissionLevel(p, ac); + if (lvl == PermissionLevel.ADMIN) return "插件/领地管理员豁免"; + if (lvl == PermissionLevel.OWNER) return "领地主豁免"; + if (lvl == PermissionLevel.MEMBER) return "已授权成员豁免"; + int landId = getLandIdFromDb(ac.name); + if (landId > 0) { + Map perms = getPlayerPermMap(landId, p.getName()); + if (perms != null && perms.containsKey("denyWax") + && !Boolean.TRUE.equals(perms.get("denyWax"))) { + return "该玩家被单独放行(denyWax=false)"; + } + } + if (lvl == PermissionLevel.VISITOR) return "白名单访客豁免"; + } catch (Exception ignored) {} + return "未知(请反馈该日志)"; + } + + // 替换整个 onInteract 方法 + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand == null) return; + + // ===== 选区工具 ===== + if (hand.getType() == Material.BLAZE_ROD) { + ItemMeta meta = hand.getItemMeta(); + if (meta != null && meta.getDisplayName() != null + && meta.getDisplayName().contains("区域选择")) { + Action action = e.getAction(); + boolean isLeft = (action == Action.LEFT_CLICK_AIR + || action == Action.LEFT_CLICK_BLOCK); + boolean isRight = (action == Action.RIGHT_CLICK_AIR + || action == Action.RIGHT_CLICK_BLOCK); + if (isLeft || isRight) { + e.setCancelled(true); + // ★ SN 归属 + 状态双校验(2026-10-03 两次收紧): + // 只认「登记在本人名下 + 状态 active」的圈地工具; + // 注销 / 报失 / 非法绑定 / 补发了结 / 已销毁 一律不支持选区 + // (isAreaAdmin 豁免);无 SN、未在册同样拦截。 + if (!isAreaAdmin(p)) { + SnManager snm = plugin.getSnManager(); + String wandSn = (snm != null) ? snm.readSn(hand) : null; + Map wandRow = (wandSn != null) ? snm.getSn(wandSn) : null; + Object wandOwnerObj = (wandRow != null) ? wandRow.get("owner") : null; + String wandOwner = (wandOwnerObj == null) ? null : String.valueOf(wandOwnerObj); + boolean ownerOk = wandSn != null && wandRow != null + && wandOwner != null && !wandOwner.isEmpty() + && samePlayer(wandOwner, p.getName()); + if (!ownerOk) { + String why = (wandSn == null + ? "该工具没有绑定 SN,请用 §e/protect 工具 §f重新申领" + : (wandOwner == null || wandOwner.isEmpty() + ? "该工具的 SN 未在登记簿中,请用 §e/protect 工具 §f重新申领" + : "这是 §e" + wandOwner + " §f的圈地工具")); + p.sendMessage("§c§l[防护] §f请使用你自己的圈地工具选区——" + why); + return; + } + Object wandStatusObj = wandRow.get("status"); + String wandStatus = (wandStatusObj == null) ? null : String.valueOf(wandStatusObj); + if (!SnManager.ST_ACTIVE.equals(wandStatus)) { + p.sendMessage("§c§l[防护] §f该圈地工具的登记状态为 §e" + + snm.statusCn(wandStatus) + + "§f,不支持选区——请用 §e/protect 工具 §f重新申领"); + return; + } + } + Block block = e.getClickedBlock(); + if (block == null) { + try { block = p.getTargetBlockExact(5); } + catch (Exception ignored) {} + } + if (block == null) return; + UUID uid = p.getUniqueId(); + if (isLeft) { + pos1.put(uid, block.getLocation()); + p.sendMessage("§a§l[防护] §f位置1: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + } else { + Location newLoc = block.getLocation(); + Location oldLoc = pos2.get(uid); + boolean changed = oldLoc == null + || oldLoc.getBlockX() != newLoc.getBlockX() + || oldLoc.getBlockY() != newLoc.getBlockY() + || oldLoc.getBlockZ() != newLoc.getBlockZ(); + pos2.put(uid, newLoc); + if (changed) { + p.sendMessage("§a§l[防护] §f位置2: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + // ★ 两个位置都选好后,显示面积+价格预览 + 一键创建超链接 + if (pos1.containsKey(uid)) { + Location lp1 = pos1.get(uid); + int w = Math.abs(newLoc.getBlockX() - lp1.getBlockX()) + 1; + int l = Math.abs(newLoc.getBlockZ() - lp1.getBlockZ()) + 1; + int a = w * l; + UserGroupManager ug = plugin.getUserGroup(); + int pricePerSqm = (ug != null) ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; + int totalCost = a * pricePerSqm; + String src = (pricePerSqm != globalCreatePricePerSqm) ? " §7(用户组优惠价)" : ""; + BondManager bnd = plugin.getBonds(); + int bal = (bnd != null) ? bnd.getBonds(p.getName()) : 0; + p.sendMessage("§e§l[防护] §f选区完成!面积: §a" + a + "㎡§7(" + w + "×" + l + ") 单价: §f" + pricePerSqm + "/㎡" + src + " 预估: §e" + totalCost + "§7债券 余额: §a" + bal); + // ★ 第57批续21 圈地棒选区后智能识别:若落在已有领地内,提示需具备该领地管理身份才能建子领地 + { + int sx1 = Math.min(lp1.getBlockX(), newLoc.getBlockX()); + int sx2 = Math.max(lp1.getBlockX(), newLoc.getBlockX()); + int sz1 = Math.min(lp1.getBlockZ(), newLoc.getBlockZ()); + int sz2 = Math.max(lp1.getBlockZ(), newLoc.getBlockZ()); + AreaConfig selMain = findContainingMainLand(p.getWorld().getName(), sx1, sz1, sx2, sz2); + if (selMain != null) { + p.sendMessage("§d§l[智能识别] §f选区位于领地 §e" + selMain.name + " §f内 → 创建成功则为【子领地】,需你具备该领地领主/管理员身份"); + if (!isQualifiedLandManager(p, selMain)) { + p.sendMessage("§c 你当前不具备 §e" + selMain.name + " §f的管理身份,确认创建将被拒绝"); + } + } else { + for (AreaConfig sot : areas.values()) { + if (sot == null || sot.parentId != 0) continue; + if (!sot.world.equalsIgnoreCase(p.getWorld().getName())) continue; + int so1x = Math.min(sot.x1, sot.x2), so2x = Math.max(sot.x1, sot.x2); + int so1z = Math.min(sot.z1, sot.z2), so2z = Math.max(sot.z1, sot.z2); + if (sx1 <= so2x && sx2 >= so1x && sz1 <= so2z && sz2 >= so1z) { + p.sendMessage("§c§l[智能识别] §f选区与领地 §e" + sot.name + " §f部分重叠(跨边界),子领地必须完整位于某主领地内"); + break; + } + } + } + } + // ★ 一键创建超链接:自动用玩家名,冲突则加数字(重名不区分大小写) + String autoName = p.getName(); + if (areaNameExists(autoName)) { + for (int i = 0; i <= 99; i++) { + String candidate = autoName + i; + if (!areaNameExists(candidate)) { autoName = candidate; break; } + } + } + String finalAutoName = autoName; + p.sendMessage(Component.empty() + .append(Component.text("§a§l[✅ 点击一键创建]")) + .hoverEvent(HoverEvent.showText(Component.text("§e以 §f" + finalAutoName + " §e为名创建领地\n§7费用: " + totalCost + " 债券"))) + .clickEvent(ClickEvent.runCommand("/protect confirm_create " + finalAutoName)) + ); + p.sendMessage(Component.text("§7或输入 §f/protect 创建 <自定义名> §7指定领地名")); + } + } + } + } + return; + } + } + + // ===== 涂蜡/刮蜡权限(denyWax)===== + // ★ 已拆成独立处理器 onWaxInteract(见本类 onInteract 方法之前): + // ① 本方法开头有多处提前 return(选区工具分支等),合并写会被短路跳过; + // ② 独立处理器改用 e.getItem()/e.getHand() 取"实际使用的手",副手蜜脾/斧同样拦截。 + + // ===== 生物蛋权限(denySpawnEgg)===== + // 上级权限=放置方块:放置未开启时,单独开启本项不生效 + if (isSpawnEggItem(hand.getType()) + && (e.getAction() == Action.RIGHT_CLICK_BLOCK || e.getAction() == Action.RIGHT_CLICK_AIR)) { + Block eggBlock = e.getClickedBlock(); + Location eggLoc = (eggBlock != null) ? eggBlock.getLocation() : p.getLocation(); + AreaConfig eggAc = getArea(p.getWorld().getName(), + eggLoc.getBlockX(), eggLoc.getBlockY(), eggLoc.getBlockZ()); + if (eggAc != null + && getEffectiveDeny(p, eggAc, "denyBlockPlace") + && getEffectiveDeny(p, eggAc, "denySpawnEgg")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋"); + return; + } + } + + // ===== 实体类物品放置:层级管控 ===== + // Level 1: denyBlockPlace(一级总开关)→ 禁止一切放置 + // Level 2: denyEntityInteract(二级实体交互)→ 禁止实体交互 + // Level 3: denyItemFrame(三级展示框)→ 仅限展示框具体控制 + Material mat = hand.getType(); + boolean isControlledEntity = + mat == Material.MINECART + || mat == Material.TNT_MINECART + || mat == Material.CHEST_MINECART + || mat == Material.HOPPER_MINECART + || mat == Material.OAK_BOAT + || mat == Material.SPRUCE_BOAT + || mat == Material.BIRCH_BOAT + || mat == Material.JUNGLE_BOAT + || mat == Material.ACACIA_BOAT + || mat == Material.DARK_OAK_BOAT + || mat == Material.MANGROVE_BOAT + || mat == Material.CHERRY_BOAT + || mat == Material.BAMBOO_RAFT + || mat == Material.ARMOR_STAND + || mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME; + if (isControlledEntity) { + // ★ 使用被点击方块定位,而非玩家脚下位置 + Block clicked = e.getClickedBlock(); + Location checkLoc = (clicked != null) ? clicked.getLocation() : p.getLocation(); + AreaConfig ac = getArea( + p.getWorld().getName(), + checkLoc.getBlockX(), + checkLoc.getBlockY(), + checkLoc.getBlockZ()); + if (ac != null) { + // Level 1: 方块放置总开关 + if (getEffectiveDeny(p, ac, "denyBlockPlace")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置方块或实体"); + return; + } + // Level 2: 实体交互 + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置实体"); + return; + } + // Level 3: 展示框单独控制 + boolean isFrame = (mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME); + if (isFrame && getEffectiveDeny(p, ac, "denyItemFrame")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止放置展示框"); + return; + } + } + // 实体物品不再往下走区域规则(防止误触发末影珍珠等检查) + return; + } + + + // ===== 容器/UI方块交互:以【被点击方块】定位领地 ===== + // 玩家站在领地外右键领地内容器/UI方块同样受控(旧逻辑用玩家脚下定位, + // 站领地外 ac==null 直接放行);双箱被点侧在领地外时补查相邻同材质半边 + if (e.getAction() == Action.RIGHT_CLICK_BLOCK) { + Block clicked = e.getClickedBlock(); + if (clicked != null && isUIBlock(clicked.getType())) { + // ★ 补刀:被点击方块定位 + 四向相邻同材质半边(双箱被点侧在领地外) + // + 上下方向(潜影盒叠放/箱子上下不合并但漏斗矿车场景仍按方块算) + // 任一侧命中领地且 denyContainer 即拦截。 + java.util.List cands = new java.util.ArrayList<>(); + AreaConfig cac = getArea(clicked.getWorld().getName(), + clicked.getX(), clicked.getY(), clicked.getZ()); + if (cac != null) cands.add(cac); + if (clicked.getType() == Material.CHEST + || clicked.getType() == Material.TRAPPED_CHEST) { + org.bukkit.block.BlockFace[] around = { + org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST, + org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH}; + for (org.bukkit.block.BlockFace bf : around) { + Block nb = clicked.getRelative(bf); + if (nb.getType() == clicked.getType()) { + AreaConfig na = getArea(nb.getWorld().getName(), + nb.getX(), nb.getY(), nb.getZ()); + if (na != null && !cands.contains(na)) cands.add(na); + } + } + } + // ★ 公共设施连控:公共设施启用时访客自动获得容器交互权限 + for (AreaConfig cand : cands) { + if (cand.isPublicBuilding) continue; + if (getEffectiveDeny(p, cand, "denyContainer")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); + return; + } + } + } + } + + // ===== 区域规则检查 ===== + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + + // 末影珍珠禁止(支持per-player权限) + if (getEffectiveDeny(p, ac, "denyEnderPearl") + && hand.getType() == Material.ENDER_PEARL) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); + return; + } + + // 弓箭禁止(支持per-player权限) + if (getEffectiveDeny(p, ac, "denyBow") + && (hand.getType() == Material.BOW + || hand.getType() == Material.CROSSBOW)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); + return; + } + + // 所有者+管理员跳过禁止使用物品检查 + if (hasPermission(p, ac, PermissionLevel.OWNER)) { + // 管理员和所有者不受限制 + } else { + // 禁止使用物品(访客级别检查) + String typeId = hand.getType().name(); + if (ac.denyUseItems.contains(typeId)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用此物品"); + return; + } + } + + // ★ 农作物收获:右键检测(南瓜/西瓜用剪刀收获) + if (ac.denyCropHarvest && e.getAction() == Action.RIGHT_CLICK_BLOCK) { + Block clicked = e.getClickedBlock(); + if (clicked != null) { + Material clickedMat = clicked.getType(); + if (clickedMat == Material.PUMPKIN || clickedMat == Material.MELON) { + if (getEffectiveDeny(p, ac, "denyCropHarvest")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止收获农作物"); + return; + } + } + } + } + + // ★ 容器交互扩展:检测非Container类型UI方块(工作台、砂轮、制图台、告示牌等) + if (ac.denyContainer && e.getAction() == Action.RIGHT_CLICK_BLOCK) { + Block clicked = e.getClickedBlock(); + if (clicked != null) { + Material clickedMat = clicked.getType(); + if (isUIBlock(clickedMat)) { + // ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(工作台/砂轮等UI方块同样放开) + if (ac.isPublicBuilding) return; + if (getEffectiveDeny(p, ac, "denyContainer")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); + return; + } + } + } + } + + // 没收检查 + handleConfiscate(p, ac); + } + + + @EventHandler + public void onExplosion(EntityExplodeEvent e) { + Iterator it = e.blockList().iterator(); + while (it.hasNext()) { + Block b = it.next(); + AreaConfig ac = getArea( + b.getWorld().getName(), + b.getX(), b.getY(), b.getZ()); + if (ac != null && ac.denyExplosion) + it.remove(); + } + } + + // ===== 火床/烈焰弹/三叉戟闪电也拦截 ===== + @EventHandler + public void onBlockExplode(org.bukkit.event.block.BlockExplodeEvent e) { + for (Iterator it = e.blockList().iterator(); it.hasNext();) { + Block b = it.next(); + AreaConfig ac = getArea(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()); + if (ac != null && ac.denyExplosion) it.remove(); + } + } + + @EventHandler + public void onEntityChangeBlock(org.bukkit.event.entity.EntityChangeBlockEvent e) { + if (e.getBlock().getType() == Material.FIRE || e.getBlock().getType() == Material.LAVA) { + AreaConfig ac = getArea(e.getBlock().getWorld().getName(), + e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ()); + if (ac != null && ac.denyFire) e.setCancelled(true); + } + // ★ 末影人行为控制:禁止末影人抱走方块 + if (e.getEntityType().name().equals("ENDERMAN")) { + AreaConfig ac = getArea(e.getBlock().getWorld().getName(), + e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ()); + if (ac != null && ac.denyEnderTeleport) { + e.setCancelled(true); + } + } + } + + // ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 ===== + @EventHandler + public void onIgniteBlock(BlockIgniteEvent e) { + Player p = e.getPlayer(); + Block tgtBlock = e.getBlock(); + + // 非玩家触发(如火蔓延、TNT引爆)-> 不拦截,由BlockBurnEvent处理 + if (p == null) return; + + AreaConfig srcArea = getArea(p.getWorld().getName(), + p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); + AreaConfig tgtArea = getArea(tgtBlock.getWorld().getName(), + tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); + + // 使用统一豁免 + boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); + boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); + if (srcDenied || tgtDenied) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 火蔓延防漏网(非玩家触发) ===== + @EventHandler + public void onFireSpreadNonPlayer(BlockIgniteEvent e) { + if (e.getPlayer() != null) return; + Block tgtBlock = e.getBlock(); + AreaConfig ac = getArea(tgtBlock.getWorld().getName(), + tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + } + } + + @EventHandler + public void onInventoryOpen(InventoryOpenEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + + InventoryHolder holder = e.getInventory().getHolder(); + java.util.List locs = new java.util.ArrayList<>(); + + // ★ 双箱特殊处理:DoubleChest 的 holder 不是 BlockState, + // 必须同时收集左右两侧位置——任一侧在领地内即受 denyContainer 管控。 + // 旧实现只反射取 left 且 getLeft() 返回 Inventory 无 getBlock(), + // 反射异常被吞后 loc=null 直接放行,导致大箱子完全绕过领地判定。 + if (e.getInventory() instanceof org.bukkit.inventory.DoubleChestInventory) { + org.bukkit.inventory.DoubleChestInventory dci = + (org.bukkit.inventory.DoubleChestInventory) e.getInventory(); + addChestSideLocation(locs, dci.getLeftSide()); + addChestSideLocation(locs, dci.getRightSide()); + } else if (holder instanceof org.bukkit.block.DoubleChest) { + org.bukkit.block.DoubleChest dc = (org.bukkit.block.DoubleChest) holder; + addChestSideLocation(locs, dc.getLeftSide()); + addChestSideLocation(locs, dc.getRightSide()); + } + // ★ 检测所有容器类型:箱子、熔炉、高炉、潜影盒、末影箱、讲台、铁砧、信标、漏斗、发射器、投掷器、酿造台、工作台等 + else if (holder instanceof BlockState) { + BlockState bs = (BlockState) holder; + locs.add(bs.getLocation()); + } else if (holder instanceof Container) { + // Container接口覆盖所有方块容器 + if (holder instanceof org.bukkit.block.Block) { + org.bukkit.block.Block block = (org.bukkit.block.Block) holder; + locs.add(block.getLocation()); + } + } + + // ★ Issue 9: 矿车容器(漏斗矿车、运输矿车) + if (holder != null && locs.isEmpty()) { + String className = holder.getClass().getSimpleName(); + if (className.contains("Minecart")) { + // 矿车容器使用实体位置 + if (holder instanceof org.bukkit.entity.Entity) { + org.bukkit.entity.Entity entity = (org.bukkit.entity.Entity) holder; + locs.add(entity.getLocation()); + } + } + } + + // ★ 补刀:holder 无法解析出位置(部分容器/插件自定义 holder)时, + // 回退用玩家视线命中的方块再定位一次,避免直接放行。 + if (locs.isEmpty()) { + try { + Block eye = p.getTargetBlockExact(6); + if (eye != null && isUIBlock(eye.getType())) { + locs.add(eye.getLocation()); + } + } catch (Throwable ignored) {} + } + + if (locs.isEmpty()) return; + + // ★ 跨边界大箱子:任一侧在领地内即判定;先取第一个命中领地做打开记录 + AreaConfig ac = null; + Location hitLoc = null; + for (Location loc : locs) { + if (loc == null || loc.getWorld() == null) continue; + AreaConfig a = getArea(loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (a != null) { ac = a; hitLoc = loc; break; } + } + if (ac == null || hitLoc == null) return; + + // ★ 领地记录(所有容器打开都记录,含领地主/管理员/公共设施) + String containerType; + if (hitLoc.getBlock().getType() != org.bukkit.Material.AIR) { + containerType = hitLoc.getBlock().getType().name(); + } else { + containerType = holder.getClass().getSimpleName(); + } + plugin.landRecordManager.recordContainerOpen(ac, p, hitLoc, containerType, e.getInventory()); + + // ★ 逐侧判定:任一侧所在领地禁止容器访问即拦截(跨边界大箱子不再漏网) + // ★ 补刀:某侧查不到领地时,回查四向同材质半边(被开侧在领地外、 + // 另一半在领地内)与视线命中方块,避免站领地外开领地内箱子漏网。 + for (Location loc : locs) { + if (loc == null || loc.getWorld() == null) continue; + AreaConfig a = getArea(loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (a == null) { + Block blk = loc.getBlock(); + if (blk.getType() == Material.CHEST || blk.getType() == Material.TRAPPED_CHEST) { + org.bukkit.block.BlockFace[] around = { + org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST, + org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH}; + for (org.bukkit.block.BlockFace bf : around) { + Block nb = blk.getRelative(bf); + if (nb.getType() != blk.getType()) continue; + a = getArea(nb.getWorld().getName(), + nb.getX(), nb.getY(), nb.getZ()); + if (a != null) break; + } + } + } + if (a == null) continue; + if (hasPermission(p, a, PermissionLevel.OWNER)) continue; + + // ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查) + if (a.isPublicBuilding) continue; + + // ★ 容器管理权限:denyContainer控制访客能否访问容器 + // getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免, + // 对无权限玩家则走领地默认 denyContainer 配置 + if (getEffectiveDeny(p, a, "denyContainer")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器"); + return; + } + } + } + + // ★ 双箱侧边定位:从半边(Inventory 或 InventoryHolder)的 BlockState holder 取其方块位置 + private void addChestSideLocation(java.util.List locs, Object side) { + if (side == null) return; + Object sh = side; + if (side instanceof org.bukkit.inventory.Inventory) { + sh = ((org.bukkit.inventory.Inventory) side).getHolder(); + } + // ★ 补刀:半边 holder 仍可能是 DoubleChest(跨边界双箱的其中一侧),递归展开 + if (sh instanceof org.bukkit.block.DoubleChest) { + org.bukkit.block.DoubleChest dc2 = (org.bukkit.block.DoubleChest) sh; + addChestSideLocation(locs, dc2.getLeftSide()); + addChestSideLocation(locs, dc2.getRightSide()); + return; + } + if (sh instanceof BlockState) { + Location l = ((BlockState) sh).getLocation(); + if (l != null) locs.add(l); + } else if (sh instanceof org.bukkit.block.Block) { + locs.add(((org.bukkit.block.Block) sh).getLocation()); + } else if (sh instanceof Container) { + // ★ 补刀:非 BlockState 的 Container(如矿车容器被包装)取其位置 + Location l = ((Container) sh).getLocation(); + if (l != null) locs.add(l); + } + } + + /** + * ★ 白名单遗留小写条目 → DB 精确拼写迁移(2026-10-10 精确匹配改造配套)。 + * area_land_permissions.player_name 保存授予时的精确拼写(权威), + * 用它把旧版小写落盘的白名单条目还原为规范名;多种拼写并存(多义)时放弃迁移。 + * @param landName 领地名;null 表示查全表(全局白名单用) + * @return 规范名;无匹配/无法确定时返回 null + */ + private String canonicalNameFromDb(String landName, String entry) { + if (dbConnection == null || entry == null || entry.isEmpty()) return null; + String want = normName(entry); + String found = null; + try { + PreparedStatement stmt; + if (landName == null) { + stmt = dbConnection.prepareStatement( + "SELECT DISTINCT player_name FROM area_land_permissions"); + } else { + int landId = getLandIdFromDb(landName); + if (landId <= 0) return null; + stmt = dbConnection.prepareStatement( + "SELECT DISTINCT player_name FROM area_land_permissions WHERE land_id = ?"); + stmt.setInt(1, landId); + } + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + String n = rs.getString("player_name"); + if (n == null || n.isEmpty() || !want.equals(normName(n))) continue; + if (n.equals(entry)) { found = entry; break; } // 已是规范拼写 + if (found != null && !found.equals(n)) { found = null; break; } // 多义,放弃 + found = n; + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + return null; + } + return found; + } + + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + boolean whitelistMigrated = false; + + if (!whitelistDir.exists()) { + whitelistDir.mkdirs(); + return; + } + + try { + // 加载全局白名单 + if (globalWhitelistFile.exists()) { + List lines = + java.nio.file.Files.readAllLines( + globalWhitelistFile.toPath(), + StandardCharsets.UTF_8); + for (String line : lines) { + String name = line.trim(); + if (!name.isEmpty() + && !name.startsWith("#")) { + // ★ 遗留小写条目 → DB 精确拼写迁移(2026-10-10 精确匹配改造配套) + String fixed = canonicalNameFromDb(null, name); + if (fixed != null && !fixed.equals(name)) { + whitelistMigrated = true; + name = fixed; + } + globalPlayerWhitelist.add(name); + } + } + } + + // 加载各区域白名单 + File[] files = whitelistDir.listFiles( + (File d, String n) -> n.endsWith(".txt") + && !n.equals("全局白名单.txt")); + if (files != null) { + for (File f : files) { + String areaName = f.getName() + .replace(".txt", ""); + Set set = + ConcurrentHashMap.newKeySet(); + List lines = + java.nio.file.Files.readAllLines( + f.toPath(), + StandardCharsets.UTF_8); + for (String line : lines) { + String name = line.trim(); + if (!name.isEmpty() + && !name.startsWith("#")) { + // ★ 精确存储原始拼写(2026-10-10):离线模式大小写不同=不同玩家。 + // 旧版统一小写落盘会让真实大小写拼写的玩家无法精确命中; + // 遗留小写条目用 area_land_permissions 里的精确拼写做规范名迁移。 + String fixed = canonicalNameFromDb(areaName, name); + if (fixed != null && !fixed.equals(name)) { + whitelistMigrated = true; + name = fixed; + } + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + if (whitelistMigrated) { + saveWhitelists(); + plugin.getLogger().info("[防护] 白名单遗留小写条目已按DB精确拼写迁移回写"); + } + plugin.getLogger().info( + "[防护] 白名单加载完成,全局: " + + globalPlayerWhitelist.size() + + ",区域数: " + + areaPlayerWhitelist.size()); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载白名单失败: " + + e.getMessage()); + } + } + + + @EventHandler + public void onPotionSplash(PotionSplashEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + AreaConfig ac = getArea( + shooter.getWorld().getName(), + shooter.getLocation().getBlockX(), + shooter.getLocation().getBlockY(), + shooter.getLocation().getBlockZ()); + if (ac != null && getEffectiveDeny(shooter, ac, "denyPotion")) { + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止使用药水"); + } + } + + // ===== ★ 投掷物检测(三叉戟、雪球、风蛋、箭)===== + @EventHandler + public void onProjectileLaunch(ProjectileLaunchEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + Entity proj = e.getEntity(); + AreaConfig ac = getArea( + shooter.getWorld().getName(), + shooter.getLocation().getBlockX(), + shooter.getLocation().getBlockY(), + shooter.getLocation().getBlockZ()); + if (ac == null) return; + if (!getEffectiveDeny(shooter, ac, "denyThrownProjectiles")) return; + // 检查投掷物类型:箭、三叉戟、雪球、风蛋、龙息弹等 + String typeName = proj.getType().name(); + if (typeName.contains("ARROW") || typeName.contains("TRIDENT") + || typeName.contains("SNOWBALL") || typeName.contains("WIND_CHARGE") + || typeName.contains("DRAGON_FIREBALL") || typeName.contains("FIREBALL") + || typeName.contains("SHULKER_BULLET") || typeName.contains("LLAMA_SPIT")) { + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止在此区域投掷物品"); + } + } + + // ★ 门禁交互检测(已移除按钮/压力板,按钮/压力板由onRedstoneInteract处理) + + // ★ 门禁交互检测(门/栅栏门) + @EventHandler + public void onDoorOpen(PlayerInteractEvent e) { + if (e.getClickedBlock() == null) return; + if (e.getAction() != Action.RIGHT_CLICK_BLOCK) return; + Player p = e.getPlayer(); + Material mat = e.getClickedBlock().getType(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getClickedBlock().getX(), + e.getClickedBlock().getY(), + e.getClickedBlock().getZ()); + if (ac == null) return; + if (!getEffectiveDeny(p, ac, "denyDoorInteraction")) return; + String name = mat.name(); + if (name.contains("DOOR") || name.contains("GATE") || name.contains("FENCE_GATE")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止操作门/栅栏门"); + } + } + + // ★ 红石交互检测(按钮、压力板、中继器、比较器、拉杆、阳光传感器) + @EventHandler + public void onRedstoneInteract(PlayerInteractEvent e) { + if (e.getClickedBlock() == null) return; + Player p = e.getPlayer(); + Material mat = e.getClickedBlock().getType(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getClickedBlock().getX(), + e.getClickedBlock().getY(), + e.getClickedBlock().getZ()); + if (ac == null) return; + // 权限检查:如果允许操作,记录状态变化并返回 + if (!getEffectiveDeny(p, ac, "denyRedstoneInteraction")) { + // 记录红石元器件状态变化 + if (isRedstoneComponent(mat)) { + plugin.landRecordManager.recordRedstoneStateChange(ac, p, e.getClickedBlock()); + } + return; + } + // 权限禁止:检查是否为红石元器件并取消 + if (isRedstoneComponent(mat)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止操作红石元件"); + } + } + + /** 判断材质是否为红石元器件(压力板、按钮、拉杆、中继器、比较器等) */ + private boolean isRedstoneComponent(Material mat) { + String name = mat.name(); + return mat == Material.REPEATER || mat == Material.COMPARATOR + || mat == Material.LEVER + || mat == Material.STONE_BUTTON || mat == Material.OAK_BUTTON + || mat == Material.SPRUCE_BUTTON || mat == Material.BIRCH_BUTTON + || mat == Material.JUNGLE_BUTTON || mat == Material.ACACIA_BUTTON + || mat == Material.DARK_OAK_BUTTON || mat == Material.MANGROVE_BUTTON + || mat == Material.CHERRY_BUTTON || mat == Material.BAMBOO_BUTTON + || mat == Material.CRIMSON_BUTTON || mat == Material.WARPED_BUTTON + || mat == Material.POLISHED_BLACKSTONE_BUTTON + || mat == Material.STONE_PRESSURE_PLATE || mat == Material.OAK_PRESSURE_PLATE + || mat == Material.SPRUCE_PRESSURE_PLATE || mat == Material.BIRCH_PRESSURE_PLATE + || mat == Material.JUNGLE_PRESSURE_PLATE || mat == Material.ACACIA_PRESSURE_PLATE + || mat == Material.DARK_OAK_PRESSURE_PLATE || mat == Material.MANGROVE_PRESSURE_PLATE + || mat == Material.CHERRY_PRESSURE_PLATE || mat == Material.BAMBOO_PRESSURE_PLATE + || mat == Material.CRIMSON_PRESSURE_PLATE || mat == Material.WARPED_PRESSURE_PLATE + || mat == Material.POLISHED_BLACKSTONE_PRESSURE_PLATE || mat == Material.LIGHT_WEIGHTED_PRESSURE_PLATE + || mat == Material.HEAVY_WEIGHTED_PRESSURE_PLATE + || mat == Material.DAYLIGHT_DETECTOR + || name.contains("REPEATER") || name.contains("COMPARATOR") + || name.contains("BUTTON") || name.contains("PRESSURE_PLATE") + || name.contains("DAYLIGHT"); + } + + // ★ 音频交互检测(音符盒、唱片机) + @EventHandler + public void onNoteblockInteract(PlayerInteractEvent e) { + if (e.getClickedBlock() == null) return; + Player p = e.getPlayer(); + Material mat = e.getClickedBlock().getType(); + AreaConfig ac = getArea( + p.getWorld().getName(), + e.getClickedBlock().getX(), + e.getClickedBlock().getY(), + e.getClickedBlock().getZ()); + if (ac == null) return; + if (!getEffectiveDeny(p, ac, "denyNoteblockJukebox")) return; + if (mat == Material.NOTE_BLOCK || mat.name().contains("JUKEBOX") + || mat.name().contains("CHERRY_JUKEBOX")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止操作音符盒/唱片机"); + } + } + + // ★ 农作物自然生长不受 denyCropHarvest 影响。 + // denyCropHarvest 只拦截"种植/收获"交互(BlockPlace / BlockBreak / 右键瓜果), + // 此处曾误取消 BlockGrowEvent,导致领地一旦开启"禁用农作物收获"庄稼就完全不长。 + + // ★ 耕地破坏(玩家在耕地上跳跃/掉落时触发) + @EventHandler + public void onFarmlandTrample(PlayerInteractEvent e) { + if (e.getAction() != Action.PHYSICAL) return; + Block block = e.getClickedBlock(); + if (block == null || block.getType() != Material.FARMLAND) return; + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFarmlandTrample) { + e.setCancelled(true); + } + } + + // ★ 末影人行为控制(禁止末影人传送进入领地、在领地内传送、自然进入领地) + @EventHandler + public void onEnderTeleport(EntityTeleportEvent e) { + if (!e.getEntityType().name().equals("ENDERMAN")) return; + Location from = e.getFrom(); + Location to = e.getTo(); + // 检查起点是否在禁止末影人传送的领地内 + if (from != null) { + AreaConfig fromAc = getArea(from.getWorld().getName(), + from.getBlockX(), from.getBlockY(), from.getBlockZ()); + if (fromAc != null && fromAc.denyEnderTeleport) { + e.setCancelled(true); + return; + } + } + // 检查终点是否在禁止末影人传送的领地内 + if (to != null) { + AreaConfig toAc = getArea(to.getWorld().getName(), + to.getBlockX(), to.getBlockY(), to.getBlockZ()); + if (toAc != null && toAc.denyEnderTeleport) { + e.setCancelled(true); + return; + } + } + // 如果起点和终点都不在禁止传送的领地内,不干预(让MC自身逻辑处理) + // 注意:如果起点在允许传送的领地内,终点在禁止传送的领地内,已在上面处理 + // 如果起点在禁止传送的领地内,终点在允许传送的领地内,已在上面处理 + } + + // ★ 末影人行为控制(禁止末影人AI寻路进入领地) + @EventHandler + public void onEndermanPathfind(EntityPathfindEvent e) { + if (!e.getEntity().getType().name().equals("ENDERMAN")) return; + Location loc = e.getLoc(); + if (loc == null) return; + AreaConfig ac = getArea(loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (ac != null && ac.denyEnderTeleport) { + e.setCancelled(true); + } + } + + // ★ 末影人生成控制(自然生成时禁止进入领地) + @EventHandler + public void onEndermanSpawn(CreatureSpawnEvent e) { + if (!e.getEntityType().name().equals("ENDERMAN")) return; + if (e.getSpawnReason() != CreatureSpawnEvent.SpawnReason.NATURAL) return; + Location loc = e.getLocation(); + AreaConfig ac = getArea(loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (ac != null && ac.denyEnderTeleport) { + e.setCancelled(true); + } + } + + // ★ 判断方块是否为农作物 + private boolean isCropBlock(Material mat) { + String name = mat.name(); + return name.contains("WHEAT") || name.contains("CARROT") + || name.contains("POTATO") || name.contains("BEETROOT") + || name.contains("MELON") || name.contains("PUMPKIN") + || name.contains("COCOA") || name.contains("CROP") + || name.contains("SWEET_BERRY") || name.contains("BAMBOO") + || name.contains("SUGAR_CANE") || name.contains("NETHER_WART") + || name.contains("KELP") || name.contains("SEA_PICKLE") + || name.contains("CHORUS") || name.contains("TWISTING_VINES") + || name.contains("WEEPING_VINES") || name.contains("PITCHER") + || name.contains("TORCHFLOWER") + || mat == Material.MELON_STEM + || mat == Material.ATTACHED_MELON_STEM + || mat == Material.PUMPKIN_STEM + || mat == Material.ATTACHED_PUMPKIN_STEM; + } + + // ★ 判断方块是否为UI交互方块(非Container类型,需要单独检测) + private boolean isUIBlock(Material mat) { + return mat == Material.CRAFTING_TABLE || mat == Material.STONECUTTER + || mat == Material.GRINDSTONE || mat == Material.ANVIL + || mat == Material.CHIPPED_ANVIL || mat == Material.DAMAGED_ANVIL + || mat == Material.CARTOGRAPHY_TABLE || mat == Material.LOOM + || mat == Material.SMITHING_TABLE || mat == Material.ENCHANTING_TABLE + || mat == Material.BREWING_STAND + || mat.name().contains("SIGN") || mat.name().contains("BANNER") + || mat == Material.BARREL || mat.name().contains("SHULKER_BOX") + // ★ 补刀:漏掉的容器/UI方块(此前这些方块在领地外右键不受 denyContainer 管控) + || mat == Material.CRAFTER || mat.name().contains("BOOKSHELF") + || mat == Material.COMPOSTER || mat.name().contains("CAULDRON") + || mat == Material.ENDER_CHEST || mat.name().contains("CHEST") + || mat.name().contains("FURNACE") || mat.name().contains("BLAST_FURNACE") + || mat.name().contains("SMOKER") || mat == Material.HOPPER + || mat.name().contains("DISPENSER") || mat.name().contains("DROPPER") + || mat == Material.BEACON || mat.name().contains("LECTERN") + // ★ 展示架/展示柜(1.21.5+新方块):SHELF是MC 1.21.5 Spring Drop加入的 + || mat.name().contains("SHELF") || mat.name().contains("DISPLAY") + || mat.name().contains("SHOWCASE") || mat.name().contains("DECORATED_POT"); + } + + // ★ 判断物品是否为可种植的农作物种子(用于BlockPlaceEvent分层权限) + private boolean isCropSeed(Material mat) { + String name = mat.name(); + return name.contains("SEEDS") || name.contains("SAPLING") + || name.contains("STEM") || name.contains("PICKLE") + || mat == Material.BAMBOO || mat == Material.SUGAR_CANE + || mat == Material.CACTUS || mat == Material.COCOA_BEANS + || mat == Material.NETHER_WART || mat == Material.KELP + || mat == Material.TWISTING_VINES || mat == Material.WEEPING_VINES + || mat == Material.CHORUS_FLOWER || mat == Material.SWEET_BERRY_BUSH + || mat == Material.CARROT || mat == Material.POTATO + || mat == Material.BEETROOT_SEEDS || mat == Material.WHEAT_SEEDS + || mat == Material.PITCHER_POD || mat == Material.TORCHFLOWER_SEEDS; + } + + // ★ 检查是否为动物食物(包括玩家食物+动物专用食物) + private boolean isAnimalFood(Material mat) { + if (mat.isEdible()) return true; // 玩家食物(面包、胡萝卜等) + // 动物专用食物(玩家不可食用) + return mat == Material.WHEAT || mat == Material.BEETROOT_SEEDS + || mat == Material.MELON_SEEDS || mat == Material.PUMPKIN_SEEDS + || mat == Material.COCOA_BEANS || mat == Material.BONE_MEAL + || mat == Material.NAME_TAG || mat == Material.SADDLE + || mat.name().contains("SEEDS"); // 各种种子 + } + + // ★ 采集羊毛检测 + @EventHandler + public void onShearEntity(PlayerShearEntityEvent e) { + Player p = e.getPlayer(); + // 检查羊的位置 + Location loc = e.getEntity().getLocation(); + AreaConfig ac = getArea( + loc.getWorld().getName(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ()); + if (ac == null) return; + if (!getEffectiveDeny(p, ac, "denyWoolShear")) return; + String entityType = e.getEntity().getType().name(); + if (entityType.contains("SHEEP") || entityType.contains("MOoshroom")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止在此区域剪毛"); + } + } + + // ★ 投喂动物检测 + @EventHandler + public void onEntityInteract(PlayerInteractEntityEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = e.getPlayer(); + Entity entity = e.getRightClicked(); + + // ★ 蜜脾涂蜡实体(铜傀儡等):受涂蜡权限管控(与其他 deny* 对齐,仅约束普通访客) + ItemStack waxHand = p.getInventory().getItemInMainHand(); + if (waxHand != null && waxHand.getType() == Material.HONEYCOMB) { + Location waxLoc = entity.getLocation(); + AreaConfig waxAc = getArea(waxLoc.getWorld().getName(), + waxLoc.getBlockX(), waxLoc.getBlockY(), waxLoc.getBlockZ()); + if (isWaxDenied(p, waxAc)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止涂蜡"); + return; + } + } + // ★ 村民交易权限(denyVillagerTrade):右键村民直接拦截,交易界面不会打开(2026-10-10) + if (entity instanceof org.bukkit.entity.Villager) { + Location vLoc = entity.getLocation(); + AreaConfig vAc = getArea(vLoc.getWorld().getName(), + vLoc.getBlockX(), vLoc.getBlockY(), vLoc.getBlockZ()); + if (vAc != null && getEffectiveDeny(p, vAc, "denyVillagerTrade")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止与此领地的村民交易"); + return; + } + } + // 只检测动物类 + String typeName = entity.getType().name(); + if (!(entity instanceof Animals || typeName.contains("IRON_GOLEM") + || typeName.contains("WOLF") || typeName.contains("CAT") + || typeName.contains("PARROT") || typeName.contains("FROG"))) { + return; + } + Location loc = entity.getLocation(); + AreaConfig ac = getArea( + loc.getWorld().getName(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ()); + if (ac == null) return; + if (!getEffectiveDeny(p, ac, "denyAnimalFeeding")) return; + // 检查玩家手持的物品是否是食物(isEdible()只检测玩家食物,需额外检测动物食物) + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand != null && hand.getType() != Material.AIR && isAnimalFood(hand.getType())) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止在此区域投喂动物"); + } + } + + // ★ 村民交易兜底拦截(2026-10-10):交易界面已打开后(进服前打开 / 权限中途切换), + // 点击交易结果槽仍受 denyVillagerTrade 管控;正常路径由 onEntityInteract 右键拦截。 + @EventHandler + public void onVillagerTradeClick(org.bukkit.event.inventory.InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) return; + org.bukkit.inventory.Inventory topInv = e.getView().getTopInventory(); + if (topInv == null + || !(topInv.getHolder() instanceof org.bukkit.entity.Villager)) return; + Player p = (Player) e.getWhoClicked(); + org.bukkit.entity.Entity holder = (org.bukkit.entity.Entity) topInv.getHolder(); + Location loc = holder.getLocation(); + AreaConfig ac = getArea(loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (ac != null && getEffectiveDeny(p, ac, "denyVillagerTrade")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止与此领地的村民交易"); + } + } + + // ★ 拴绳使用检测 + @EventHandler + public void onLeadUse(PlayerLeashEntityEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = e.getPlayer(); + Entity entity = e.getEntity(); + Location loc = entity.getLocation(); + AreaConfig ac = getArea( + loc.getWorld().getName(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ()); + if (ac == null) return; + if (!getEffectiveDeny(p, ac, "denyLead")) return; + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止在此区域使用拴绳"); + } + + /** + * 解析区域名:先精准匹配,再模糊匹配 + * @param input 用户输入的区域名 + * @return 匹配到的区域名,无匹配返回null + */ + public String resolveAreaName(String input) { + if (input == null || input.isEmpty()) return null; + if (input.equalsIgnoreCase("global")) return "global"; + + // 第一步:精准匹配 + for (String name : areas.keySet()) { + if (name.equalsIgnoreCase(input)) { + return name; + } + } + + // 第二步:包含匹配(用户只打了部分名字) + for (String name : areas.keySet()) { + if (name.contains(input) || input.contains(name)) { + return name; + } + } + + // 第三步:编辑距离模糊匹配(著称→主城) + String bestMatch = null; + int bestDistance = Integer.MAX_VALUE; + for (String name : areas.keySet()) { + int dist = levenshteinDistance( + input.toLowerCase(), name.toLowerCase()); + if (dist < bestDistance) { + bestDistance = dist; + bestMatch = name; + } + } + + // 容错阈值:编辑距离 <= 名字长度的一半 + if (bestMatch != null) { + int threshold = Math.max(1, bestMatch.length() / 2); + if (bestDistance <= threshold) { + return bestMatch; + } + } + + return null; + } + + /** + * ★ 严格解析领地名(写操作/权限敏感操作专用) + * 只做"完全一致"匹配:先区分大小写,再忽略大小写; + * 不做包含匹配,不做编辑距离模糊匹配。 + * 目的:防止攻击者用大小写变体、部分字符串或特殊字符 + * 把输入"归一化"成他人领地名后越权操作。 + */ + public String resolveAreaNameStrict(String input) { + if (input == null || input.isEmpty()) return null; + if (input.equalsIgnoreCase("global")) return "global"; + if (areas.containsKey(input)) return input; + for (String name : areas.keySet()) { + if (name.equalsIgnoreCase(input)) return name; + } + return null; + } + + /** + * ★ 大小写不敏感的领地重名检查(创建/改名防重) + */ + public boolean areaNameExists(String name) { + if (name == null || name.isEmpty()) return false; + for (String k : areas.keySet()) { + if (k.equalsIgnoreCase(name)) return true; + } + return false; + } + + /** + * ★ 领地名格式校验(创建/改名) + * 只允许中英文、数字、下划线、连字符,1-20 字符。 + * 拒绝空格、颜色代码、斜杠、点号等特殊字符——领地名会被 + * 直接当作白名单/配置文件名落盘,特殊字符会造成文件互相 + * 覆盖或路径穿越(Windows 下文件名不区分大小写,A/a 会互覆盖)。 + * @return null=合法,非 null=错误提示 + */ + public static String validateAreaName(String name) { + if (name == null || name.trim().isEmpty()) { + return "名字不能为空"; + } + String n = name.trim(); + if (n.length() > 20) return "名字不能超过20个字符"; + if (!n.matches("^[A-Za-z0-9一-龥_-]{1,20}$")) { + return "名字只能包含中英文、数字、下划线和连字符"; + } + if (n.equalsIgnoreCase("global")) { + return "global 是保留名,请换一个"; + } + return null; + } + + /** + * ★ 写操作权限闸门:领地所有者 / 领地管理员 / 插件管理员 / 控制台 + * 凡是"解析出领地名后直接改数据"的子命令,都必须先过这道闸。 + */ + private boolean requireLandOwnership(CommandSender sender, String areaName) { + if (areaName == null || areaName.equalsIgnoreCase("global")) { + return requireGlobalAdmin(sender, "全服级(global)操作"); + } + AreaConfig ac = areas.get(areaName); + if (ac == null) { + sender.sendMessage("§c区域不存在: " + areaName); + return false; + } + if (!(sender instanceof Player)) return true; // 控制台=管理员 + if (!hasPermission((Player) sender, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限: " + areaName); + return false; + } + return true; + } + + /** + * ★ 全服级(global)写操作闸门:仅插件管理员 / 控制台 + */ + private boolean requireGlobalAdmin(CommandSender sender, String what) { + if (isAreaAdmin(sender)) return true; + sender.sendMessage("§c" + what + " 仅管理员可用"); + return false; + } + + /** + * 判断字符串是否是区域名(精准或模糊) + */ + private boolean isAreaName(String name) { + if (name.equalsIgnoreCase("global")) return false; + return resolveAreaName(name) != null; + } + + /** + * 获取模糊匹配的实际区域名(用于提示用户) + */ + private String getResolvedName(String input) { + return resolveAreaName(input); + } + + /** + * 编辑距离算法(莱文斯坦距离) + */ + private int levenshteinDistance(String s1, String s2) { + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) dp[i][0] = i; + for (int j = 0; j <= s2.length(); j++) dp[0][j] = j; + for (int i = 1; i <= s1.length(); i++) { + for (int j = 1; j <= s2.length(); j++) { + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], + Math.min(dp[i - 1][j], dp[i][j - 1])); + } + } + } + return dp[s1.length()][s2.length()]; + } + + /** + * 智能重排参数:找出子命令位置,重排为标准顺序 + * 标准顺序:/protect 子命令 参数1 参数2 ... + */ + private String[] smartReorderArgs(String[] original) { + if (original.length <= 1) return original; + + Set knownSubs = new HashSet<>(Arrays.asList( + "add", "remove", "additem", "removeitem", + "addname", "removename", "listname", + "addwhite", "removewhite", "listwhite", + "list", "listitem", "创建", "create", "删除", "delete", "重载", "reload", + "工具", "wand", "expand", "contraction", + "on", "off", "tempon", "modeexempt", + "settp", "setwarp", "tp", "warp", + "setowner", "setadmin", "unsetadmin", "addvisitor", "removevisitor", "listvisitors", "transfer", "transfeland", + "info", "setowner", "addvisitor", "removevisitor", + "listvisitors", "transfer", "shop", + "menu", "菜单", "sdf1debug", "testclear", "cli", + "取消删除", "canceldelete", "canceladminchange", + "removemember", "addmember", + "config", "配置", + "public", "公共", "listpublic", "公共列表", "tpb", "传送公共", + "group", "用户组", "groupadd", "groupdel", "grouplist", "groupset", "groupedit", "groupdelconfig", "groupmembers", + "confirm_create", "accept", "deny" + )); + + String first = original[0].toLowerCase(); + if (knownSubs.contains(first)) { + return original; + } + + int subIndex = -1; + for (int i = 0; i < original.length; i++) { + if (knownSubs.contains(original[i].toLowerCase())) { + subIndex = i; + break; + } + } + + if (subIndex < 0) { + return original; + } + + String[] reordered = new String[original.length]; + reordered[0] = original[subIndex]; + int idx = 1; + for (int i = 0; i < original.length; i++) { + if (i == subIndex) continue; + reordered[idx] = original[i]; + idx++; + } + + return reordered; + } + + // ==================== 管理命令 ==================== + public boolean handleCommand(CommandSender sender, + String[] args) { + args = smartReorderArgs(args); + if (args.length == 0) { + // ★ 读取用户偏好决定打开GUI还是CLI + if (sender instanceof Player) { + Player p = (Player) sender; + int uiMode = 1; + try { + uiMode = plugin.getDb() + .getUiMode(p.getName()); + } catch (Exception ignored) {} + if (uiMode == 0) { + // GUI模式 → 打开领地管理GUI + if (plugin.areaGUIManager != null) { + plugin.areaGUIManager.openMainMenu(p); + } else { + p.sendMessage("§cGUI管理器未初始化,显示文本帮助"); + showHelp(sender); + } + return true; + } + } + // CLI模式 → 显示交互式菜单 + if (sender instanceof Player) { + Player p = (Player) sender; + if (plugin.areaCLIManager != null) { + plugin.areaCLIManager.showMainMenu(p); + return true; + } + } + showHelp(sender); + return true; + } + String sub = args[0].toLowerCase(); + + // ★ 管理员操作时,自动分配无owner的领地 + if (sender instanceof Player && isAreaAdmin(sender)) { + assignUnownedLandsToAdmin((Player) sender); + } +// ===== 诊断命令 ===== + if (sub.equals("sdf1debug")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + UUID uid = p.getUniqueId(); + + sender.sendMessage("§e===== 调试信息 ====="); + sender.sendMessage("§7当前区域: " + + playerCurrentArea.get(uid)); + sender.sendMessage("§7标记效果数: " + + (playerMarkedEffects.containsKey(uid) + ? playerMarkedEffects.get(uid).size() + : "无标记")); + sender.sendMessage("§7待清理任务: " + + pendingClearTask.containsKey(uid)); + sender.sendMessage("§7区域数: " + areas.size()); + + if (playerMarkedEffects.containsKey(uid)) { + List marked = + playerMarkedEffects.get(uid); + for (PotionEffectType type : marked) { + sender.sendMessage("§7 标记: " + + type.getName() + + " 身上有=" + + (p.getPotionEffect(type) != null)); + } + } + return true; + } + +// ===== 强制清理命令(测试用)===== + if (sub.equals("testclear")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + UUID uid = p.getUniqueId(); + + List marked = + playerMarkedEffects.remove(uid); + if (marked != null) { + for (PotionEffectType type : marked) { + p.removePotionEffect(type); + } + sender.sendMessage("§a已强制清理 " + + marked.size() + " 个效果"); + } else { + sender.sendMessage("§c无标记效果可清理"); + } + + // 取消延时任务 + if (pendingClearTask.containsKey(uid)) { + pendingClearTask.get(uid).cancel(); + pendingClearTask.remove(uid); + pendingClearArea.remove(uid); + pendingClearEffects.remove(uid); + sender.sendMessage("§a已取消延时任务"); + } + + removePlayerEffects(uid, + playerCurrentArea.getOrDefault(uid, "")); + sender.sendMessage("§aDB已清理"); + return true; + } + + // ===== 工具 ===== + if (sub.equals("工具") || sub.equals("wand")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + // ★ 5秒冷却 + long now = System.currentTimeMillis(); + Long lastGive = wandCooldownMap.get(p.getUniqueId()); + if (lastGive != null && now - lastGive < 5000) { + long remaining = (5000 - (now - lastGive)) / 1000 + 1; + p.sendMessage("§c§l[防护] §f请等待 " + remaining + " 秒后再获取"); + return true; + } + // ★ 检查是否已拥有 + for (ItemStack item : p.getInventory().getContents()) { + if (item != null && item.getType() == WAND + && item.hasItemMeta() && item.getItemMeta() != null + && item.getItemMeta().hasDisplayName() + && item.getItemMeta().getDisplayName().contains("区域选择工具")) { + p.sendMessage("§e§l[防护] §f你已拥有圈地工具"); + wandCooldownMap.put(p.getUniqueId(), now); + return true; + } + } + // ★ SN 管控:1人同种类仅限1个 + if (plugin.getSnManager() != null + && plugin.getSnManager().isBlocked( + p, SnManager.TYPE_WAND)) { + long cdMin = plugin.getSnManager().cooldownMinutesLeft(p.getName()); + if (cdMin > 0) { + p.sendMessage("§c§l[防护] §f注销冷静期未结束,还剩约 §e" + cdMin + " §f分钟,期间无法申领区域选择工具"); + } else { + p.sendMessage("§c§l[防护] §f你已有区域选择工具的有效登记," + + "旧SN未注销前不能重新申领"); + } + wandCooldownMap.put(p.getUniqueId(), now); + return true; + } + ItemStack tool = new ItemStack(WAND); + ItemMeta meta = tool.getItemMeta(); + meta.setDisplayName("§a§l区域选择工具"); + meta.setLore(Arrays.asList( + "§7左键: 位置1", "§7右键: 位置2")); + tool.setItemMeta(meta); + if (plugin.getSnManager() != null) { + String sn = plugin.getSnManager().applySn( + p, SnManager.TYPE_WAND, "区域选择工具"); + if (sn == null) { + wandCooldownMap.put(p.getUniqueId(), now); + return true; + } + plugin.getSnManager().writeSn( + tool, sn, SnManager.TYPE_WAND); + } + p.getInventory().addItem(tool); + p.sendMessage("§a§l[防护] §f已获取选择工具"); + wandCooldownMap.put(p.getUniqueId(), now); + return true; + } + + // ===== GUI菜单 ===== + if (sub.equals("menu") || sub.equals("菜单")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (plugin.areaGUIManager != null) { + plugin.areaGUIManager.openMainMenu(p); + } else { + p.sendMessage("§cGUI管理器未初始化"); + } + return true; + } + + // ===== 回声碎片(领地记录查询工具)===== + if (sub.equals("echoshard") || sub.equals("回声碎片")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (plugin.landRecordManager != null) { + plugin.landRecordManager.giveEchoShard(p); + } else { + p.sendMessage("§c领地记录管理器未初始化"); + } + return true; + } + + // ===== UI模式偏好 ===== + if (sub.equals("uimode") || sub.equals("偏好")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + int current = plugin.getDb() + .getUiMode(p.getName()); + p.sendMessage("§e当前模式: §f" + + (current == 0 ? "GUI" : "CLI")); + p.sendMessage("§7用法: /protect uimode "); + return true; + } + String mode = args[1].toLowerCase(); + if (mode.equals("gui") || mode.equals("0")) { + plugin.getDb().setUiMode(p.getName(), 0); + p.sendMessage("§a已切换为 §6GUI §a模式"); + // ★ 立即打开GUI + if (plugin.areaGUIManager != null) { + plugin.areaGUIManager.openMainMenu(p); + } + } else if (mode.equals("cli") || mode.equals("1")) { + plugin.getDb().setUiMode(p.getName(), 1); + p.sendMessage("§a已切换为 §6CLI §a模式"); + // ★ 立即显示CLI菜单 + if (plugin.areaCLIManager != null) { + plugin.areaCLIManager.showMainMenu(p); + } + } else { + p.sendMessage("§c用法: /protect uimode "); + } + return true; + } + + // ===== CLI交互式菜单 ===== + if (sub.equals("cli")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (plugin.areaCLIManager == null) { + p.sendMessage("§cCLI管理器未初始化"); + return true; + } + String action = args.length >= 2 ? args[1] : "menu"; + switch (action) { + case "menu": + plugin.areaCLIManager.showMainMenu(p); + break; + case "lands": + int page = 1; + if (args.length >= 3) { + try { page = Integer.parseInt(args[2]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showLandList(p, page); + break; + case "manage": + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli manage <领地名> [页码]"); + break; + } + int permPage = 1; + if (args.length >= 4) { + try { permPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showLandManage(p, args[2], permPage); + break; + case "members": + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli members <领地名> [页码]"); + break; + } + int memberPage = 1; + if (args.length >= 4) { + try { memberPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showMemberList(p, args[2], memberPage); + break; + case "visitorperm": + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli visitorperm <领地名> [页码]"); + break; + } + int vPermPage = 1; + if (args.length >= 4) { + try { vPermPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showVisitorPerm(p, args[2], vPermPage); + break; + case "toggle": + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli toggle <领地名> <权限key> [页码]"); + break; + } + int togglePage = 1; + if (args.length >= 5) { + try { togglePage = Integer.parseInt(args[4]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.togglePerm(p, args[2], args[3], togglePage); + // ★ 切换后刷新到访客授权页面(停留当前页面,不退出) + plugin.areaCLIManager.refreshCurrentPage(p); + break; + case "config": + // ★ CLI全局配置页面 + plugin.areaCLIManager.showConfigPage(p); + break; + case "memberperm": + // ★ 成员独立权限列表 + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli memberperm <领地名> [页码]"); + break; + } + int mpPage = 1; + if (args.length >= 4) { + try { mpPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showMemberPermList(p, args[2], mpPage); + break; + case "playerperm": + // ★ 某成员的独立权限编辑 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli playerperm <领地名> <玩家> [页码]"); + break; + } + int ppPage = 1; + if (args.length >= 5) { + try { ppPage = Integer.parseInt(args[4]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showPlayerPerm(p, args[2], args[3], ppPage); + break; + case "toggleplayerperm": + // ★ 切换某成员的独立权限 + if (args.length < 5) { + p.sendMessage("§c用法: /protect cli toggleplayerperm <领地名> <玩家> <权限key> [页码]"); + break; + } + int tpPage = 1; + if (args.length >= 6) { + try { tpPage = Integer.parseInt(args[5]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.togglePlayerPerm(p, args[2], args[3], args[4], tpPage); + break; + case "toggleadmin": + // ★ 切换玩家的管理员身份(领地主专属) + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli toggleadmin <领地名> <玩家> [页码]"); + break; + } + int taPage = 1; + if (args.length >= 5) { + try { taPage = Integer.parseInt(args[4]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.toggleAdmin(p, args[2], args[3], taPage); + break; + case "clearplayerperm": + // ★ 清除某成员所有自定义权限 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli clearplayerperm <领地名> <玩家>"); + break; + } + plugin.areaCLIManager.clearPlayerPerm(p, args[2], args[3]); + break; + case "effectsmgmt": + // ★ 效果管理菜单 + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli effectsmgmt <领地名> [子页]"); + break; + } + int effPage = 1; + if (args.length >= 4) { + try { effPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showEffectsManagement(p, args[2], effPage); + break; + case "effectsclearall": + // ★ 切换「全清负面效果」 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsclearall <领地名> on/off"); + break; + } + plugin.areaCLIManager.toggleClearAllBadEffects(p, args[2], args[3]); + // 刷新:回到效果管理(subPage 1) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 1); + break; + case "effectsdenyall": + // ★ 切换「禁止所有效果」 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsdenyall <领地名> on/off"); + break; + } + plugin.areaCLIManager.toggleDenyAllEffects(p, args[2], args[3]); + // 刷新:回到效果管理(subPage 1) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 1); + break; + case "effectsclearremove": + // ★ 移除指定清除效果 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsclearremove <领地名> <序号>"); + break; + } + int clrIdx = 1; + try { clrIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} + plugin.areaCLIManager.removeClearEffect(p, args[2], clrIdx); + // 刷新:回到单清效果列表(subPage 2) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 2); + break; + case "effectsclearadd": + // ★ 添加清除效果 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsclearadd <领地名> <效果名>"); + break; + } + plugin.areaCLIManager.addClearEffect(p, args[2], args[3]); + // 刷新:回到单清效果列表(subPage 2) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 2); + break; + case "effectsaddremove": + // ★ 移除指定增益效果 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsaddremove <领地名> <序号>"); + break; + } + int giveIdx = 1; + try { giveIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} + plugin.areaCLIManager.removeGiveEffect(p, args[2], giveIdx); + // 刷新:回到增益效果列表(subPage 3) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 3); + break; + case "effectsaddadd": + // ★ 添加增益效果: 效果名 [等级] [秒数] + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsaddadd <领地名> <效果名> [等级] [秒数]"); + break; + } + String effName = args[3]; + String effLevel = "1"; + String effDuration = "999"; + if (args.length >= 5) effLevel = args[4]; + if (args.length >= 6) effDuration = args[5]; + plugin.areaCLIManager.addGiveEffect(p, args[2], effName, effLevel, effDuration); + // 刷新:回到增益效果列表(subPage 3) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 3); + break; + case "effectsaddedit": + // ★ 编辑增益效果等级或时长: 序号 level|duration + if (args.length < 5) { + p.sendMessage("§c用法: /protect cli effectsaddedit <领地名> <序号> "); + break; + } + plugin.areaCLIManager.startEditGiveEffect(p, args[2], args[3], args[4]); + break; + case "effectsbadremove": + // ★ 移除指定负面效果(2026-10-04 从增益拆出) + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsbadremove <领地名> <序号>"); + break; + } + int badIdx = 1; + try { badIdx = Integer.parseInt(args[3]); } catch (Exception ignored) {} + plugin.areaCLIManager.removeBadEffect(p, args[2], badIdx); + // 刷新:回到负面效果列表(subPage 6) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 6); + break; + case "effectsbadadd": + // ★ 添加负面效果: 效果名 [等级] [秒数] + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli effectsbadadd <领地名> <效果名> [等级] [秒数]"); + break; + } + String badEffName = args[3]; + String badEffLevel = "1"; + String badEffDuration = "999"; + if (args.length >= 5) badEffLevel = args[4]; + if (args.length >= 6) badEffDuration = args[5]; + plugin.areaCLIManager.addBadEffect(p, args[2], badEffName, badEffLevel, badEffDuration); + // 刷新:回到负面效果列表(subPage 6) + plugin.areaCLIManager.showEffectsManagement(p, args[2], 6); + break; + case "effectsbadedit": + // ★ 编辑负面效果等级或时长: 序号 level|duration + if (args.length < 5) { + p.sendMessage("§c用法: /protect cli effectsbadedit <领地主> <序号> "); + break; + } + plugin.areaCLIManager.startEditBadEffect(p, args[2], args[3], args[4]); + break; + case "announcement": + // ★ 领地公告管理 + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli announcement <领地名> [子页]"); + break; + } + int annPage = 1; + if (args.length >= 4) { + try { annPage = Integer.parseInt(args[3]); } catch (Exception ignored) {} + } + plugin.areaCLIManager.showAnnouncementManagement(p, args[2], annPage); + break; + case "confiscate": + // ★ 违禁品管理面板(2026-10-11) + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli confiscate <领地名>"); + break; + } + plugin.areaCLIManager.showConfiscateManagement(p, args[2]); + break; + case "confiscateret": + // ★ 切换「退出领地是否归还没收物」 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli confiscateret <领地名> on/off"); + break; + } + plugin.areaCLIManager.toggleReturnOnLeave(p, args[2], args[3]); + plugin.areaCLIManager.showConfiscateManagement(p, args[2]); + break; + case "confiscateadd": + // ★ 添加禁带物品(交互式输入) + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli confiscateadd <领地名>"); + break; + } + plugin.areaCLIManager.startAddConfiscateItem(p, args[2]); + break; + case "confiscateremove": + // ★ 移除指定禁带物品 + if (args.length < 4) { + p.sendMessage("§c用法: /protect cli confiscateremove <领地名> <序号>"); + break; + } + plugin.areaCLIManager.removeConfiscateItem(p, args[2], args[3]); + plugin.areaCLIManager.showConfiscateManagement(p, args[2]); + break; + case "rename": + // ★ 领地改名(交互式输入) + if (args.length < 3) { + p.sendMessage("§c用法: /protect cli rename <领地名>"); + break; + } + { + String rnLandName = args[2]; + AreaConfig rnAc = getLand(rnLandName); + if (rnAc == null) { + p.sendMessage("§c领地不存在: " + rnLandName); + break; + } + boolean rnOwner = samePlayer(p.getName(), rnAc.owner); + boolean rnAdmin = isAreaAdmin(p); + if (!rnOwner && !rnAdmin) { + p.sendMessage("§c需要领地所有者或管理员权限"); + break; + } + // 设置等待输入状态 + pendingEffectInput.put(p.getUniqueId(), + new String[]{rnLandName, "rename_land"}); + p.sendMessage(AreaCLIManager.header("领地改名: " + rnLandName)); + long[] cd = getRenameCooldown(p); + if (cd != null) { + long rm = cd[0] / 60; + long rs = cd[0] % 60; + p.sendMessage(Component.text("§c冷却中!已用" + cd[1] + "/" + RENAME_LIMIT + "次," + + rm + "分" + rs + "秒后解锁")); + } else { + p.sendMessage(Component.text("§7剩余次数: §a" + getRemainingRenames(p) + "/" + RENAME_LIMIT)); + } + p.sendMessage(Component.text("§e请在聊天栏输入新的领地名:")); + p.sendMessage(Component.text("§7输入 §c取消 §7放弃修改")); + } + break; + case "create": + // 跳转到创建命令 + // 直接转发 + break; + default: + plugin.areaCLIManager.showMainMenu(p); + break; + } + return true; + } + + // ===== 创建 ===== + if (sub.equals("创建") || sub.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + 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以自身为中心) + if (args.length < 2) { + if (!pos1.containsKey(u) || !pos2.containsKey(u)) { + // 无选点:快速创建3×3 + if (quickCreateLand(p)) { + p.sendMessage("§7领地创建成功,如需精细控制请使用圈地工具选点后再次使用"); + } + } else { + // 有选点但没有名字:用默认名创建 + String autoName = p.getName(); + if (areaNameExists(autoName)) { + for (int i = 0; i <= 999; i++) { + String c = autoName + i; + if (!areaNameExists(c)) { autoName = c; break; } + } + } + p.sendMessage("§e用法: /protect 创建 <自定义名>"); + p.sendMessage("§7自动创建领地名: §e" + autoName); + // 通过confirm_create走标准流程 + p.performCommand("protect confirm_create " + autoName); + } + return true; + } + + String areaName = args[1].trim(); + // ★ 领地名格式校验:特殊字符会污染按名字落盘的白名单/配置文件 + String nameErr = validateAreaName(areaName); + if (nameErr != null) { + p.sendMessage("§c§l[防护] §f领地名无效: " + nameErr); + return true; + } + // 检查重名(不区分大小写:Windows 文件名大小写不敏感会互相覆盖) + if (areaNameExists(areaName)) { + p.sendMessage("§c区域已存在(大小写不同也算重复)"); + return true; + } + // ★ 检查每人领地数量上限(用户组专属上限)— 同时查内存和DB兜底 + UserGroupManager ugm = plugin.getUserGroup(); + int playerMaxLands = (ugm != null) ? ugm.getPlayerMaxLands(p.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; + long playerLandCount = areas.values().stream() + .filter(a -> samePlayer(p.getName(), a.owner)) + .count(); + // ★ DB兜底:如果内存计数<上限但DB实际已有更多领地(内存可能过期) + if (playerLandCount < playerMaxLands && dbConnection != null) { + try { + java.sql.PreparedStatement cntStmt = dbConnection.prepareStatement( + "SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); + cntStmt.setString(1, p.getName()); + java.sql.ResultSet cntRs = cntStmt.executeQuery(); + if (cntRs.next()) { + long dbCount = cntRs.getLong("cnt"); + if (dbCount > playerLandCount) playerLandCount = dbCount; + } + cntRs.close(); cntStmt.close(); + } catch (Exception ignored) {} + } + if (playerLandCount >= playerMaxLands) { + String groupHint = (ugm != null && ugm.getHighestGroup(p.getName()) != null) + ? "(用户组: " + ugm.getHighestGroup(p.getName()).displayName + ")" : ""; + p.sendMessage("§c§l[防护] §f已达领地上限!每人最多 §e" + playerMaxLands + " §f个领地(当前: " + playerLandCount + "个)" + groupHint); + return true; + } + Location l1 = pos1.get(u); + Location l2 = pos2.get(u); + + // ★ 领地计费:计算面积并预估价格(用户组专属价格) + int width = Math.abs(l2.getBlockX() - l1.getBlockX()) + 1; + int length = Math.abs(l2.getBlockZ() - l1.getBlockZ()) + 1; + int area = width * length; + int effectivePricePerSqm = (ugm != null) ? ugm.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; + int cost = area * effectivePricePerSqm; + String priceSource = (effectivePricePerSqm != globalCreatePricePerSqm) ? "§7(用户组优惠价)" : ""; + + // ★ Issue 4: 创建确认提示 — 显示预估价格 + 交互链接 + BondManager bm = plugin.getBonds(); + int balance = (bm != null) ? bm.getBonds(p.getName()) : 0; + String balanceStr = (bm != null) ? String.valueOf(balance) : "?"; + p.sendMessage("§e§l[防护] §f创建领地 §a" + areaName + " §f的预览:"); + p.sendMessage("§7 面积: §f" + area + "㎡(" + width + "×" + length + ")"); + if (width > globalMaxLandSideBlocks || length > globalMaxLandSideBlocks + || area > globalMaxFreeAreaSqm) { + p.sendMessage("§e§l[防护] §f选区超出免审上限(单次 " + globalMaxFreeAreaSqm + + "㎡ / 边长 " + globalMaxLandSideBlocks + " 格),确认创建后将提交管理员现场审批"); + } + p.sendMessage("§7 单价: §f" + effectivePricePerSqm + "/㎡" + priceSource); + p.sendMessage("§7 费用: §e" + cost + " §7债券 余额: §a" + balanceStr + " §7债券"); + if (bm != null && balance < cost) { + p.sendMessage("§c§l[防护] §f余额不足!需要 §e" + cost + " §f,当前 §e" + balance); + return true; + } + if (p instanceof Player) { + Player pp = (Player) p; + String confirmCmd = "/protect confirm_create " + areaName; + pp.sendMessage(Component.empty() + .append(Component.text("§a§l[✅ 点击确认创建]")) + .hoverEvent(HoverEvent.showText(Component.text("§e点击确认创建领地 §f" + areaName + "\n§7费用: " + cost + " 债券"))) + .clickEvent(ClickEvent.runCommand(confirmCmd)) + ); + pp.sendMessage(Component.text("§7或手动输入: §f/protect 创建 " + areaName)); + } + return true; + } + + // ===== 确认创建领地(交互链接触发) ===== + if (sub.equals("confirm_create")) { + 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); + if (cnErr != null) { + cp.sendMessage("§c§l[防护] §f领地名无效: " + cnErr); + return true; + } + // 检查选点 + if (!pos1.containsKey(cp.getUniqueId()) || !pos2.containsKey(cp.getUniqueId())) { + cp.sendMessage("§c请先用工具选点"); return true; + } + // ★ 第57批续21 提前取选区坐标(供空间重叠识别,原在下方重复声明已移除) + Location cl1 = pos1.get(cp.getUniqueId()); + Location cl2 = pos2.get(cp.getUniqueId()); + int cw = Math.abs(cl2.getBlockX() - cl1.getBlockX()) + 1; + int cl = Math.abs(cl2.getBlockZ() - cl1.getBlockZ()) + 1; + int cArea = cw * cl; + // 检查重名(不区分大小写) + if (areaNameExists(confirmName)) { cp.sendMessage("§c区域已存在(大小写不同也算重复)"); return true; } + // ★ 第57批续21 空间重叠智能识别 + 子领地身份闸门(修"任何人能在别人领地内建重叠领地"绕过漏洞) + int cnx1 = Math.min(cl1.getBlockX(), cl2.getBlockX()); + int cnz1 = Math.min(cl1.getBlockZ(), cl2.getBlockZ()); + int cnx2 = Math.max(cl1.getBlockX(), cl2.getBlockX()); + int cnz2 = Math.max(cl1.getBlockZ(), cl2.getBlockZ()); + String cnWorld = cl1.getWorld().getName(); + AreaConfig cnMain = findContainingMainLand(cnWorld, cnx1, cnz1, cnx2, cnz2); + boolean cnIsSub = cnMain != null; + if (cnIsSub) { + if (!isQualifiedLandManager(cp, cnMain)) { + cp.sendMessage("§c§l[防护] §f拒绝创建: 当前选区已在领地 §e" + cnMain.name + + " §f范围内,你需具备该领地的领主/管理员/授权管理员身份才能创建子领地。"); + return true; + } + } else { + // 无完全包含的主领地 → 部分重叠(跨边界)一律拒绝 + for (AreaConfig ot : areas.values()) { + if (ot == null || ot.parentId != 0) continue; + if (!ot.world.equalsIgnoreCase(cnWorld)) continue; + int omx = Math.min(ot.x1, ot.x2), omx2 = Math.max(ot.x1, ot.x2); + int omz = Math.min(ot.z1, ot.z2), omz2 = Math.max(ot.z1, ot.z2); + if (cnx1 <= omx2 && cnx2 >= omx && cnz1 <= omz2 && cnz2 >= omz) { + cp.sendMessage("§c§l[防护] §f拒绝创建: 选区与领地 §e" + ot.name + + " §f部分重叠(跨边界),子领地必须完整位于某主领地内,请重选区域。"); + return true; + } + } + } + if (cnIsSub) { + // 子领地命名冲突:自动追加序号(父领地内子领地可多个) + String subName = confirmName; + if (areaNameExists(subName)) { + int s2 = 1; + while (areaNameExists(subName + s2)) s2++; + confirmName = subName + s2; + } + } + // ★ 检查上限(含DB兜底) + UserGroupManager cugm = plugin.getUserGroup(); + int cMaxLands = (cugm != null) ? cugm.getPlayerMaxLands(cp.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; + long cLandCount = areas.values().stream().filter(a -> samePlayer(cp.getName(), a.owner)).count(); + if (cLandCount < cMaxLands && dbConnection != null) { + try { + java.sql.PreparedStatement cs = dbConnection.prepareStatement("SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); + cs.setString(1, cp.getName()); + java.sql.ResultSet cr = cs.executeQuery(); + if (cr.next()) { long dbc = cr.getLong("cnt"); if (dbc > cLandCount) cLandCount = dbc; } + cr.close(); cs.close(); + } catch (Exception ignored) {} + } + if (!cnIsSub && cLandCount >= cMaxLands) { cp.sendMessage("§c§l[防护] §f已达领地上限!"); return true; } + int cPrice = (cugm != null) ? cugm.getPlayerLandPricePerSqm(cp.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; + int cCost = cArea * cPrice; + + // ★ 创建面积限制(2026-09-29):选区面积/边长超免审上限 → 转管理员现场审批,暂不扣费 + if (!isAreaAdmin(cp) + && (cArea > globalMaxFreeAreaSqm + || cw > globalMaxLandSideBlocks + || cl > globalMaxLandSideBlocks)) { + registerPendingCreate(cp, confirmName, cl1, cl2, cw, cl, cArea, cCost); + return true; + } + + // 扣费 + int chargedCost = 0; + if (cCost > 0) { + BondManager cbm = plugin.getBonds(); + if (cbm != null) { + if (!cbm.deductBonds(cp.getName(), cCost, "land_create", cp.getName(), cp.getName(), "创建领地: " + confirmName + " (" + cArea + "㎡×" + cPrice + ")")) { + cp.sendMessage("§c§l[防护] §f扣费失败,请稍后重试"); return true; + } + String cSrc = (cPrice != globalCreatePricePerSqm) ? "(用户组优惠价)" : ""; + cp.sendMessage("§a§l[防护] §f创建领地扣除 §e" + cCost + " §f债券(" + cArea + "㎡×" + cPrice + "/㎡)" + cSrc); + chargedCost = cCost; + } + } + AreaConfig cac = new AreaConfig(); + cac.name = confirmName; + // ★ 第57批续21 子领地:owner 沿用主领地主;主领地:owner=创建者 + cac.owner = cnIsSub ? cnMain.owner : cp.getName(); + cac.world = cl1.getWorld().getName(); + cac.x1 = cl1.getBlockX(); cac.z1 = cl1.getBlockZ(); cac.x2 = cl2.getBlockX(); cac.z2 = cl2.getBlockZ(); + cac.yMin = 0; cac.yMax = 255; + cac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数) + cac.parentId = cnIsSub ? getLandIdFromDb(cnMain.name) : 0; // ★ 第57批续21 子领地父ID + saveAreaToDb(cac); + areas.put(confirmName, cac); // ★ 立即更新内存 + if (cnIsSub) { + cp.sendMessage("§a§l[防护] §f子领地 §e" + confirmName + " §f已创建(父领地: " + cnMain.name + ")"); + } else { + cp.sendMessage("§a§l[防护] §f区域 " + confirmName + " 已创建 (owner: " + cac.owner + ")"); + } + loadAllAreas(); + return true; + } + + // ===== 和平白名单 ===== + if (sub.equals("addname")) { + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别: " + + args[1] + " 或 " + args[2]); + return true; + } + AreaConfig ac = areas.get(parsed[0]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + // ★ 权限闸门:只有领地主/领地管理员才能改和平白名单 + if (!requireLandOwnership(sender, parsed[0])) return true; + ac.peaceWhitelist.add(parsed[1]); + saveAreaToDb(ac); + sender.sendMessage("§a已添加: " + + parsed[1] + " → " + + parsed[0] + " 和平白名单"); + return true; + } + sender.sendMessage("§c用法: /protect addname " + + "<区域> <名字>"); + return true; + } + if (sub.equals("removename")) { + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别"); + return true; + } + AreaConfig ac = areas.get(parsed[0]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + // ★ 权限闸门:只有领地主/领地管理员才能改和平白名单 + if (!requireLandOwnership(sender, parsed[0])) return true; + ac.peaceWhitelist.remove(parsed[1]); + saveAreaToDb(ac); + sender.sendMessage("§a已移除: " + + parsed[1] + " ← " + parsed[0]); + return true; + } + sender.sendMessage("§c用法: /protect removename " + + "<区域> <名字>"); + return true; + } + if (sub.equals("listname")) { + if (args.length < 2) { + sender.sendMessage( + "§c用法: /protect listname <区域>"); + return true; + } + AreaConfig ac = areas.get(args[1]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + sender.sendMessage("§a" + args[1] + + " 和平白名单:"); + if (ac.peaceWhitelist.isEmpty()) { + sender.sendMessage("§7 (空)"); + } else { + for (String n : ac.peaceWhitelist) { + sender.sendMessage("§7 - " + n); + } + } + return true; + } + + // ===== 模式排除 ===== + if (sub.equals("addwhite")) { + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别"); + return true; + } + AreaConfig ac = areas.get(parsed[0]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + // ★ 权限闸门:只有领地主/领地管理员才能改模式排除名单 + if (!requireLandOwnership(sender, parsed[0])) return true; + ac.modeExempt.add(parsed[1]); + saveAreaToDb(ac); + sender.sendMessage("§a已添加: " + + parsed[1] + " → " + + parsed[0] + " 模式排除"); + return true; + } + sender.sendMessage("§c用法: /protect addwhite " + + "<区域> <玩家>"); + return true; + } + if (sub.equals("removewhite")) { + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别"); + return true; + } + AreaConfig ac = areas.get(parsed[0]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + // ★ 权限闸门:只有领地主/领地管理员才能改模式排除名单 + if (!requireLandOwnership(sender, parsed[0])) return true; + ac.modeExempt.remove(parsed[1]); + saveAreaToDb(ac); + sender.sendMessage("§a已移除: " + + parsed[1] + " ← " + + parsed[0] + " 模式排除"); + return true; + } + sender.sendMessage("§c用法: /protect removewhite " + + "<区域> <玩家>"); + return true; + } + if (sub.equals("listwhite")) { + if (args.length < 2) { + sender.sendMessage( + "§c用法: /protect listwhite <区域>"); + return true; + } + AreaConfig ac = areas.get(args[1]); + if (ac == null) { + sender.sendMessage("§c区域不存在"); + return true; + } + sender.sendMessage("§a" + args[1] + + " 模式排除名单:"); + if (ac.modeExempt.isEmpty()) { + sender.sendMessage("§7 (空)"); + } else { + for (String n : ac.modeExempt) { + sender.sendMessage("§7 - " + n); + } + } + return true; + } + + // ===== 列表 ===== + if (sub.equals("列表") || sub.equals("list")) { + if (args.length == 2) { + if (args[1].equalsIgnoreCase("global")) { + sender.sendMessage("§a全局白名单:"); + if (globalPlayerWhitelist.isEmpty()) { + sender.sendMessage("§7 (空)"); + } else { + for (String n + : globalPlayerWhitelist) { + sender.sendMessage("§7 - " + n); + } + } + return true; + } + String an = args[1]; + Set wl = + areaPlayerWhitelist.get(an); + sender.sendMessage("§a" + an + " 白名单:"); + if (wl == null || wl.isEmpty()) { + sender.sendMessage("§7 (空)"); + } else { + for (String n : wl) { + sender.sendMessage("§7 - " + n); + } + } + return true; + } + if (areas.isEmpty()) { + sender.sendMessage("§7暂无区域"); + } else { + sender.sendMessage("§e§l区域列表:"); + for (Map.Entry en + : areas.entrySet()) { + sender.sendMessage("§a " + en.getKey() + + " §7规则:" + + en.getValue().ruleCount() + + "条"); + } + } + return true; + } + + // ===== config 全局配置 ===== + if (sub.equals("config") || sub.equals("配置")) { + if (!isAreaAdmin(sender)) { + sender.sendMessage("§c需要管理员权限"); + return true; + } + if (args.length < 3) { + // 查看配置 + sender.sendMessage("§e§l==== 全局配置 ===="); + sender.sendMessage("§a创建价格(每㎡): §f" + globalCreatePricePerSqm); + sender.sendMessage("§a每人最大领地数: §f" + globalMaxLandsPerPlayer); + sender.sendMessage("§a删除退费比例(%): §f" + globalRefundRatioPerCent + + (globalRefundRatioPerCent <= 0 ? " §7(已关闭退费)" + : (globalRefundRatioPerCent >= 100 ? " §7(全额退款)" : " §7(原价×比例)"))); + sender.sendMessage("§a单次扩建免审面积(㎡): §f" + globalMaxFreeAreaSqm + " §7(超限转管理员审批)"); + sender.sendMessage("§a领地免审边长(格): §f" + globalMaxLandSideBlocks + "×" + globalMaxLandSideBlocks + " §7(超限转管理员审批)"); + sender.sendMessage("§7用法: /protect config "); + sender.sendMessage("§7可用key: create_price, max_lands, default_height, peace_duration, refund_ratio, max_free_area, max_land_side"); + return true; + } + String key = args[1]; + String value = args[2]; + try { + switch (key) { + case "create_price": + globalCreatePricePerSqm = parseSmartNumber(value, globalCreatePricePerSqm, 0); + updateAreaConfig("create_price_per_sqm", String.valueOf(globalCreatePricePerSqm)); + break; + case "max_lands": + globalMaxLandsPerPlayer = parseSmartNumber(value, globalMaxLandsPerPlayer, 1); + updateAreaConfig("max_lands_per_player", String.valueOf(globalMaxLandsPerPlayer)); + break; + case "default_height": + int newHeight = parseSmartNumber(value, 255, 1); + updateAreaConfig("default_height", String.valueOf(newHeight)); + break; + case "peace_duration": + int newDuration = parseSmartNumber(value, 3600, 0); + updateAreaConfig("peace_mode_max_duration", String.valueOf(newDuration)); + break; + case "refund_ratio": + int newRatio = parseSmartNumber(value, globalRefundRatioPerCent, 0); + if (newRatio > 100) newRatio = 100; // 上限100%=全额退款 + if (newRatio < 0) newRatio = 0; // 0=关闭退费 + globalRefundRatioPerCent = newRatio; + updateAreaConfig("refund_ratio_per_cent", String.valueOf(newRatio)); + break; + case "max_free_area": + int newFree = parseSmartNumber(value, globalMaxFreeAreaSqm, 1); + if (newFree < 1) newFree = 1; + globalMaxFreeAreaSqm = newFree; + updateAreaConfig("max_free_area_sqm", String.valueOf(newFree)); + break; + case "max_land_side": + int newSide = parseSmartNumber(value, globalMaxLandSideBlocks, 1); + if (newSide < 1) newSide = 1; + globalMaxLandSideBlocks = newSide; + updateAreaConfig("max_land_side_blocks", String.valueOf(newSide)); + break; + default: + sender.sendMessage("§c未知配置项: " + key); + return true; + } + // ★ 配置变更:立即触发PHP同步 + try { + if (plugin.webManager != null) { + plugin.webManager.requestImmediateLandSync(); + } + } catch (Exception ignored) {} + + // ★ 更新后显示完整配置页面(而非仅打印"已更新") + if (plugin.areaCLIManager != null) { + plugin.areaCLIManager.showConfigPage((Player) sender); + } else { + sender.sendMessage("§a配置已更新"); + } + } catch (Exception e) { + sender.sendMessage("§c无效的值: " + value + "(支持: 纯数字、+/-前缀、中文数字、罗马数字、英文数字)"); + } + return true; + } + + // ===== 重载 ===== + if (sub.equals("重载") || sub.equals("reload")) { + reload(); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); + return true; + } + + // ===== 公共建筑设施 ===== + if (sub.equals("public") || sub.equals("公共")) { + if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§c用法: /protect public <领地名>"); + return true; + } + String landName = args[1]; + AreaConfig ac = areas.get(landName); + if (ac == null) { sender.sendMessage("§c领地不存在: " + landName); return true; } + // 检查权限 + if (!samePlayer(p.getName(), ac.owner) && !isAreaAdmin(sender)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + ac.isPublicBuilding = !ac.isPublicBuilding; + saveAreaToDb(ac); + if (ac.isPublicBuilding) { + sender.sendMessage("§a§l[公共建筑] §f" + landName + " §a已设为公共建筑设施"); + sender.sendMessage("§7所有访客将自动获得传送、免疫伤害、攻击敌对生物权限"); + } else { + sender.sendMessage("§a§l[公共建筑] §f" + landName + " §7已取消公共建筑标记"); + } + return true; + } + if (sub.equals("listpublic") || sub.equals("公共列表")) { + if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } + Player p = (Player) sender; + java.util.List publicLands = new ArrayList<>(); + for (AreaConfig ac : areas.values()) { + if (ac.isPublicBuilding) publicLands.add(ac); + } + if (publicLands.isEmpty()) { + sender.sendMessage("§7当前没有公共建筑设施"); + return true; + } + sender.sendMessage("§e§l==== 公共建筑设施 ===="); + int idx = 1; + for (AreaConfig ac : publicLands) { + String warpInfo = (ac.warpX != 0 || ac.warpZ != 0) ? "§a[有传送点]" : "§7[无传送点]"; + // 可点击传送的列表项 + p.sendMessage(Component.empty() + .append(Component.text("§e" + idx + ". §f" + ac.name + " §7所有者:" + ac.owner + " " + warpInfo)) + .append(Component.text(" §a[传送到此]")) + .hoverEvent(HoverEvent.showText(Component.text("§e点击传送到 " + ac.name))) + .clickEvent(ClickEvent.runCommand("/protect tpb " + ac.name)) + ); + idx++; + } + return true; + } + if (sub.equals("tpb") || sub.equals("传送公共")) { + if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§c用法: /protect tpb <序号或领地名>"); + return true; + } + java.util.List publicLands = new ArrayList<>(); + for (AreaConfig ac : areas.values()) { + if (ac.isPublicBuilding) publicLands.add(ac); + } + if (publicLands.isEmpty()) { sender.sendMessage("§c没有公共建筑设施"); return true; } + AreaConfig target = null; + // 尝试序号 + try { + int num = Integer.parseInt(args[1]); + if (num >= 1 && num <= publicLands.size()) { + target = publicLands.get(num - 1); + } + } catch (NumberFormatException ignored) {} + // 尝试名字 + if (target == null) { + for (AreaConfig ac : publicLands) { + if (ac.name.equalsIgnoreCase(args[1])) { target = ac; break; } + } + } + if (target == null) { sender.sendMessage("§c未找到公共建筑: " + args[1]); return true; } + // 传送(优先用传送点,否则传送到领地中心) + Location dest; + if (target.warpX != 0 || target.warpZ != 0 || target.warpY != 0) { + World w = Bukkit.getWorld(target.warpWorld != null && !target.warpWorld.isEmpty() ? target.warpWorld : p.getWorld().getName()); + if (w == null) w = p.getWorld(); + dest = new Location(w, target.warpX, target.warpY, target.warpZ, target.warpYaw, target.warpPitch); + } else { + dest = new Location(p.getWorld(), + (target.x1 + target.x2) / 2.0 + 0.5, + p.getWorld().getHighestBlockYAt((target.x1 + target.x2) / 2, (target.z1 + target.z2) / 2) + 1, + (target.z1 + target.z2) / 2.0 + 0.5); + } + p.teleport(dest); + sender.sendMessage("§a已传送至公共建筑: §f" + target.name); + return true; + } + + // ===== 用户组管理 ===== + if (sub.equals("grouplist") || sub.equals("用户组")) { + if (!sender.hasPermission("sdf1.admin")) { + sender.sendMessage("§c需要管理员权限"); return true; + } + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + + // ★ 先从PHP拉取最新用户组(确保PHP端创建的组能显示) + if (plugin.webManager != null) { + plugin.webManager.pullUserGroupsFromPHP(); + } + + showGroupListPanel(sender); + return true; + } + + if (sub.equals("groupset")) { + if (!sender.hasPermission("sdf1.admin")) { + sender.sendMessage("§c需要管理员权限"); return true; + } + if (args.length < 2) { + sender.sendMessage("§e用法: /protect groupset <组名> [显示名] [颜色] [优先级] [每㎡价格] [最大领地数]"); + sender.sendMessage("§7或: /protect groupset <组名> price <值> | maxlands <值> | priority <值>"); + sender.sendMessage("§7或: /protect groupset sync §c(从PHP同步)"); + return true; + } + // ★ groupset sync → 从PHP拉取用户组 + if (args[1].equalsIgnoreCase("sync")) { + sender.sendMessage("§b正在从PHP拉取用户组..."); + if (plugin.webManager != null) { + plugin.webManager.pullUserGroupsFromPHP(); + } + showGroupListPanel(sender); + return true; + } + String groupName = args[1].toLowerCase(); + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(groupName); + + // ★ 支持单字段编辑: /protect groupset <组名> price/maxlands/priority <值> + if (args.length == 4 && cfg != null) { + String field = args[2].toLowerCase(); + String valStr = args[3]; + // 智能数字解析(中文/罗马/英文数字) + Integer parsed = parseSmartNumberBase(valStr); + if (parsed == null) { + sender.sendMessage("§c无法解析数字: " + valStr); + return true; + } + int val = parsed; + switch (field) { + case "price": + case "价格": + cfg.landPricePerSqm = val; + break; + case "maxlands": + case "上限": + case "领地上限": + cfg.maxLands = val; + break; + case "priority": + case "优先级": + cfg.priority = val; + break; + default: + sender.sendMessage("§c未知字段: " + field + "。支持: price/maxlands/priority"); + return true; + } + ugm.updateGroupConfig(cfg); + sender.sendMessage("§a已更新用户组 §f" + cfg.displayName + " §a的 " + field + " = " + val); + // 刷新编辑面板 + if (sender instanceof Player) showGroupEditCLI((Player) sender, cfg); + return true; + } + + if (cfg == null) cfg = new UserGroupManager.UserGroupConfig(); + cfg.name = groupName; + if (args.length >= 3) cfg.displayName = args[2]; + if (args.length >= 4) cfg.displayColor = args[3]; + if (args.length >= 5) { + try { cfg.priority = Integer.parseInt(args[4]); } catch (Exception ignored) {} + } + if (args.length >= 6) { + try { cfg.landPricePerSqm = Integer.parseInt(args[5]); } catch (Exception ignored) {} + } + if (args.length >= 7) { + try { cfg.maxLands = Integer.parseInt(args[6]); } catch (Exception ignored) {} + } + if (cfg.displayName.isEmpty()) cfg.displayName = groupName; + ugm.saveGroupConfigToDB(cfg); + ugm.loadGroupConfigs(); + sender.sendMessage("§a已创建/更新用户组: §f" + cfg.displayColor + cfg.displayName + + " §7(name=" + groupName + ", 价格=" + cfg.landPricePerSqm + "/㎡" + + ", 上限=" + cfg.maxLands + ")"); + // ★ 创建/更新后重新打印面板,让用户看到组并能操作 + showGroupListPanel(sender); + return true; + } + + if (sub.equals("groupdelconfig")) { + if (!sender.hasPermission("sdf1.admin")) { + sender.sendMessage("§c需要管理员权限"); return true; + } + if (args.length < 2) { + sender.sendMessage("§e用法: /protect groupdelconfig <组名>"); + return true; + } + String groupName = args[1].toLowerCase(); + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + if (groupName.equals(UserGroupManager.DEFAULT_GROUP)) { + sender.sendMessage("§c不能删除默认组"); return true; + } + if (ugm.deleteGroupConfig(groupName)) { + sender.sendMessage("§a已删除用户组: " + groupName); + } else { + sender.sendMessage("§c未找到用户组: " + groupName); + } + showGroupListPanel(sender); + return true; + } + + // ★ 用户组交互式编辑(CLI模式) + if (sub.equals("groupedit")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); return true; + } + if (args.length < 2) { + sender.sendMessage("§e用法: /protect groupedit <组名>"); + return true; + } + Player gp = (Player) sender; + String gName = args[1]; + UserGroupManager ugmEdit = plugin.getUserGroup(); + if (ugmEdit == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + UserGroupManager.UserGroupConfig cfgEdit = ugmEdit.getGroupConfig(gName); + if (cfgEdit == null) { + sender.sendMessage("§c未找到用户组: " + gName); + return true; + } + showGroupEditCLI(gp, cfgEdit); + return true; + } + + if (sub.equals("groupadd") || sub.equals("addmember")) { + if (args.length < 3) { + sender.sendMessage("§e用法: /protect groupadd <组名> <玩家名>"); + return true; + } + String inputName = args[1]; + String playerName = args[2]; + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + UserGroupManager.UserGroupConfig cfgAdd = ugm.getGroupConfig(inputName); + if (cfgAdd == null) { + sender.sendMessage("§c未找到用户组: " + inputName); + return true; + } + String groupName = cfgAdd.name; // 使用DB中的实际组名 + String err = ugm.addPlayer(playerName, groupName, sender.getName()); + if (err == null) { + sender.sendMessage("§a已将 §f" + playerName + " §a加入用户组: §f" + cfgAdd.displayName); + // 通知在线玩家 + Player target = plugin.getServer().getPlayerExact(playerName); + if (target != null) { + target.sendMessage("§a§l[用户组] §f你已被加入用户组: §e" + cfgAdd.displayName); + } + } else { + sender.sendMessage("§c" + err); + } + return true; + } + + if (sub.equals("groupdel")) { + if (args.length < 3) { + sender.sendMessage("§e用法: /protect groupdel <组名> <玩家名>"); + return true; + } + String inputName = args[1]; + String playerName = args[2]; + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + UserGroupManager.UserGroupConfig cfgDel = ugm.getGroupConfig(inputName); + String groupName = (cfgDel != null) ? cfgDel.name : inputName; + if (ugm.removePlayer(playerName, groupName)) { + String displayName = (cfgDel != null) ? cfgDel.displayName : groupName; + sender.sendMessage("§a已将 §f" + playerName + " §a移出用户组: " + displayName); + Player target = plugin.getServer().getPlayerExact(playerName); + if (target != null) { + target.sendMessage("§a§l[用户组] §f你已被移出用户组: §e" + displayName); + } + } else { + sender.sendMessage("§c操作失败(玩家可能不在此组)"); + } + return true; + } + + // ===== 用户组成员管理面板 ===== + if (sub.equals("groupmembers")) { + if (args.length < 2) { + sender.sendMessage("§e用法: /protect groupmembers <组名>"); + return true; + } + String inputName = args[1]; + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return true; } + UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(inputName); + if (cfg == null) { sender.sendMessage("§c未找到用户组: " + inputName); return true; } + String groupName = cfg.name; // 使用DB中的实际组名(保留大小写) + + java.util.List> members = ugm.getGroupMembers(groupName); + sender.sendMessage("§a§l========== " + cfg.displayColor + cfg.displayName + " §a成员管理 =========="); + sender.sendMessage("§7共 §f" + members.size() + " §7名成员"); + + if (members.isEmpty()) { + sender.sendMessage("§7暂无成员"); + } else { + for (Map m : members) { + String pn = (String) m.get("player_name"); + String addedBy = (String) m.get("added_by"); + if (sender instanceof Player) { + Player pp = (Player) sender; + pp.sendMessage(Component.empty() + .append(Component.text(" §f" + pn + " §7(添加者: " + (addedBy != null ? addedBy : "system") + ") ")) + .append(Component.text("§c[移除]")) + .hoverEvent(HoverEvent.showText(Component.text("§c点击移除此成员"))) + .clickEvent(ClickEvent.runCommand("/protect groupdel " + groupName + " " + pn)) + ); + } else { + sender.sendMessage(" §f" + pn + " §7(添加者: " + (addedBy != null ? addedBy : "system") + ")"); + } + } + } + + // 操作入口 + sender.sendMessage(""); + if (sender instanceof Player) { + Player pp = (Player) sender; + // 添加在线玩家 + pp.sendMessage(Component.empty() + .append(Component.text("§a[添加在线玩家] ")) + .hoverEvent(HoverEvent.showText(Component.text("§e输入玩家名添加到此组"))) + .clickEvent(ClickEvent.suggestCommand("/protect groupadd " + groupName + " ")) + ); + // 返回用户组列表(上一层) + pp.sendMessage(Component.empty() + .append(Component.text("§c[返回用户组列表] ")) + .hoverEvent(HoverEvent.showText(Component.text("§c返回用户组管理列表"))) + .clickEvent(ClickEvent.runCommand("/protect grouplist")) + ); + // 返回领地系统首页(CLI模式) + pp.sendMessage(Component.empty() + .append(Component.text("§7[返回领地首页] ")) + .hoverEvent(HoverEvent.showText(Component.text("§7返回领地系统主菜单"))) + .clickEvent(ClickEvent.runCommand("/protect")) + ); + } + sender.sendMessage("§7§l─────────────────────────────────"); + return true; + } + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { + if (args.length == 2) { + // ★ 全服级物品黑名单,必须管理员 + if (!requireGlobalAdmin(sender, "全局物品黑名单操作")) return true; + String itemName = args[1].toUpperCase(); + if (Material.matchMaterial(itemName) + == null) { + sender.sendMessage( + "§c无效物品ID: " + args[1]); + return true; + } + globalItemBlacklist.add(itemName); + sender.sendMessage("§a已全局加黑: " + + itemName); + return true; + } + if (args.length == 3) { + String r1 = resolveAreaNameStrict(args[1]); + String r2 = resolveAreaNameStrict(args[2]); + String areaName; + String itemName; + if (r1 != null) { + areaName = r1; + itemName = args[2].toUpperCase(); + } else if (r2 != null) { + areaName = r2; + itemName = args[1].toUpperCase(); + } else { + areaName = "global"; + itemName = args[1].toUpperCase(); + } + // ★ 权限闸门:领地黑名单须领地主/领地管理员,global 须插件管理员 + if (!requireLandOwnership(sender, areaName)) return true; + if (Material.matchMaterial(itemName) + == null) { + sender.sendMessage( + "§c无效物品ID: " + itemName); + return true; + } + if (areaName.equalsIgnoreCase("global")) { + globalItemBlacklist.add(itemName); + sender.sendMessage("§a已全局加黑: " + + itemName); + } else { + areaItemBlacklist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap + .newKeySet()) + .add(itemName); + sender.sendMessage("§a已加黑: " + + itemName + " → " + areaName); + } + return true; + } + sender.sendMessage("§c用法: /protect additem " + + "<区域/物品> <区域/物品>"); + return true; + } + if (sub.equals("removeitem")) { + if (args.length == 2) { + // ★ 全服级物品黑名单,必须管理员 + if (!requireGlobalAdmin(sender, "全局物品黑名单操作")) return true; + String itemName = args[1].toUpperCase(); + if (!globalItemBlacklist + .contains(itemName)) { + sender.sendMessage( + "§c全局黑名单中没有: " + + itemName); + return true; + } + globalItemBlacklist.remove(itemName); + sender.sendMessage("§a已移除: " + itemName); + return true; + } + if (args.length == 3) { + String r1 = resolveAreaNameStrict(args[1]); + String r2 = resolveAreaNameStrict(args[2]); + String areaName; + String itemName; + if (r1 != null) { + areaName = r1; + itemName = args[2].toUpperCase(); + } else if (r2 != null) { + areaName = r2; + itemName = args[1].toUpperCase(); + } else { + areaName = "global"; + itemName = args[1].toUpperCase(); + } + // ★ 权限闸门:领地黑名单须领地主/领地管理员,global 须插件管理员 + if (!requireLandOwnership(sender, areaName)) return true; + if (areaName.equalsIgnoreCase("global")) { + globalItemBlacklist.remove(itemName); + } else { + Set list = + areaItemBlacklist.get(areaName); + if (list != null) { + list.remove(itemName); + } + } + sender.sendMessage("§a已移除: " + + itemName); + return true; + } + sender.sendMessage("§c用法: /protect removeitem " + + "<区域/物品> <区域/物品>"); + return true; + } + if (sub.equals("listitem")) { + if (args.length < 2) { + sender.sendMessage( + "§a全局物品黑名单:"); + if (globalItemBlacklist.isEmpty()) { + sender.sendMessage("§7(空)"); + } else { + for (String item + : globalItemBlacklist) { + sender.sendMessage("§f- " + item); + } + } + return true; + } + Set list = + areaItemBlacklist.get(args[1]); + sender.sendMessage("§a" + args[1] + + " 物品黑名单:"); + if (list == null || list.isEmpty()) { + sender.sendMessage("§7(空)"); + } else { + for (String item : list) { + sender.sendMessage("§f- " + item); + } + } + return true; + } + + // ===== accept / deny 面积审批(管理员现场审批) ===== + if (sub.equals("accept") || sub.equals("deny")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + handleApproval((Player) sender, args, sub.equals("accept")); + return true; + } + + // ===== expand 扩建 ===== + if (sub.equals("expand")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + p.sendMessage("§c你不在任何防护区域内"); + return true; + } + // ★ 权限闸门:改边界必须是领地主/领地管理员 + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + p.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + int amount = 5; + if (args.length >= 2) { + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException ignored) {} + } + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + float yaw = p.getLocation().getYaw(); + String dir = yawToDir(yaw); + + // ★ 面积限制(2026-09-29):先按原边界算基数,再算提案边界 + if (amount <= 0) { + p.sendMessage("§c§l[防护] §f扩建格数必须为正整数"); + return true; + } + if (amount > 100000) { + p.sendMessage("§c§l[防护] §f单次扩建格数上限 100000,请分次扩建"); + return true; + } + long oldW = (long) maxX - minX + 1; + long oldL = (long) maxZ - minZ + 1; + long oldAreaSq = oldW * oldL; + + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; + + long newW = (long) maxX - minX + 1; + long newL = (long) maxZ - minZ + 1; + long newAreaSq = newW * newL; + long deltaSq = newAreaSq - oldAreaSq; + boolean overFree = deltaSq > globalMaxFreeAreaSqm + || newW > globalMaxLandSideBlocks + || newL > globalMaxLandSideBlocks; + if (overFree && !isAreaAdmin(p)) { + // 超限 → 转管理员现场审批(插件管理员不受限) + registerPendingExpand(p, ac, minX, maxX, minZ, maxZ, + amount, dir, deltaSq, newW, newL, oldW, oldL); + return true; + } + + // ★ 扩建扣费(2026-10-03):与创建同单价,余额不足则拒绝扩建 + int expPrice = landPricePerSqm(p); + long expCostL = deltaSq * (long) expPrice; + if (expCostL > Integer.MAX_VALUE) { + p.sendMessage("§c§l[防护] §f本次扩建费用超出上限,请分次扩建"); + return true; + } + int expCost = (int) expCostL; + if (expCost > 0) { + BondManager ebm = plugin.getBonds(); + if (ebm == null || !ebm.deductBonds(p.getName(), expCost, "land_expand", + p.getName(), p.getName(), + "扩建领地: " + ac.name + " (+" + deltaSq + "㎡×" + expPrice + ")")) { + p.sendMessage("§c§l[防护] §f余额不足,扩建需 §e" + expCost + + " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡),请先充值再扩建"); + return true; + } + } + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + ac.createCost += expCost; // ★ 计入删除退费基数(后续删除按比例退) + saveAreaToDb(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格" + + (expCost > 0 + ? (",扣除 §e" + expCost + " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡)") + : "")); + return true; + } + + // ===== contract 收缩 ===== + if (sub.equals("contraction") + || sub.equals("contract")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + p.sendMessage("§c你不在任何防护区域内"); + return true; + } + // ★ 权限闸门:改边界必须是领地主/领地管理员 + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + p.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + int amount = 5; + if (args.length >= 2) { + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException ignored) {} + } + int minX = Math.min(ac.x1, ac.x2); + int maxX = Math.max(ac.x1, ac.x2); + int minZ = Math.min(ac.z1, ac.z2); + int maxZ = Math.max(ac.z1, ac.z2); + float yaw = p.getLocation().getYaw(); + String dir = yawToDir(yaw); + // ★ 收缩前置校验(2026-10-03):负数会变成"负数扩建"白嫖,一并挡掉 + if (amount <= 0) { + p.sendMessage("§c§l[防护] §f收缩格数必须为正整数"); + return true; + } + if (amount > 100000) { + p.sendMessage("§c§l[防护] §f单次收缩格数上限 100000,请分次收缩"); + return true; + } + long oldSqW = (long) maxX - minX + 1; + long oldSqL = (long) maxZ - minZ + 1; + long oldSq = oldSqW * oldSqL; + if (dir.equals("北")) minZ += amount; + else if (dir.equals("南")) maxZ -= amount; + else if (dir.equals("东")) maxX -= amount; + else if (dir.equals("西")) minX += amount; + if (minX > maxX || minZ > maxZ) { + p.sendMessage("§c收缩过度"); + return true; + } + long newSqW = (long) maxX - minX + 1; + long newSqL = (long) maxZ - minZ + 1; + long shrinkSq = oldSq - newSqW * newSqL; + + // ★ 收缩退款(2026-10-03):单价 × 收缩面积 × 删除退款比例 + // 基数不超过已付 createCost(历史免费地不凭空造钱),退款给领地主 + int shrinkPrice = landPricePerSqm(p); + int shrinkRatio = globalRefundRatioPerCent; + if (shrinkRatio > 100) shrinkRatio = 100; + long baseL = shrinkSq * (long) shrinkPrice; + if (baseL > ac.createCost) baseL = ac.createCost; + if (baseL < 0) baseL = 0; + int refundBase = (int) baseL; + int shrinkRefund = 0; + if (shrinkRatio > 0 && refundBase > 0) { + shrinkRefund = (int) Math.floor(refundBase * (shrinkRatio / 100.0)); + } + if (shrinkRefund > 0) { + BondManager sbm = plugin.getBonds(); + if (sbm == null) { + shrinkRefund = 0; + } else { + int afterBal = sbm.addBonds(ac.owner, shrinkRefund, "land_refund", + ac.owner, p.getName(), + "收缩领地退费: " + ac.name + " (-" + shrinkSq + "㎡)"); + if (afterBal < 0) { + plugin.getLogger().warning("[防护] 领地收缩退费失败(账户冻结?): " + + ac.owner + " +" + shrinkRefund); + shrinkRefund = 0; + } + } + } + ac.createCost -= refundBase; // ★ 收缩掉的部分不再计入删除退费基数 + if (ac.createCost < 0) ac.createCost = 0; + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaToDb(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格" + + (shrinkRefund > 0 + ? (",退还 §e" + shrinkRefund + " §f债券(" + + shrinkSq + "㎡×" + shrinkPrice + "/㎡×" + shrinkRatio + "%)") + : (shrinkRatio <= 0 ? "§7(退款比例 0,不退费)" : ""))); + return true; + } + + // ===== 删除(带60秒延迟自动删除 + 取消)===== + if (sub.equals("删除") || sub.equals("delete")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§e用法: /protect 删除 <名> [-f]"); + sender.sendMessage("§7-f = 管理员强制删除:跳过60秒倒计时与退费流程,立即删除"); + return true; + } + String areaName = args[1]; + // 严格匹配(写操作不做模糊归一化) + String resolved = resolveAreaNameStrict(areaName); + if (resolved != null) areaName = resolved; + if (!areas.containsKey(areaName)) { + sender.sendMessage("§c领地不存在: " + areaName); + return true; + } + // ★ 领主或管理员均可删除 + AreaProtection.AreaConfig delAc = areas.get(areaName); + boolean isOwner = delAc != null && samePlayer(p.getName(), delAc.owner); + if (!isAreaAdmin(sender) && !isOwner) { + sender.sendMessage("§c需要管理员权限或领地所有权"); + return true; + } + // ★ 管理员强制删除 -f:跳过60秒倒计时与退费流程,立即删除 + boolean forceDelete = false; + for (String argTok : args) { + if (argTok.equalsIgnoreCase("-f") || argTok.equalsIgnoreCase("--force")) { + forceDelete = true; + break; + } + } + // ★ 非管理员携带 -f:不报错、不提示,静默回退到普通60秒删除流程 + if (forceDelete && !isAreaAdmin(sender)) { + forceDelete = false; + } + if (forceDelete) { + if (delAc == null) { + sender.sendMessage("§c领地数据异常: " + areaName); + return true; + } + cancelPendingDelete(areaName); + deleteAreaFromDb(areaName); + areas.remove(areaName); + plugin.getLogger().warning("[防护] 管理员 " + sender.getName() + " 强制删除领地: " + areaName + + " (owner=" + delAc.owner + ", 未退费, 坐标 " + delAc.world + " " + + delAc.x1 + "," + delAc.z1 + " ~ " + delAc.x2 + "," + delAc.z2 + ")"); + sender.sendMessage("§a§l[防护] §f已强制删除领地 §e" + areaName + " §7(跳过倒计时与退费)"); + reportLandRemovalToAdmins("§6§l[防护-强制删除] §f" + sender.getName() + + " 强制删除领地 §e" + areaName + " §7(owner=" + delAc.owner + + ", 不退款) [" + delAc.world + " " + delAc.x1 + "," + delAc.z1 + + " ~ " + delAc.x2 + "," + delAc.z2 + "]"); + return true; + } + // ★ 检查是否已有待删除请求 + PendingDelete existing = pendingDeletes.get(areaName); + if (existing != null) { + // 已有等待期,提示取消方式 + long elapsed = System.currentTimeMillis() - existing.startTime; + long remaining = Math.max(0, (60000 - elapsed) / 1000); + sender.sendMessage("§e领地 §c" + areaName + " §e正在等待删除中,还剩 §c" + remaining + " §e秒后自动删除"); + sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消"); + return true; + } + // ★ 新建等待期:60秒后自动删除 + sender.sendMessage("§e⚠ 此操作不可逆!领地 §c" + areaName + " §e将在 §c60秒 §e后自动删除"); + sender.sendMessage("§7输入 §e/protect 取消删除 " + areaName + " §7可取消"); + // 也显示到领地列表(CLI/GUI取消按钮会自动刷新) + UUID uid = p.getUniqueId(); + String finalAreaName = areaName; + final String finalOperator = p.getName(); + long start = System.currentTimeMillis(); + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + pendingDeletes.remove(finalAreaName); + // ★ 60秒到期后自动删除领地 + if (areas.containsKey(finalAreaName)) { + // ★ 到期删除:按创建扣款比例退费(0%=关闭退费) + refundOnLandDelete(areas.get(finalAreaName), finalOperator, "删除领地退费: " + finalAreaName); + deleteAreaFromDb(finalAreaName); + areas.remove(finalAreaName); + plugin.getLogger().info("[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除"); + // 通知发起者 + Player online = Bukkit.getPlayer(uid); + if (online != null && online.isOnline()) { + online.sendMessage("§a§l[防护] §f领地 §e" + finalAreaName + " §f等待期结束,已自动删除"); + } + } + } + }.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick + pendingDeletes.put(areaName, new PendingDelete(areaName, p.getName(), start, task)); + return true; + } + + // ===== 取消删除 ===== + if (sub.equals("取消删除") || sub.equals("canceldelete")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§e用法: /protect 取消删除 <领地名>"); + return true; + } + String areaName = args[1]; + String resolved = resolveAreaNameStrict(areaName); + if (resolved != null) areaName = resolved; + // ★ 权限闸门:只有领地主/领地管理员能取消删除 + if (!requireLandOwnership(sender, areaName)) return true; + PendingDelete pd = pendingDeletes.remove(areaName); + if (pd == null) { + sender.sendMessage("§c没有待删除的领地: " + areaName); + return true; + } + pd.task.cancel(); + sender.sendMessage("§a§l[防护] §f已取消删除领地: §e" + areaName); + return true; + } + + // ===== 取消管理员改主 ===== + if (sub.equals("canceladminchange")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§e用法: /protect canceladminchange <领地名>"); + return true; + } + String areaName = args[1]; + String resolved = resolveAreaName(areaName); + if (resolved != null) areaName = resolved; + + // 检查领地是否存在 + AreaConfig ac = getLand(areaName); + if (ac == null) { + sender.sendMessage("§c领地不存在: " + areaName); + return true; + } + + // 检查是否是原主人 + if (!samePlayer(p.getName(), ac.owner)) { + sender.sendMessage("§c只有原主人才能撤回改主"); + return true; + } + + // 查找最近的管理员改主记录 + try { + if (dbConnection == null) { + sender.sendMessage("§c数据库未连接"); + return true; + } + PreparedStatement ps = dbConnection.prepareStatement( + "SELECT id, change_data FROM web_admin_changes WHERE change_type = 'owner_change' AND target_name = ? AND status = 'completed' ORDER BY id DESC LIMIT 1"); + ps.setString(1, areaName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + int changeId = rs.getInt("id"); + String changeDataStr = rs.getString("change_data"); + java.util.Map changeData = WebManager.parseJson(changeDataStr); + String newOwner = (String) changeData.get("new_owner"); + String oldOwner = (String) changeData.get("old_owner"); + + // 验证当前owner是否是newOwner(即已经被改主了) + if (!samePlayer(p.getName(), newOwner)) { + sender.sendMessage("§c当前所有者不是你,无法撤回"); + rs.close(); + ps.close(); + return true; + } + + // 回退owner + PreparedStatement updatePs = dbConnection.prepareStatement( + "UPDATE area_lands SET owner = ? WHERE name = ?"); + updatePs.setString(1, oldOwner); + updatePs.setString(2, areaName); + updatePs.executeUpdate(); + updatePs.close(); + + // 更新内存 + ac.owner = oldOwner; + + // 通知PHP回滚 + plugin.webManager.callbackOwnerChangeToPHP(changeId, false, "原主人撤回"); + + sender.sendMessage("§a§l[防护] §f已撤回领地 §e" + areaName + " §f的改主,恢复为 §a" + oldOwner); + plugin.getLogger().info("[防护] " + p.getName() + " 撤回了领地 " + areaName + " 的管理员改主"); + } else { + sender.sendMessage("§c没有可撤回的改主记录"); + } + rs.close(); + ps.close(); + } catch (Exception e) { + sender.sendMessage("§c撤回失败: " + e.getMessage()); + plugin.getLogger().warning("[防护] 撤回管理员改主失败: " + e.getMessage()); + } + return true; + } + + // ===== add 加白名单 ===== + if (sub.equals("add")) { + if (args.length == 2) { + // ★ 全服级白名单(等于全服免检),必须管理员 + if (!requireGlobalAdmin(sender, "全局白名单操作")) return true; + String name = args[1]; // ★ 保留原始拼写(2026-10-10 精确匹配) + if (globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 已在全局白名单中"); + return true; + } + globalPlayerWhitelist.add(name); + saveWhitelists(); + sender.sendMessage("§a已全局加白: " + name); + return true; + } + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别"); + return true; + } + String areaName = parsed[0]; + String playerName = parsed[1]; // ★ 保留原始拼写(2026-10-10 精确匹配) + // ★ 权限闸门:领地白名单须领地主/领地管理员,global 须插件管理员 + if (!requireLandOwnership(sender, areaName)) return true; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.add(playerName); + saveWhitelists(); + sender.sendMessage("§a已全局加白: " + + playerName); + } else { + Set wl = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap + .newKeySet()); + wl.add(playerName); + saveWhitelists(); + sender.sendMessage("§a已加白: " + + playerName + " → " + areaName); + } + return true; + } + sender.sendMessage("§c用法: /protect add " + + "<区域/玩家> <区域/玩家>"); + return true; + } + + // ===== remove 移除白名单 ===== + if (sub.equals("remove")) { + if (args.length == 2) { + // ★ 全服级白名单,必须管理员 + if (!requireGlobalAdmin(sender, "全局白名单操作")) return true; + String name = args[1]; // ★ 保留原始拼写;下方按三种历史格式尝试移除 + if (!globalPlayerWhitelist.contains(name) + && !globalPlayerWhitelist.contains(name.toLowerCase()) + && !globalPlayerWhitelist.contains(normName(name))) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); + return true; + } + globalPlayerWhitelist.remove(name); + globalPlayerWhitelist.remove(name.toLowerCase()); + globalPlayerWhitelist.remove(normName(name)); + saveWhitelists(); + sender.sendMessage("§a已从全局白名单移除: " + + name); + return true; + } + if (args.length == 3) { + String[] parsed = parseAreaAndTarget( + args[1], args[2]); + if (parsed == null) { + sender.sendMessage("§c无法识别"); + return true; + } + String areaName = parsed[0]; + String playerName = parsed[1]; // ★ 保留原始拼写;下方按三种历史格式尝试移除 + // ★ 权限闸门:领地白名单须领地主/领地管理员,global 须插件管理员 + if (!requireLandOwnership(sender, areaName)) return true; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.remove(playerName); + globalPlayerWhitelist.remove(playerName.toLowerCase()); + globalPlayerWhitelist.remove(normName(playerName)); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); + } else { + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + // 精确 / 旧版小写 / normName 三种历史格式都尝试移除 + wl.remove(playerName); + wl.remove(playerName.toLowerCase()); + wl.remove(normName(playerName)); + } + saveWhitelists(); + sender.sendMessage("§a已从 " + areaName + + " 移除: " + playerName); + } + return true; + } + sender.sendMessage("§c用法: /protect remove " + + "<区域/玩家> <区域/玩家>"); + return true; + } + + // ===== on/off/tempon 边框 ===== + if (sub.equals("on")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + p.sendMessage("§c你不在任何防护区域内"); + return true; + } + showBorder(p, ac, false); + p.sendMessage("§a§l[防护] §f已显示 " + + ac.name + " 边框"); + return true; + } + if (sub.equals("off")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + cancelBorder((Player) sender); + ((Player) sender).sendMessage( + "§a§l[防护] §f已关闭边框显示"); + return true; + } + if (sub.equals("tempon")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + p.sendMessage("§c你不在任何防护区域内"); + return true; + } + showBorder(p, ac, true); + p.sendMessage("§a§l[防护] §f临时显示 " + + ac.name + " 边框(15秒)"); + return true; + } + + // ===== shop 权限商店 ===== + if (sub.equals("shop")) { + String shopAction = args.length >= 2 ? args[1] : ""; + // shop create <领地> <价格> [时长秒] — 所有者创建权限售卖 + if (shopAction.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 4) { + sender.sendMessage("§e用法: /protect shop create <领地> <价格> [时长秒]"); + sender.sendMessage("§7时长默认86400秒(24h),最大86400秒"); + return true; + } + String areaName = resolveAreaName(args[2]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[2]); + return true; + } + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + int price; + try { price = Integer.parseInt(args[3]); } + catch (NumberFormatException e) { + sender.sendMessage("§c价格必须是整数"); + return true; + } + if (price < 1000) { + sender.sendMessage("§c最低价格为1000债券"); + return true; + } + int duration = 86400; // 默认24h + if (args.length >= 5) { + try { duration = Integer.parseInt(args[4]); } + catch (NumberFormatException e) { + sender.sendMessage("§c时长必须是整数(秒)"); + return true; + } + } + if (duration > 86400) { + sender.sendMessage("§c最大时长为86400秒(24小时)"); + return true; + } + // 获取land_id + int landId = getLandIdFromDb(areaName); + if (landId <= 0) { + sender.sendMessage("§c领地数据库记录不存在"); + return true; + } + // 插入商店记录 + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "INSERT INTO area_permission_shop (land_id, seller, permission, price, duration, created_at) " + + "VALUES (?, ?, 'visitor', ?, ?, ?)"); + stmt.setInt(1, landId); + stmt.setString(2, p.getName()); + stmt.setInt(3, price); + stmt.setInt(4, duration); + stmt.setLong(5, System.currentTimeMillis()); + stmt.executeUpdate(); + stmt.close(); + sender.sendMessage("§a§l[权限商店] §f已上架访客权限: §e" + areaName + " §f价格: §e" + price + "债券 §f时长: §e" + formatDuration(duration)); + } catch (SQLException e) { + sender.sendMessage("§c数据库错误: " + e.getMessage()); + } + return true; + } + + // shop remove — 下架 + if (shopAction.equals("remove")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 3) { + sender.sendMessage("§e用法: /protect shop remove <商品ID>"); + return true; + } + int shopId; + try { shopId = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + sender.sendMessage("§cID必须是整数"); + return true; + } + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "DELETE FROM area_permission_shop WHERE id = ? AND seller = ? AND status = 'active'"); + stmt.setInt(1, shopId); + stmt.setString(2, p.getName()); + int rows = stmt.executeUpdate(); + stmt.close(); + if (rows > 0) { + sender.sendMessage("§a§l[权限商店] §f已下架商品 #" + shopId); + } else { + sender.sendMessage("§c商品不存在或你不是卖家"); + } + } catch (SQLException e) { + sender.sendMessage("§c数据库错误: " + e.getMessage()); + } + return true; + } + + // shop list — 查看可购买的权限 + if (shopAction.equals("list") || shopAction.isEmpty()) { + try { + Statement stmt = dbConnection.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT s.id, l.name AS land_name, s.seller, s.price, s.duration " + + "FROM area_permission_shop s " + + "JOIN area_lands l ON s.land_id = l.id " + + "WHERE s.status = 'active' AND s.buyer = '' " + + "ORDER BY s.created_at DESC LIMIT 50"); + sender.sendMessage("§e§l==== 权限商店 ===="); + boolean found = false; + while (rs.next()) { + found = true; + int id = rs.getInt("id"); + String landName = rs.getString("land_name"); + String seller = rs.getString("seller"); + int price = rs.getInt("price"); + int dur = rs.getInt("duration"); + sender.sendMessage("§a#" + id + " §f" + landName + " §7卖家:" + seller + " §e" + price + "债券 §7" + formatDuration(dur)); + } + if (!found) { + sender.sendMessage("§7(暂无在售权限)"); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + sender.sendMessage("§c数据库错误: " + e.getMessage()); + } + return true; + } + + // shop buy — 购买权限 + if (shopAction.equals("buy")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 3) { + sender.sendMessage("§e用法: /protect shop buy <商品ID>"); + return true; + } + int shopId; + try { shopId = Integer.parseInt(args[2]); } + catch (NumberFormatException e) { + sender.sendMessage("§cID必须是整数"); + return true; + } + // 查询商品 + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT s.*, l.name AS land_name FROM area_permission_shop s " + + "JOIN area_lands l ON s.land_id = l.id " + + "WHERE s.id = ? AND s.status = 'active' AND s.buyer = ''"); + stmt.setInt(1, shopId); + ResultSet rs = stmt.executeQuery(); + if (!rs.next()) { + sender.sendMessage("§c商品不存在或已售出"); + stmt.close(); + return true; + } + String seller = rs.getString("seller"); + int price = rs.getInt("price"); + int duration = rs.getInt("duration"); + int landId = rs.getInt("land_id"); + String landName = rs.getString("land_name"); + rs.close(); + stmt.close(); + + // 不能买自己的(精确匹配:离线服里大小写不同的名字是不同玩家) + if (samePlayer(seller, p.getName())) { + sender.sendMessage("§c不能购买自己上架的权限"); + return true; + } + + // 扣款 + BondManager bm = plugin.getBonds(); + if (bm == null) { + sender.sendMessage("§c债券系统未初始化"); + return true; + } + if (bm.getBonds(p.getName()) < price) { + sender.sendMessage("§c债券不足,需要 §e" + price + " §c债券"); + return true; + } + if (!bm.deductBonds(p.getName(), price, "permission_shop_buy", "", "权限商店", "购买权限: " + landName)) { + sender.sendMessage("§c扣款失败"); + return true; + } + + // 分账:80%给卖家,20%税 + int ownerShare = price * 80 / 100; + int tax = price - ownerShare; + bm.addBonds(seller, ownerShare, "permission_shop_sell", p.getName(), "权限商店", "出售权限: " + landName); + + // 标记已售出 + PreparedStatement updateStmt = dbConnection.prepareStatement( + "UPDATE area_permission_shop SET status = 'sold', buyer = ?, bought_at = ? WHERE id = ?"); + updateStmt.setString(1, p.getName()); + updateStmt.setLong(2, System.currentTimeMillis()); + updateStmt.setInt(3, shopId); + updateStmt.executeUpdate(); + updateStmt.close(); + + // 写入访客权限(带过期时间) + long expiresAt = System.currentTimeMillis() + (long) duration * 1000; + PermRow existingPerm = findPermRow(landId, p.getName()); + if (existingPerm != null) { + // 已有行(含大小写/变种变体)→ 原地更新,杜绝 BINARY 唯一约束产生的重复行 + PreparedStatement permStmt = dbConnection.prepareStatement( + "UPDATE area_land_permissions SET role = 'visitor', permissions = 'shop_purchase', " + + "granted_at = ?, expires_at = ? WHERE id = ?"); + permStmt.setLong(1, System.currentTimeMillis()); + permStmt.setLong(2, expiresAt); + permStmt.setInt(3, existingPerm.id); + permStmt.executeUpdate(); + permStmt.close(); + } else { + PreparedStatement permStmt = dbConnection.prepareStatement( + "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at, expires_at) " + + "VALUES (?, ?, 'visitor', 'shop_purchase', ?, ?)"); + permStmt.setInt(1, landId); + permStmt.setString(2, p.getName()); + permStmt.setLong(3, System.currentTimeMillis()); + permStmt.setLong(4, expiresAt); + permStmt.executeUpdate(); + permStmt.close(); + } + + sender.sendMessage("§a§l[权限商店] §f购买成功!"); + sender.sendMessage("§f领地: §e" + landName + " §f时长: §e" + formatDuration(duration)); + sender.sendMessage("§f花费: §e" + price + " §f债券 (卖家得 §e" + ownerShare + " §f债券)"); + + // 通知卖家 + Player sellerPlayer = Bukkit.getPlayerExact(seller); + if (sellerPlayer != null) { + sellerPlayer.sendMessage("§a§l[权限商店] §f你的权限商品被 §e" + p.getName() + " §f购买,到账 §e" + ownerShare + " §f债券"); + } + } catch (SQLException e) { + sender.sendMessage("§c数据库错误: " + e.getMessage()); + } + return true; + } + + // shop my — 查看我购买的权限 + if (shopAction.equals("my")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + try { + PreparedStatement stmt = dbConnection.prepareStatement( + "SELECT p2.*, l.name AS land_name FROM area_land_permissions p2 " + + "JOIN area_lands l ON p2.land_id = l.id " + + "WHERE p2.player_name = ? AND p2.role = 'visitor' " + + "AND (p2.expires_at = 0 OR p2.expires_at > ?) " + + "ORDER BY p2.expires_at DESC"); + stmt.setString(1, p.getName()); + stmt.setLong(2, System.currentTimeMillis()); + ResultSet rs = stmt.executeQuery(); + sender.sendMessage("§e§l==== 我的领地权限 ===="); + boolean found = false; + long now = System.currentTimeMillis(); + while (rs.next()) { + found = true; + String landName = rs.getString("land_name"); + long expiresAt = rs.getLong("expires_at"); + if (expiresAt > 0) { + long remain = expiresAt - now; + sender.sendMessage("§a" + landName + " §7剩余: §e" + formatDuration((int)(remain / 1000))); + } else { + sender.sendMessage("§a" + landName + " §7永久"); + } + } + if (!found) { + sender.sendMessage("§7(暂无领地权限)"); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + sender.sendMessage("§c数据库错误: " + e.getMessage()); + } + return true; + } + + sender.sendMessage("§e用法: /protect shop "); + return true; + } + + // ===== info 显示当前领地权限信息 ===== + if (sub.equals("info")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + p.sendMessage("§c你不在任何防护区域内"); + return true; + } + PermissionLevel level = getPermissionLevel(p, ac); + String levelName = level != null ? level.name() : "无权限"; + p.sendMessage("§e§l==== 领地信息 ===="); + p.sendMessage("§f名称: §e" + ac.name); + p.sendMessage("§f所有者: §e" + (ac.owner != null && !ac.owner.isEmpty() ? ac.owner : "无")); + p.sendMessage("§f你的权限: §a" + levelName); + if (level == PermissionLevel.OWNER) { + p.sendMessage("§7(你是此领地的所有者)"); + } else if (level == PermissionLevel.ADMIN) { + p.sendMessage("§7(你是服务器管理员)"); + } else if (level == PermissionLevel.MEMBER) { + p.sendMessage("§7(你是此领地的已授权成员)"); + } else if (level == PermissionLevel.VISITOR) { + p.sendMessage("§7(你是此领地的访客)"); + } + // ★ info时展示领地边框粒子(5秒) + showBorderParticles(p, ac, 100); + return true; + } + + // ===== setowner 设置领地所有者(管理员命令)===== + if (sub.equals("setowner") || sub.equals("setowner")) { + if (!isAreaAdmin(sender)) { + sender.sendMessage("§c需要管理员权限"); + return true; + } + // If args.length < 3, try to auto-assign: use current location's land and set admin as owner + if (args.length < 3) { + if (!(sender instanceof Player)) { + sender.sendMessage("§e用法: /protect setowner <领地> <玩家>"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = getArea(p.getWorld().getName(), p.getLocation().getBlockX(), p.getLocation().getBlockY(), (int) p.getLocation().getZ()); + if (ac == null) { + sender.sendMessage("§c你不在任何领地内"); + return true; + } + if (ac.owner != null && !ac.owner.isEmpty()) { + sender.sendMessage("§c该领地已有所有者: " + ac.owner + "。用法: /protect setowner <领地> <新玩家>"); + return true; + } + ac.owner = p.getName(); + saveAreaToDb(ac); + cancelPendingDelete(ac.name); + sender.sendMessage("§a已自动将 §e" + ac.name + " §a的所有者设为 §e" + p.getName()); + return true; + } + String areaName = resolveAreaName(args[1]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[1]); + return true; + } + AreaConfig ac = areas.get(areaName); + ac.owner = args[2]; + saveAreaToDb(ac); + // ★ 删除冷却期间变更所有者 → 自动取消删除 + cancelPendingDelete(areaName); + sender.sendMessage("§a已设置 §e" + areaName + " §a的所有者为 §e" + args[2]); + return true; + } + + // ===== setadmin 设置领地管理员(限指定领地,DB role='admin',不再授予全局标签)===== + if (sub.equals("setadmin")) { + if (!isAreaAdmin(sender)) { + sender.sendMessage("§c需要管理员权限"); + return true; + } + if (args.length < 3) { + sender.sendMessage("§e用法: /protect setadmin <领地名> <玩家>"); + return true; + } + String landName = args[1]; + String targetName = args[2]; + AreaConfig ac = getLand(landName); + if (ac == null) { + sender.sendMessage("§c领地不存在: " + landName); + return true; + } + Player target = Bukkit.getPlayerExact(targetName); + if (target == null) { + sender.sendMessage("§c玩家不在线: " + targetName); + return true; + } + if (isLandAdmin(landName, targetName)) { + sender.sendMessage("§e" + targetName + " §c已经是该领地管理员"); + return true; + } + setLandAdmin(landName, targetName, true); + sender.sendMessage("§a已将 §e" + targetName + " §a设为领地 §b" + landName + + " §a的管理员(仅在该领地生效,不跨领地)"); + target.sendMessage("§6[区域防护] §a你已被设为领地 " + landName + " 的管理员"); + return true; + } + + // ===== unsetadmin 移除领地管理员(限指定领地)===== + if (sub.equals("unsetadmin")) { + if (!isAreaAdmin(sender)) { + sender.sendMessage("§c需要管理员权限"); + return true; + } + if (args.length < 3) { + sender.sendMessage("§e用法: /protect unsetadmin <领地名> <玩家>"); + return true; + } + String landName = args[1]; + String targetName = args[2]; + AreaConfig ac = getLand(landName); + if (ac == null) { + sender.sendMessage("§c领地不存在: " + landName); + return true; + } + Player target = Bukkit.getPlayerExact(targetName); + if (target == null) { + sender.sendMessage("§c玩家不在线: " + targetName); + return true; + } + boolean hadRole = isLandAdmin(landName, targetName); + // 兼容旧版:旧 setadmin 会给玩家全局管理员ScoreboardTag,造成跨领地越权; + // 撤销时一并清除该遗留标签,确保彻底收回全局权限 + boolean hadTag = false; + ConfigManager cfg = plugin.getConfigMgr(); + String tag = cfg != null ? cfg.areaProtectAdminTag : null; + if (tag != null && !tag.isEmpty() && target.getScoreboardTags().contains(tag)) { + hadTag = true; + target.removeScoreboardTag(tag); + } + if (hadRole) setLandAdmin(landName, targetName, false); + if (!hadRole && !hadTag) { + sender.sendMessage("§e" + targetName + " §c不是该领地管理员,也未持有全局管理员标签"); + return true; + } + sender.sendMessage("§a已移除 §e" + targetName + " §a在领地 §b" + landName + " §a的管理员权限" + + (hadTag ? "(并清除其遗留的全局管理员标签)" : "")); + target.sendMessage("§6[区域防护] §c你在领地 " + landName + " 的管理员权限已被移除" + + (hadTag ? "(全局管理员标签已清除)" : "")); + return true; + } + + // ===== addvisitor 添加访客(所有者+管理员)===== + if (sub.equals("addvisitor")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + String[] parsed = parseAreaAndTarget(args.length >= 3 ? args[1] : null, args.length >= 3 ? args[2] : null); + if (parsed == null || args.length < 2) { + // 尝试获取玩家当前所在领地 + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + sender.sendMessage("§c用法: /protect addvisitor <领地> <玩家>"); + return true; + } + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + // ★ 删除冷却期间添加成员 → 自动取消删除 + if (hasPendingDelete(ac.name)) { + cancelPendingDelete(ac.name); + p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); + } + if (args.length < 2) { + sender.sendMessage("§c用法: /protect addvisitor <玩家>"); + return true; + } + // ★ 领地主不能作为成员添加 + if (samePlayer(args[1], ac.owner)) { + sender.sendMessage("§c领地所有者本身就是领地主,无需添加为成员"); + return true; + } + // ★ 校验玩家是否在login.db中注册 + { + DatabaseManager dbMgr = plugin.getDb(); + if (dbMgr != null && !dbMgr.userExists(args[1])) { + sender.sendMessage("§c§l[添加成员] §f玩家 " + args[1] + " 不存在(未在login.db中注册)"); + 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的访客"); + return true; + } + String areaName = parsed[0]; + String playerName = parsed[1]; + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + // ★ 领地主不能作为成员添加 + if (samePlayer(playerName, ac.owner)) { + sender.sendMessage("§c领地所有者本身就是领地主,无需添加为成员"); + return true; + } + // ★ 校验玩家是否在login.db中注册 + { + DatabaseManager dbMgr = plugin.getDb(); + if (dbMgr != null && !dbMgr.userExists(playerName)) { + sender.sendMessage("§c§l[添加成员] §f玩家 " + playerName + " 不存在(未在login.db中注册)"); + return true; + } + } + // ★ 删除冷却期间添加成员 → 自动取消删除 + if (hasPendingDelete(areaName)) { + 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的访客"); + return true; + } + + // ===== removevisitor / removemember 移除访客/成员 ===== + if (sub.equals("removevisitor") || sub.equals("removemember")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + String[] parsed = parseAreaAndTarget(args.length >= 3 ? args[1] : null, args.length >= 3 ? args[2] : null); + if (parsed == null || args.length < 2) { + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + sender.sendMessage("§c用法: /protect removevisitor <领地> <玩家>"); + return true; + } + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + // ★ 删除冷却期间移除成员 → 自动取消删除 + if (hasPendingDelete(ac.name)) { + cancelPendingDelete(ac.name); + p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); + } + if (args.length < 2) { + sender.sendMessage("§c用法: /protect removevisitor <玩家>"); + return true; + } + removePlayerFromAreaWhitelist(ac.name, args[1]); + sender.sendMessage("§a已移除 §e" + args[1] + " §a的 §e" + ac.name + " §a访客权限"); + return true; + } + String areaName = parsed[0]; + String playerName = parsed[1]; + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + // ★ 删除冷却期间移除成员 → 自动取消删除 + if (hasPendingDelete(areaName)) { + cancelPendingDelete(areaName); + p.sendMessage("§e§l[防护] §f检测到成员变更,已自动取消领地删除"); + } + removePlayerFromAreaWhitelist(areaName, playerName); + sender.sendMessage("§a已移除 §e" + playerName + " §a的 §e" + areaName + " §a访客权限"); + return true; + } + + // ===== listvisitors 列出访客 ===== + if (sub.equals("listvisitors")) { + String areaName = args.length >= 2 ? resolveAreaName(args[1]) : null; + if (areaName == null && sender instanceof Player) { + Player p = (Player) sender; + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null) areaName = ac.name; + } + if (areaName == null) { + sender.sendMessage("§c用法: /protect listvisitors <领地>"); + return true; + } + Set visitors = areaPlayerWhitelist.get(areaName); + sender.sendMessage("§e§l==== " + areaName + " 访客列表 ===="); + if (visitors == null || visitors.isEmpty()) { + sender.sendMessage("§7(无访客)"); + } else { + for (String v : visitors) { + sender.sendMessage("§f- §a" + v); + } + } + return true; + } + + // ===== transfer / transfeland 转让领地 ===== + if (sub.equals("transfer") || sub.equals("transfeland")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§e用法: /protect transfer <领地> <新所有者>"); + return true; + } + // ★ 如果只给了领地名,进入交互式输入流程 + if (args.length == 2) { + String areaName = resolveAreaName(args[1]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[1]); + return true; + } + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + // 记录待输入状态 + pendingTransferInput.put(p.getUniqueId(), new String[]{areaName}); + p.sendMessage("§e请在聊天栏输入新所有者名称(输入 §c取消§e)"); + p.sendMessage("§7§l───────────────────────────────"); + return true; + } + String areaName = resolveAreaName(args[1]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[1]); + return true; + } + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + String newOwner = args[2]; + String oldOwner = ac.owner; + + // ★ 验证新所有者名格式 + if (!newOwner.matches("^[a-zA-Z0-9_]{3,16}$")) { + sender.sendMessage("§c玩家名格式无效:仅允许英文字母、数字和下划线(3-16位)"); + return true; + } + if (samePlayer(newOwner, oldOwner)) { + sender.sendMessage("§c不能转让给自己"); + return true; + } + + // ★ 验证新所有者是否存在(login.db) + DatabaseManager dbMgr = plugin.getDb(); + if (dbMgr != null && !dbMgr.userExists(newOwner)) { + sender.sendMessage("§c玩家 §e" + newOwner + " §c尚未注册"); + return true; + } + + // ★ 验证玩家注册时间必须超过5分钟(防止注册秒退玩家接收领地) + if (dbMgr != null) { + long registerTime = 0; + try { + Object regTimeObj = dbMgr.getField(newOwner, "register_time"); + if (regTimeObj instanceof Number) { + // ★ 第十一批:秒/毫秒混存归一化 + registerTime = DurationParser.toMs( + ((Number) regTimeObj).longValue()); + } + } catch (Exception e) { + // 忽略异常 + } + if (registerTime > 0) { + long now = System.currentTimeMillis(); + long fiveMinutesMs = 5 * 60 * 1000; + if (now - registerTime < fiveMinutesMs) { + long minutesLeft = (fiveMinutesMs - (now - registerTime)) / 60000; + sender.sendMessage("§c玩家 §e" + newOwner + " §c注册时间不足5分钟(还差" + minutesLeft + "分钟)"); + return true; + } + } + } + + // ★ 执行转让 + ac.owner = newOwner; + saveAreaToDb(ac); + + // ★ 获取权限快照 + 追踪cooldown + if (plugin.webManager != null) { + String snapshot = getLandPermissionsSnapshot(areaName); + long expiresAt = System.currentTimeMillis() + 60000; // 60秒cooldown + WebManager.TransferInfo info = new WebManager.TransferInfo( + areaName, oldOwner, newOwner, expiresAt, 0, 0, snapshot + ); + plugin.webManager.trackTransfer(areaName, info); + // 通知PHP同步 + plugin.webManager.requestImmediateLandSync(); + } + + sender.sendMessage("§a已将 §e" + areaName + " §a从 §e" + (oldOwner != null ? oldOwner : "无") + " §a转让给 §e" + newOwner + "§a,§e60秒冷却期内可取消"); + return true; + } + + // ===== canceltransfer 取消过户 ===== + if (sub.equals("canceltransfer")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + if (args.length < 2) { + sender.sendMessage("§e用法: /protect canceltransfer <领地>"); + return true; + } + String areaName = resolveAreaName(args[1]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[1]); + return true; + } + AreaConfig ac = areas.get(areaName); + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + + // ★ 查找并取消进行中的过户 + if (plugin.webManager != null && plugin.webManager.cancelTransfer(areaName, p.getName())) { + sender.sendMessage("§a过户已取消,领地 §e" + areaName + " §a已恢复"); + } else { + sender.sendMessage("§c没有可取消的过户(可能已过期或不存在)"); + } + return true; + } + + // ===== settp 设置领地传送点 ===== + if (sub.equals("settp") || sub.equals("setwarp")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = null; + // ★ 支持指定领地名:/protect settp <领地名> + if (args.length >= 2) { + String targetName = args[1]; + String resolved = resolveAreaName(targetName); + if (resolved != null) targetName = resolved; + ac = areas.get(targetName); + if (ac == null) { + sender.sendMessage("§c领地不存在: " + targetName); + return true; + } + } else { + // 找到玩家所在的领地 + ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + sender.sendMessage("§c你不在任何领地内,请指定领地名: /protect settp <领地名>"); + return true; + } + } + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + sender.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + ac.warpX = p.getLocation().getX(); + ac.warpY = p.getLocation().getY(); + ac.warpZ = p.getLocation().getZ(); + ac.warpYaw = p.getLocation().getYaw(); + ac.warpPitch = p.getLocation().getPitch(); + ac.warpWorld = p.getWorld().getName(); + saveAreaToDb(ac); + cancelPendingDelete(ac.name); + sender.sendMessage("§a§l[防护] §f已将 §e" + ac.name + " §f的传送点设置为当前坐标"); + return true; + } + + // ===== tp 传送到领地传送点 ===== + if (sub.equals("tp") || sub.equals("warp")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + AreaConfig ac = null; + if (args.length >= 2) { + String areaName = resolveAreaName(args[1]); + if (areaName == null) { + sender.sendMessage("§c领地不存在: " + args[1]); + return true; + } + ac = areas.get(areaName); + } else { + // 传送到当前所在领地的传送点 + ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + sender.sendMessage("§c你不在任何领地内,请指定领地名: /protect tp <领地名>"); + return true; + } + } + if (ac == null) { + sender.sendMessage("§c领地不存在"); + return true; + } + // ★ 传送权限检查:领地主/插件管理员/领地管理员 无条件允许;其余需授权 + if (!canTeleportToLand(p, ac)) { + sender.sendMessage("§c你没有权限传送到该领地,需领地主或管理员授权(/protect cli memberperm " + ac.name + ")"); + return true; + } + // 检查传送点是否已设置 + if (ac.warpWorld == null || ac.warpWorld.isEmpty()) { + // 没有传送点,传送到领地中心 + int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2; + int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2; + World w = Bukkit.getWorld(ac.world); + plugin.getLogger().info("[防护-TP] ★★★ 无传送点,传送到领地中心: 区域=" + ac.name + + " 世界=" + ac.world + " 中心=(" + cx + "," + cz + ") worldObj=" + (w != null ? "OK" : "NULL")); + if (w == null) { + sender.sendMessage("§c世界不存在: " + ac.world); + return true; + } + // 找到安全位置:从最高向下找第一个上方有2格空气、下方有实体的方块 + Location safeLoc = null; + for (int y = w.getMaxHeight(); y >= w.getMinHeight(); y--) { + Location check = new Location(w, cx, y, cz); + // 脚下必须有实体方块 + if (!check.getBlock().getType().isSolid()) continue; + // 脚上和头顶至少2格空气(站立空间) + if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue; + if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue; + safeLoc = check.clone().add(0, 1, 0); // 站在方块上表面+1的位置 + break; + } + if (safeLoc != null) { + p.teleport(safeLoc); + sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(未设置传送点)"); + } else { + sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)"); + } + } else { + World w = Bukkit.getWorld(ac.warpWorld); + plugin.getLogger().info("[防护-TP] ★★★ 有传送点: 区域=" + ac.name + + " warpWorld=" + ac.warpWorld + " warpPos=(" + ac.warpX + "," + ac.warpY + "," + ac.warpZ + ")" + + " worldObj=" + (w != null ? "OK" : "NULL")); + if (w == null) { + // 传送点世界不存在,回退到领地中心传送 + plugin.getLogger().warning("[防护-TP] 传送点世界不存在,回退到领地中心: " + ac.warpWorld); + int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2; + int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2; + World fallback = Bukkit.getWorld(ac.world); + if (fallback == null) { + sender.sendMessage("§c世界不存在: " + ac.world); + return true; + } + // 找到安全位置 + Location safeLoc = null; + for (int y = fallback.getMaxHeight(); y >= fallback.getMinHeight(); y--) { + Location check = new Location(fallback, cx, y, cz); + if (!check.getBlock().getType().isSolid()) continue; + if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue; + if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue; + safeLoc = check.clone().add(0, 1, 0); + break; + } + if (safeLoc != null) { + p.teleport(safeLoc); + sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(传送点世界不存在)"); + } else { + sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)"); + } + return true; + } + Location warp = new Location(w, ac.warpX, ac.warpY, ac.warpZ, ac.warpYaw, ac.warpPitch); + p.teleport(warp); + sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f传送点"); + } + return true; + } + + showHelp(sender); + return true; + } + + /** + * ★ 删除领地时按创建扣款比例退费 + * 退费比例读取 area_config.refund_ratio_per_cent: + * 0 = 关闭退费(不退) + * 100 = 全额退款(原价 100%) + * 其他= 原价 × 比例% + * 债券走 BondManager.addBonds(记 land_refund 流水) + */ + private void refundOnLandDelete(AreaConfig ac, String operator, String reason) { + if (ac == null) return; + int ratio = globalRefundRatioPerCent; + if (ratio <= 0) return; // 0 = 关闭退费 + if (ratio > 100) ratio = 100; // 上限 100% + if (ac.createCost <= 0) return; // 未扣款(历史数据/管理员创建)不退 + int refund = (int) Math.floor(ac.createCost * (ratio / 100.0)); + if (refund <= 0) return; + BondManager bm = plugin.getBonds(); + if (bm == null) return; + String op = (operator == null || operator.isEmpty()) ? "system" : operator; + int after = bm.addBonds(ac.owner, refund, "land_refund", ac.owner, op, reason); + if (after < 0) { + plugin.getLogger().warning("[防护] 领地退费失败(账户冻结?): " + ac.owner + " +" + refund); + return; + } + plugin.getLogger().info("[防护] 领地 " + ac.name + " 删除退费: " + ac.owner + + " +" + refund + " (原价 " + ac.createCost + " × " + ratio + "%)"); + Player ownerOnline = Bukkit.getPlayerExact(ac.owner); + if (ownerOnline != null && ownerOnline.isOnline()) { + ownerOnline.sendMessage("§a§l[防护] §f领地 §e" + ac.name + " §f已删除,退还债券 §e" + refund + + " §7(创建原价 " + ac.createCost + " × " + ratio + "%)"); + } + } + + /** + * ★ 向在线的插件管理员 & OP 上报领地强制删除信息(含坐标) + */ + private void reportLandRemovalToAdmins(String message) { + int sent = 0; + for (Player online : Bukkit.getOnlinePlayers()) { + if (online.isOp() || isAreaAdmin(online)) { + online.sendMessage(message); + sent++; + } + } + plugin.getLogger().info("[防护] 已上报 " + sent + " 名在线管理员: " + message.replaceAll("§.", "")); + } + + /** + * ★ 扩建 / 收缩单价:与创建同一口径(用户组可覆盖,缺省为创建单价 globalCreatePricePerSqm)。 + * 扩建收费、收缩退费都按这个单价算,保证"创建一次扣费、扩建不再白嫖"。 + */ + private int landPricePerSqm(Player p) { + UserGroupManager ug = plugin.getUserGroup(); + return (ug != null) + ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) + : globalCreatePricePerSqm; + } + + // ==================== 领地面积审批流(2026-09-29) ==================== + + /** 待审批的面积超限请求(expand=扩建提案 / create=创建提案) */ + private static class PendingExpand { + String type; // "expand" | "create" + String landName; // 领地名(create 为拟定名) + String requester; // 申请人 + String world; + int oldX1, oldX2, oldZ1, oldZ2; // expand=原边界快照 / create=选区 + int newX1, newX2, newZ1, newZ2; // 提案边界(create 时同选区) + int amount; + String dir; + long deltaSq, newW, newL, oldW, oldL; // 展示用(expand) + int area, cost; // 展示用(create,申请时价格快照) + long createdAt; + } + + /** 领地名(小写) → 待审批请求;同一领地重复申请以最新为准 */ + private final Map pendingExpands = new ConcurrentHashMap<>(); + + private PendingExpand findPending(String key) { + PendingExpand byLand = pendingExpands.get(key.toLowerCase()); + if (byLand != null) return byLand; + PendingExpand found = null; + for (PendingExpand pe : pendingExpands.values()) { + if (pe.requester.equalsIgnoreCase(key)) { + if (found != null) return null; // 多个匹配 → 要求用领地名精确指定 + found = pe; + } + } + return found; + } + + /** 申请人名下待审批条数(用于"多条申请请用领地名"的精确提示) */ + private int countPendingByRequester(String name) { + int n = 0; + for (PendingExpand pe : pendingExpands.values()) { + if (pe.requester.equalsIgnoreCase(name)) n++; + } + return n; + } + + /** 待审批提案有效期:24 小时(超时自动作废,防陈旧提案被误批) */ + private static final long PENDING_EXPIRE_MS = 24L * 60 * 60 * 1000L; + + /** 惰性清理超时提案(读取待审批列表前调用) */ + private void pruneExpiredPending() { + if (pendingExpands.isEmpty()) return; + long now = System.currentTimeMillis(); + for (java.util.Iterator> it = + pendingExpands.entrySet().iterator(); it.hasNext();) { + Map.Entry e = it.next(); + PendingExpand pe = e.getValue(); + if (now - pe.createdAt <= PENDING_EXPIRE_MS) continue; + it.remove(); + Player req = Bukkit.getPlayerExact(pe.requester); + if (req != null && req.isOnline()) { + req.sendMessage("§c§l[防护] §f你的领地 §e" + pe.landName + " §f" + + ("expand".equals(pe.type) ? "扩建" : "创建") + + "申请已超时(24小时)自动作废,请重新提交"); + } + plugin.getLogger().info("[领地审批] 提案超时作废: " + pe.landName + + " (" + pe.type + ", 申请人 " + pe.requester + ")"); + } + } + + /** ★ 扩建超限 → 登记审批请求 + 通知申请人与在线管理员 */ + private void registerPendingExpand(Player p, AreaConfig ac, + int nx1, int nx2, int nz1, int nz2, + int amount, String dir, long deltaSq, long newW, long newL, + long oldW, long oldL) { + PendingExpand pe = new PendingExpand(); + pe.type = "expand"; + pe.landName = ac.name; + pe.requester = p.getName(); + pe.world = ac.world; + pe.oldX1 = Math.min(ac.x1, ac.x2); + pe.oldX2 = Math.max(ac.x1, ac.x2); + pe.oldZ1 = Math.min(ac.z1, ac.z2); + pe.oldZ2 = Math.max(ac.z1, ac.z2); + pe.newX1 = nx1; pe.newX2 = nx2; pe.newZ1 = nz1; pe.newZ2 = nz2; + pe.amount = amount; + pe.dir = dir; + pe.deltaSq = deltaSq; pe.newW = newW; pe.newL = newL; + pe.oldW = oldW; pe.oldL = oldL; + pe.createdAt = System.currentTimeMillis(); + pendingExpands.put(ac.name.toLowerCase(), pe); + + p.sendMessage("§e§l[防护] §f扩建超出免审上限(本次 +§e" + deltaSq + + "§f㎡,上限 §e" + globalMaxFreeAreaSqm + "§f㎡;新尺寸 §e" + + newW + "×" + newL + "§f,边长上限 §e" + globalMaxLandSideBlocks + + "§f),已提交管理员现场审批。"); + p.sendMessage("§7管理员需进入该领地后执行 §a/protect accept " + ac.name + " §7或 §c/protect deny " + ac.name); + long peCostL = deltaSq * (long) landPricePerSqm(p); + p.sendMessage("§7审批通过时按创建单价 §e" + landPricePerSqm(p) + + "/㎡ §f收取 §e" + (peCostL > Integer.MAX_VALUE ? "(超额,请分次扩建)" : String.valueOf(peCostL)) + + " §f债券"); + reportLandRemovalToAdmins("§6§l[领地审批] §f玩家 §e" + p.getName() + + " §f申请扩建领地 §e" + ac.name + " §7(+" + deltaSq + "㎡, 向" + dir + amount + "格, 新尺寸 " + + newW + "×" + newL + ") §f——管理员到现场后 §a/protect accept " + ac.name + "§f/§cdeny"); + plugin.getLogger().info("[领地审批] 扩建申请: " + p.getName() + " → " + ac.name + + " +" + deltaSq + "㎡ (上限" + globalMaxFreeAreaSqm + ") 新尺寸 " + newW + "×" + newL); + } + + /** ★ 创建超限 → 登记审批请求(不扣费,审批通过时按当时价格扣) */ + private void registerPendingCreate(Player p, String name, + Location cl1, Location cl2, int cw, int cl, int cArea, int cCost) { + PendingExpand pe = new PendingExpand(); + pe.type = "create"; + pe.landName = name; + pe.requester = p.getName(); + pe.world = cl1.getWorld().getName(); + pe.oldX1 = cl1.getBlockX(); pe.oldZ1 = cl1.getBlockZ(); + pe.oldX2 = cl2.getBlockX(); pe.oldZ2 = cl2.getBlockZ(); + pe.newX1 = Math.min(cl1.getBlockX(), cl2.getBlockX()); + pe.newX2 = Math.max(cl1.getBlockX(), cl2.getBlockX()); + pe.newZ1 = Math.min(cl1.getBlockZ(), cl2.getBlockZ()); + pe.newZ2 = Math.max(cl1.getBlockZ(), cl2.getBlockZ()); + pe.area = cArea; + pe.cost = cCost; + pe.newW = cw; pe.newL = cl; + pe.createdAt = System.currentTimeMillis(); + pendingExpands.put(name.toLowerCase(), pe); + + p.sendMessage("§e§l[防护] §f创建选区 §e" + name + " §f(" + cw + "×" + cl + + " = " + cArea + "㎡)超出免审上限(单次 " + globalMaxFreeAreaSqm + + "㎡ / 边长 " + globalMaxLandSideBlocks + "格),已提交管理员现场审批,暂未扣费。"); + p.sendMessage("§7管理员需进入选区后执行 §a/protect accept " + name + " §7或 §c/protect deny " + name); + reportLandRemovalToAdmins("§6§l[领地审批] §f玩家 §e" + p.getName() + + " §f申请创建领地 §e" + name + " §7(" + cw + "×" + cl + " = " + cArea + + "㎡) §f——管理员到选区后 §a/protect accept " + name + "§f/§cdeny"); + plugin.getLogger().info("[领地审批] 创建申请: " + p.getName() + " → " + name + + " " + cw + "×" + cl + " (" + cArea + "㎡)"); + } + + /** ★ 审批命令核心:/protect accept|deny <领地名|申请人> */ + private void handleApproval(Player adm, String[] args, boolean accept) { + pruneExpiredPending(); + if (!isAreaAdmin(adm)) { + adm.sendMessage("§c§l[审批] §f需要插件管理员权限"); + return; + } + if (args.length < 2) { + adm.sendMessage("§e用法: /protect " + (accept ? "accept" : "deny") + " <领地名|申请人>"); + if (pendingExpands.isEmpty()) { + adm.sendMessage("§7当前没有待审批的面积申请"); + return; + } + adm.sendMessage("§e==== 待审批列表 ===="); + for (PendingExpand pe : pendingExpands.values()) { + adm.sendMessage((("expand".equals(pe.type)) ? "§e[扩建] " : "§e[创建] ") + + "§f" + pe.landName + " §7申请人=" + pe.requester + + ("expand".equals(pe.type) + ? (" +" + pe.deltaSq + "㎡ 新尺寸" + pe.newW + "×" + pe.newL) + : (" " + pe.newW + "×" + pe.newL + " = " + pe.area + "㎡"))); + } + adm.sendMessage("§7审批必须亲自站到领地/选区范围内"); + return; + } + PendingExpand pe = findPending(args[1]); + if (pe == null) { + if (countPendingByRequester(args[1]) > 1) { + adm.sendMessage("§c§l[审批] §f申请人 §e" + args[1] + + " §f名下有多条申请,请用领地名指定"); + } else { + adm.sendMessage("§c§l[审批] §f没有找到待审批的申请: §e" + args[1]); + } + return; + } + + // ★ 现场校验:审批的管理员必须在领地/选区范围内 + Location admLoc = adm.getLocation(); + boolean inRange; + String rangeDesc; + if ("expand".equals(pe.type)) { + AreaConfig ac = areas.get(pe.landName); + AreaConfig here = getArea(admLoc.getWorld().getName(), + admLoc.getBlockX(), admLoc.getBlockY(), admLoc.getBlockZ()); + inRange = (ac != null && here != null && here.name.equalsIgnoreCase(pe.landName)); + rangeDesc = "领地 §e" + pe.landName; + } else { + inRange = admLoc.getWorld().getName().equals(pe.world) + && admLoc.getBlockX() >= Math.min(pe.newX1, pe.newX2) + && admLoc.getBlockX() <= Math.max(pe.newX1, pe.newX2) + && admLoc.getBlockZ() >= Math.min(pe.newZ1, pe.newZ2) + && admLoc.getBlockZ() <= Math.max(pe.newZ1, pe.newZ2); + rangeDesc = "创建选区 §e" + pe.landName; + } + if (!inRange) { + adm.sendMessage("§c§l[审批] §f审批" + (accept ? "通过" : "驳回") + + "需要你亲自站在" + rangeDesc + " §f范围内(当前不在)"); + return; + } + + Player requester = Bukkit.getPlayerExact(pe.requester); + + if (!accept) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§a§l[审批] §f已驳回 §e" + pe.landName + " §f的" + + ("expand".equals(pe.type) ? "扩建" : "创建") + "申请"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§c§l[防护] §f你的领地 §e" + pe.landName + " §f" + + ("expand".equals(pe.type) ? "扩建" : "创建") + "申请被管理员驳回"); + } + plugin.getLogger().info("[领地审批] 驳回: " + pe.landName + " (申请人 " + pe.requester + + ", 审批人 " + adm.getName() + ")"); + return; + } + + if ("expand".equals(pe.type)) { + // 边界已被改动的提案作废(防申请后领地被转让/缩放再套用旧提案) + AreaConfig ac = areas.get(pe.landName); + if (ac == null) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f领地 §e" + pe.landName + " §f已不存在,申请作废"); + return; + } + int curX1 = Math.min(ac.x1, ac.x2), curX2 = Math.max(ac.x1, ac.x2); + int curZ1 = Math.min(ac.z1, ac.z2), curZ2 = Math.max(ac.z1, ac.z2); + if (curX1 != pe.oldX1 || curX2 != pe.oldX2 + || curZ1 != pe.oldZ1 || curZ2 != pe.oldZ2) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f领地 §e" + pe.landName + + " §f边界在申请后已发生变化,提案作废,请申请人重新提交"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§c§l[防护] §f你的扩建申请作废:领地边界已变化,请重新 /protect expand"); + } + return; + } + // ★ 扩建审批扣费(2026-10-03):审批时按创建单价 × 新增面积收款 + UserGroupManager ePugm = plugin.getUserGroup(); + int ePrice = (ePugm != null) + ? ePugm.getPlayerLandPricePerSqm(pe.requester, globalCreatePricePerSqm) + : globalCreatePricePerSqm; + long eCostL = pe.deltaSq * (long) ePrice; + if (eCostL > Integer.MAX_VALUE) { + adm.sendMessage("§c§l[审批] §f该扩建费用超出上限,无法扣费,申请保留待处理"); + return; + } + int eCost = (int) eCostL; + int eCharged = 0; + if (eCost > 0) { + BondManager ebm = plugin.getBonds(); + if (ebm == null || !ebm.deductBonds(pe.requester, eCost, "land_expand", + pe.requester, pe.requester, + "管理员审批扩建领地: " + pe.landName + " (+" + pe.deltaSq + "㎡×" + ePrice + ")")) { + adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester + + " §f余额不足(需 §e" + eCost + "§f),无法扣费,申请保留待处理"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§c§l[防护] §f你的扩建申请因余额不足(需 " + eCost + + " 债券)未能完成,请充值后请管理员重新审批"); + } + return; + } + eCharged = eCost; + } + ac.x1 = pe.newX1; ac.x2 = pe.newX2; + ac.z1 = pe.newZ1; ac.z2 = pe.newZ2; + ac.createCost += eCharged; // ★ 计入删除退费基数 + saveAreaToDb(ac); + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§a§l[审批] §f已批准 §e" + pe.landName + " §f扩建:+§e" + + pe.deltaSq + "§f㎡(" + pe.oldW + "×" + pe.oldL + " → §e" + + pe.newW + "×" + pe.newL + "§f),向申请人扣除 §e" + eCharged + " §f债券"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a§l[防护] §f你的领地 §e" + pe.landName + + " §f扩建申请已被管理员批准,已生效,扣除 " + eCharged + " 债券"); + } + plugin.getLogger().info("[领地审批] 扩建通过: " + pe.landName + " (申请人 " + pe.requester + + ", 审批人 " + adm.getName() + ") +" + pe.deltaSq + "㎡"); + } else { + // ---- 创建审批通过:重名/上限/扣费 重新校验后落库 ---- + if (areaNameExists(pe.landName)) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f领地名 §e" + pe.landName + " §f已被占用,申请作废"); + return; + } + // ★ 第57批续21 审批路径也套空间重叠闸门:申请人具备主领地管理身份(领地主/管理员/授权管理员) + // 才能在重叠区创建子领地;审批管理员本人申请时 isAreaAdmin=true 恒合格。 + int aN1x = Math.min(pe.newX1, pe.newX2), aN2x = Math.max(pe.newX1, pe.newX2); + int aN1z = Math.min(pe.newZ1, pe.newZ2), aN2z = Math.max(pe.newZ1, pe.newZ2); + AreaConfig apMain = findContainingMainLand(pe.world, aN1x, aN1z, aN2x, aN2z); + boolean apIsSub = apMain != null; + Player apReqOnline = Bukkit.getPlayerExact(pe.requester); + if (apIsSub) { + if (apReqOnline == null || !isQualifiedLandManager(apReqOnline, apMain)) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f申请区已在领地 §e" + apMain.name + + " §f内,但申请人 §e" + pe.requester + " §f不具备该领地的管理身份,申请作废"); + return; + } + } else { + for (AreaConfig aot : areas.values()) { + if (aot == null || aot.parentId != 0) continue; + if (!aot.world.equalsIgnoreCase(pe.world)) continue; + int ao1x = Math.min(aot.x1, aot.x2), ao2x = Math.max(aot.x1, aot.x2); + int ao1z = Math.min(aot.z1, aot.z2), ao2z = Math.max(aot.z1, aot.z2); + if (aN1x <= ao2x && aN2x >= ao1x && aN1z <= ao2z && aN2z >= ao1z) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f申请区与领地 §e" + aot.name + + " §f部分重叠(跨边界),子领地必须完整位于某主领地内,申请作废"); + return; + } + } + } + if (apIsSub && areaNameExists(pe.landName)) { + int sa = 1; + while (areaNameExists(pe.landName + sa)) sa++; + pe.landName = pe.landName + sa; + } + UserGroupManager aPugm = plugin.getUserGroup(); + int aMax = (aPugm != null) ? aPugm.getPlayerMaxLands(pe.requester, globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; + long aCnt = areas.values().stream().filter(a -> samePlayer(pe.requester, a.owner)).count(); + if (aCnt >= aMax) { + pendingExpands.remove(pe.landName.toLowerCase()); + adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester + " §f已达领地上限(" + aMax + "),申请作废"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§c§l[防护] §f你的领地数量已达上限,创建申请作废"); + } + return; + } + int aPrice = (aPugm != null) ? aPugm.getPlayerLandPricePerSqm(pe.requester, globalCreatePricePerSqm) : globalCreatePricePerSqm; + int aCost = pe.area * aPrice; + int charged = 0; + if (aCost > 0) { + BondManager abm = plugin.getBonds(); + if (abm != null) { + if (!abm.deductBonds(pe.requester, aCost, "land_create", pe.requester, pe.requester, + "管理员审批创建领地: " + pe.landName + " (" + pe.area + "㎡×" + aPrice + ")")) { + adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester + + " §f余额不足(需 §e" + aCost + "§f),无法扣费,申请保留待处理"); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§c§l[防护] §f你的领地创建申请因余额不足(需 " + aCost + " 债券)未能完成,请充值后请管理员重新审批"); + } + return; + } + charged = aCost; + } + } + AreaConfig nc = new AreaConfig(); + nc.name = pe.landName; + // ★ 第57批续21 子领地:owner 沿用主领地主;主领地:owner=申请人 + nc.owner = apIsSub ? apMain.owner : pe.requester; + nc.world = pe.world; + nc.x1 = pe.newX1; nc.z1 = pe.newZ1; + nc.x2 = pe.newX2; nc.z2 = pe.newZ2; + nc.yMin = 0; nc.yMax = 255; + nc.createCost = charged; + nc.parentId = apIsSub ? getLandIdFromDb(apMain.name) : 0; // ★ 第57批续21 子领地父ID + saveAreaToDb(nc); + areas.put(pe.landName, nc); + pendingExpands.remove(pe.landName.toLowerCase()); + loadAllAreas(); + if (apIsSub) { + adm.sendMessage("§a§l[审批] §f已批准创建子领地 §e" + pe.landName + + " §f(父领地 " + apMain.name + ", §e" + pe.area + "㎡§f),向申请人扣除 §e" + charged + " §f债券"); + } else { + adm.sendMessage("§a§l[审批] §f已批准创建领地 §e" + pe.landName + " §f(§e" + + pe.area + "㎡§f),向申请人扣除 §e" + charged + " §f债券"); + } + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a§l[防护] §f你的领地 §e" + pe.landName + + " §f已由管理员审批创建成功(" + pe.area + "㎡,扣除 " + charged + " 债券)"); + } + plugin.getLogger().info("[领地审批] 创建通过: " + pe.landName + " (申请人 " + pe.requester + + ", 审批人 " + adm.getName() + ", 扣费 " + charged + ")"); + } + } + + /** ★ 供 Tab 补全:待审批目标(领地名 + 申请人名) */ + public List getPendingApprovalTargets() { + pruneExpiredPending(); + List out = new ArrayList<>(); + for (PendingExpand pe : pendingExpands.values()) { + out.add(pe.landName); + if (!out.contains(pe.requester)) out.add(pe.requester); + } + return out; + } + + // ==================== 封禁/注销领主的领地自动清理 ==================== + + private BukkitTask bannedOwnerSweepTask; + /** 扫描周期:5分钟 */ + private static final long SWEEP_INTERVAL_TICKS = 6000L; + /** 剩余封禁时长 ≥ 9年,视为"10年封禁" */ + private static final long LONG_BAN_REMAIN_MS = 9L * 365 * 24 * 3600 * 1000L; + + /** + * ★ 启动封禁/注销领主的领地自动清理任务 + * 命中以下任一条件的领地 → 立即强制删除(不退款、不注销账号), + * 并把领地坐标上报给在线的插件管理员 & OP: + * 1) 领主被永久封禁 + * 2) 领主被 10 年封禁 + * 3) 领主账户已注销 + */ + public void startBannedOwnerSweepTask() { + if (bannedOwnerSweepTask != null) { + bannedOwnerSweepTask.cancel(); + } + bannedOwnerSweepTask = new BukkitRunnable() { + @Override + public void run() { + try { + sweepBannedOwnerLands(); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 领地自动清理任务异常: " + e.getMessage()); + } + } + }.runTaskTimer(plugin, 200L, SWEEP_INTERVAL_TICKS); + } + + public void stopBannedOwnerSweepTask() { + if (bannedOwnerSweepTask != null) { + bannedOwnerSweepTask.cancel(); + bannedOwnerSweepTask = null; + } + } + + /** + * ★ 两阶段执行: + * 阶段1(主线程):只做 Bukkit 侧判定(封禁表 / 在线状态),不碰 DB; + * 阶段2(异步):userExists 判定 + deleteAreaFromDb 落库 + 触发 PHP 同步(含 HTTP)。 + * 上报要遍历在线玩家,回主线程。 + */ + private void sweepBannedOwnerLands() { + if (areas.isEmpty()) return; + + // ---- 阶段1(主线程,纯 Bukkit 只读判定)---- + final List candidates = new ArrayList<>(); + for (AreaConfig ac : new ArrayList<>(areas.values())) { + if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue; + // 快照遍历期间已被删除/重载的跳过 + if (areas.get(ac.name) != ac) continue; + + String reason = null; + + // 1/2) 永久封禁 / 10年封禁 + try { + org.bukkit.BanEntry ban = + Bukkit.getBanList(org.bukkit.BanList.Type.NAME).getBanEntry(ac.owner); + if (ban != null) { + java.util.Date exp = ban.getExpiration(); + if (exp == null) { + reason = "永久封禁"; + } else if (exp.getTime() - System.currentTimeMillis() >= LONG_BAN_REMAIN_MS) { + reason = "10年封禁"; + } + } + } catch (Exception ignored) {} + + if (reason == null) { + // 3) 账户已注销(在线领主一律跳过,避免误删)—— 在线判定是 Bukkit API + Player ownerOnline = Bukkit.getPlayerExact(ac.owner); + if (ownerOnline != null && ownerOnline.isOnline()) continue; + } + // reason 为 null = 还需异步阶段用 DB 判定是否注销 + candidates.add(new Object[]{ac, reason}); + } + if (candidates.isEmpty()) return; + + final DatabaseManager dbMgr = plugin.getDb(); + // ---- 阶段2(异步:DB 判定 + 删除落库 + PHP 同步)---- + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + final List reports = new ArrayList<>(); + try { + for (Object[] o : candidates) { + AreaConfig ac = (AreaConfig) o[0]; + String reason = (String) o[1]; + if (reason == null) { + if (dbMgr == null) continue; + try { + if (!dbMgr.userExists(ac.owner)) { + reason = "账户已注销"; + } + } catch (Exception ignored) {} + if (reason == null) continue; + } + if (areas.get(ac.name) != ac) continue; + + // 不退款、不注销账号,直接强制删除 + cancelPendingDelete(ac.name); + deleteAreaFromDb(ac.name); + areas.remove(ac.name); + String coord = ac.world + " " + ac.x1 + "," + ac.z1 + " ~ " + ac.x2 + "," + ac.z2; + plugin.getLogger().warning("[防护] 自动清理领地 " + ac.name + ":领主 " + ac.owner + + " 因" + reason + ",已强制删除(不退款)坐标 " + coord); + reports.add("§6§l[防护-自动清理] §f领主 §e" + ac.owner + + " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name + + " §7(不退款) [" + coord + "]"); + } + } catch (Exception e) { + plugin.getLogger().warning("[防护] 领地自动清理(异步阶段)异常: " + e.getMessage()); + } + if (reports.isEmpty()) return; + // ---- 阶段3(回主线程上报:遍历在线玩家 + OP/管理员判定)---- + Bukkit.getScheduler().runTask(plugin, () -> { + for (String m : reports) reportLandRemovalToAdmins(m); + }); + }); + } + + /** + * 如果领地有待删除请求,取消它(玩家操作领地时自动解除冻结) + */ + // ★ package-private供AreaCLIManager访问 + void cancelPendingDelete(String areaName) { + PendingDelete pd = pendingDeletes.remove(areaName); + if (pd != null) { + pd.task.cancel(); + Player p = Bukkit.getPlayerExact(pd.playerName); + if (p != null && p.isOnline()) { + p.sendMessage("§c§l[防护] §f领地 §e" + areaName + " §f已被操作,删除请求已自动取消"); + } + } + } + + + private String[] parseAreaAndTarget(String arg1, String arg2) { + // ★ 严格匹配:写操作绝不做模糊归一化,避免部分/大小写/特殊字符输入命中他人领地 + String r1 = resolveAreaNameStrict(arg1); + if (r1 != null) { + return new String[]{r1, arg2}; + } + String r2 = resolveAreaNameStrict(arg2); + if (r2 != null) { + return new String[]{r2, arg1}; + } + return null; + } + + private boolean playerExists(String playerName) { + // 在线玩家 + for (Player online : Bukkit.getOnlinePlayers()) { + if (online.getName().equalsIgnoreCase(playerName)) return true; + } + // 离线玩家 + @SuppressWarnings("deprecation") + OfflinePlayer op = Bukkit.getOfflinePlayer(playerName); + return op.hasPlayedBefore(); + } + + + private boolean validateArea(CommandSender sender, String areaName) { + if (!areas.containsKey(areaName)) { + sender.sendMessage("§c区域不存在: " + areaName); + return false; + } + return true; + } + + + private String matname(Material mat) { + String name = mat.name(); + String[] parts = name.split("_"); + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (sb.length() > 0) sb.append(" "); + sb.append(part.substring(0, 1).toUpperCase()) + .append(part.substring(1).toLowerCase()); + } + return sb.toString(); + } + private String yawToDir(float yaw) { + // MC中: 0=南, 90=西, 180=北, -90/270=东 + yaw = yaw % 360; + if (yaw < 0) yaw += 360; + if (yaw >= 315 || yaw < 45) return "南"; + if (yaw >= 45 && yaw < 135) return "西"; + if (yaw >= 135 && yaw < 225) return "北"; + return "东"; + } + private void saveAreaConfig(AreaConfig ac) { + File f = new File(rootDir, ac.name + ".txt"); + if (!f.exists()) return; + try { + List lines = new ArrayList<>(); + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + if (line.trim().startsWith("起点:")) { + lines.add("起点: " + ac.world + + "," + ac.x1 + "," + ac.z1); + } else if (line.trim().startsWith("终点:")) { + lines.add("终点: " + ac.world + + "," + ac.x2 + "," + ac.z2); + } else { + lines.add(line); + } + } + br.close(); + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + for (String l : lines) pw.println(l); + pw.close(); + } catch (IOException ignored) {} + } + + /** + * 保存领地配置到数据库(新方法) + */ + public void saveAreaToDb(AreaConfig ac) { + if (dbConnection == null) return; + try { + // ★ 使用 INSERT ... ON CONFLICT 替代 INSERT OR REPLACE: + // INSERT OR REPLACE 会删除旧行插入新行,导致自增 id 改变, + // 进而使 area_land_permissions.land_id 指向不存在的老 id,造成管理员/成员权限丢失。 + PreparedStatement stmt = dbConnection.prepareStatement( + "INSERT INTO area_lands (name, owner, world, x1, z1, x2, z2, y_min, y_max, " + + "confiscate_items, deny_use_items, give_effects, clear_effects, clear_all_bad, " + + "punish_commands, deny_block_place, deny_block_break, deny_pvp, deny_fall_damage, " + + "deny_hunger, deny_all_damage, deny_drop, deny_mount, deny_ender_pearl, " + + "deny_bow, deny_potion, deny_explosion, deny_raid, deny_fire_spread, " + + "deny_all_effects, deny_item_frame, deny_entity_interact, deny_move, deny_pickup, deny_fire, " + + "peace_mode, peace_mode_duration, " + + "peace_whitelist, enforce_game_mode, mode_exempt, enter_msg, leave_msg, " + + "confiscate_msg, enable_announce, announce_template, txt_content, created_at, " + + "deny_thrown_projectiles, deny_glowing, deny_redstone_interaction, deny_door_interaction, " + + "deny_noteblock_jukebox, deny_lead, deny_crop_harvest, deny_wool_shear, deny_animal_feeding, " + + "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport, deny_farmland_trample, deny_ender_teleport, create_cost, deny_spawn_egg, deny_wax, bad_effects, deny_villager_trade, confiscate_count, return_on_leave, parent_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + "ON CONFLICT(name) DO UPDATE SET " + + "owner=excluded.owner, world=excluded.world, x1=excluded.x1, z1=excluded.z1, x2=excluded.x2, z2=excluded.z2, y_min=excluded.y_min, y_max=excluded.y_max, " + + "confiscate_items=excluded.confiscate_items, deny_use_items=excluded.deny_use_items, give_effects=excluded.give_effects, clear_effects=excluded.clear_effects, clear_all_bad=excluded.clear_all_bad, " + + "punish_commands=excluded.punish_commands, deny_block_place=excluded.deny_block_place, deny_block_break=excluded.deny_block_break, deny_pvp=excluded.deny_pvp, deny_fall_damage=excluded.deny_fall_damage, " + + "deny_hunger=excluded.deny_hunger, deny_all_damage=excluded.deny_all_damage, deny_drop=excluded.deny_drop, deny_mount=excluded.deny_mount, deny_ender_pearl=excluded.deny_ender_pearl, " + + "deny_bow=excluded.deny_bow, deny_potion=excluded.deny_potion, deny_explosion=excluded.deny_explosion, deny_raid=excluded.deny_raid, deny_fire_spread=excluded.deny_fire_spread, " + + "deny_all_effects=excluded.deny_all_effects, deny_item_frame=excluded.deny_item_frame, deny_entity_interact=excluded.deny_entity_interact, deny_move=excluded.deny_move, deny_pickup=excluded.deny_pickup, deny_fire=excluded.deny_fire, " + + "peace_mode=excluded.peace_mode, peace_mode_duration=excluded.peace_mode_duration, " + + "peace_whitelist=excluded.peace_whitelist, enforce_game_mode=excluded.enforce_game_mode, mode_exempt=excluded.mode_exempt, enter_msg=excluded.enter_msg, leave_msg=excluded.leave_msg, " + + "confiscate_msg=excluded.confiscate_msg, enable_announce=excluded.enable_announce, announce_template=excluded.announce_template, txt_content=excluded.txt_content, created_at=excluded.created_at, " + + "deny_thrown_projectiles=excluded.deny_thrown_projectiles, deny_glowing=excluded.deny_glowing, deny_redstone_interaction=excluded.deny_redstone_interaction, deny_door_interaction=excluded.deny_door_interaction, " + + "deny_noteblock_jukebox=excluded.deny_noteblock_jukebox, deny_lead=excluded.deny_lead, deny_crop_harvest=excluded.deny_crop_harvest, deny_wool_shear=excluded.deny_wool_shear, deny_animal_feeding=excluded.deny_animal_feeding, " + + "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport, deny_farmland_trample=excluded.deny_farmland_trample, deny_ender_teleport=excluded.deny_ender_teleport, create_cost=excluded.create_cost, deny_spawn_egg=excluded.deny_spawn_egg, deny_wax=excluded.deny_wax, bad_effects=excluded.bad_effects, deny_villager_trade=excluded.deny_villager_trade, confiscate_count=excluded.confiscate_count, return_on_leave=excluded.return_on_leave, parent_id=excluded.parent_id"); + + stmt.setString(1, ac.name); + stmt.setString(2, ac.owner != null ? ac.owner : ""); + stmt.setString(3, ac.world); + stmt.setInt(4, ac.x1); + stmt.setInt(5, ac.z1); + stmt.setInt(6, ac.x2); + stmt.setInt(7, ac.z2); + stmt.setInt(8, ac.yMin); + stmt.setInt(9, ac.yMax); + stmt.setString(10, String.join(",", ac.confiscateItems)); + stmt.setString(11, String.join(",", ac.denyUseItems)); + stmt.setString(12, effectsToString(ac.giveEffects)); + stmt.setString(13, String.join(",", ac.clearEffects)); + stmt.setInt(14, ac.clearAllBadEffects ? 1 : 0); + stmt.setString(15, String.join("|", ac.punishCommands)); + stmt.setInt(16, ac.denyBlockPlace ? 1 : 0); + stmt.setInt(17, ac.denyBlockBreak ? 1 : 0); + stmt.setInt(18, ac.denyPVP ? 1 : 0); + stmt.setInt(19, ac.denyFallDamage ? 1 : 0); + stmt.setInt(20, ac.denyHunger ? 1 : 0); + stmt.setInt(21, ac.denyAllDamage ? 1 : 0); + stmt.setInt(22, ac.allowDrop ? 1 : 0); + stmt.setInt(23, ac.denyMount ? 1 : 0); + stmt.setInt(24, ac.denyEnderPearl ? 1 : 0); + stmt.setInt(25, ac.denyBow ? 1 : 0); + stmt.setInt(26, ac.denyPotion ? 1 : 0); + stmt.setInt(27, ac.denyExplosion ? 1 : 0); + stmt.setInt(28, ac.denyRaid ? 1 : 0); + stmt.setInt(29, ac.denyFireSpread ? 1 : 0); + stmt.setInt(30, ac.denyAllEffects ? 1 : 0); + stmt.setInt(31, ac.denyItemFrame ? 1 : 0); + stmt.setInt(32, ac.denyEntityInteract ? 1 : 0); // ★ 修复:deny_entity_interact 列 + stmt.setInt(33, ac.denyMove ? 1 : 0); + stmt.setInt(34, ac.allowPickup ? 1 : 0); + stmt.setInt(35, ac.denyFire ? 1 : 0); + stmt.setInt(36, ac.peaceMode ? 1 : 0); + stmt.setInt(37, ac.peaceModeDuration / 1000); + stmt.setString(38, String.join(",", ac.peaceWhitelist)); + stmt.setString(39, ac.enforceGameMode != null ? ac.enforceGameMode : ""); + stmt.setString(40, String.join(",", ac.modeExempt)); + stmt.setString(41, ac.enterMsg); + stmt.setString(42, ac.leaveMsg); + stmt.setString(43, ac.confiscateMsg); + stmt.setInt(44, ac.enableAnnounce ? 1 : 0); + stmt.setString(45, ac.announceTemplate); + stmt.setString(46, ac.txtContent != null ? ac.txtContent : ""); + stmt.setLong(47, System.currentTimeMillis() / 1000); + stmt.setInt(48, ac.denyThrownProjectiles ? 1 : 0); + stmt.setInt(49, ac.denyGlowing ? 1 : 0); + stmt.setInt(50, ac.denyRedstoneInteraction ? 1 : 0); + stmt.setInt(51, ac.denyDoorInteraction ? 1 : 0); + stmt.setInt(52, ac.denyNoteblockJukebox ? 1 : 0); + stmt.setInt(53, ac.denyLead ? 1 : 0); + stmt.setInt(54, ac.denyCropHarvest ? 1 : 0); + stmt.setInt(55, ac.denyWoolShear ? 1 : 0); + stmt.setInt(56, ac.denyAnimalFeeding ? 1 : 0); + stmt.setDouble(57, ac.warpX); + stmt.setDouble(58, ac.warpY); + stmt.setDouble(59, ac.warpZ); + stmt.setFloat(60, ac.warpYaw); + stmt.setFloat(61, ac.warpPitch); + stmt.setString(62, ac.warpWorld != null ? ac.warpWorld : ""); + stmt.setInt(63, ac.denyContainer ? 1 : 0); + stmt.setInt(64, ac.denyMobAttack ? 1 : 0); + stmt.setInt(65, ac.denySignEdit ? 1 : 0); + stmt.setInt(66, ac.isPublicBuilding ? 1 : 0); + stmt.setInt(67, ac.allowVisitorTeleport ? 1 : 0); + stmt.setInt(68, ac.denyFarmlandTrample ? 1 : 0); + stmt.setInt(69, ac.denyEnderTeleport ? 1 : 0); + stmt.setInt(70, ac.createCost); + stmt.setInt(71, ac.denySpawnEgg ? 1 : 0); + stmt.setInt(72, ac.denyWax ? 1 : 0); + stmt.setString(73, effectsToString(ac.badEffects)); + stmt.setInt(74, ac.denyVillagerTrade ? 1 : 0); + stmt.setInt(75, ac.confiscateCount); + stmt.setInt(76, ac.returnOnLeave ? 1 : 0); + stmt.setInt(77, ac.parentId); // ★ 第57批续21 子领地父ID(0=主领地) + stmt.executeUpdate(); + stmt.close(); + // ★ 领地设置变更:立即触发PHP同步(防抖10秒) + try { + if (plugin.webManager != null) { + plugin.webManager.requestImmediateLandSync(); + } + } catch (Exception ignored) {} + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 保存到DB失败: " + ac.name + " - " + e.getMessage()); + } + } + + /** + * 从数据库删除领地 + */ + public void deleteAreaFromDb(String name) { + if (dbConnection == null) return; + try { + PreparedStatement stmt = dbConnection.prepareStatement("DELETE FROM area_lands WHERE name = ?"); + stmt.setString(1, name); + stmt.executeUpdate(); + stmt.close(); + // ★ 领地删除:立即触发PHP同步 + try { + if (plugin.webManager != null) { + plugin.webManager.requestImmediateLandSync(); + } + } catch (Exception ignored) {} + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 从DB删除领地失败: " + name + " - " + e.getMessage()); + } + } + + /** + * 获取所有领地数据(供WebManager同步到PHP) + */ + public List> getAllLandsForSync() { + List> result = new ArrayList<>(); + if (dbConnection == null) return result; + try { + Statement stmt = dbConnection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM area_lands"); + int cols = rs.getMetaData().getColumnCount(); + while (rs.next()) { + Map map = new HashMap<>(); + for (int i = 1; i <= cols; i++) { + String colName = rs.getMetaData().getColumnName(i); + map.put(colName, rs.getObject(colName)); + } + result.add(map); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 获取领地数据失败: " + e.getMessage()); + } + return result; + } + + /** + * 获取权限商店数据(供WebManager同步到PHP) + */ + public List> getPermissionShopForSync() { + List> result = new ArrayList<>(); + if (dbConnection == null) return result; + try { + Statement stmt = dbConnection.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT s.id, s.land_id, l.name AS land_name, s.seller, s.permission, s.price, s.duration, s.status, s.buyer, s.bought_at, s.created_at " + + "FROM area_permission_shop s " + + "LEFT JOIN area_lands l ON s.land_id = l.id"); + while (rs.next()) { + Map map = new HashMap<>(); + map.put("id", rs.getInt("id")); + map.put("land_id", rs.getInt("land_id")); + map.put("land_name", rs.getString("land_name")); + map.put("seller", rs.getString("seller")); + map.put("permission", rs.getString("permission")); + map.put("price", rs.getInt("price")); + map.put("duration", rs.getInt("duration")); + map.put("status", rs.getString("status")); + map.put("buyer", rs.getString("buyer")); + map.put("bought_at", rs.getLong("bought_at")); + map.put("created_at", rs.getLong("created_at")); + result.add(map); + } + rs.close(); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 获取权限商店数据失败: " + e.getMessage()); + } + return result; + } + + /** + * 从数据库获取领地ID + */ + /** ★ 第57批续21:按 area_lands.id 反查领地名(子领地继承判定用) */ + public String findLandNameById(int id) { + if (dbConnection == null || id <= 0) return null; + try { + PreparedStatement stmt = dbConnection.prepareStatement("SELECT name FROM area_lands WHERE id = ?"); + stmt.setInt(1, id); + ResultSet rs = stmt.executeQuery(); + String n = rs.next() ? rs.getString("name") : null; + rs.close(); + stmt.close(); + return n; + } catch (SQLException e) { + return null; + } + } + + public int getLandIdFromDb(String name) { + if (dbConnection == null) return -1; + try { + PreparedStatement stmt = dbConnection.prepareStatement("SELECT id FROM area_lands WHERE name = ?"); + stmt.setString(1, name); + ResultSet rs = stmt.executeQuery(); + int id = rs.next() ? rs.getInt("id") : -1; + rs.close(); + stmt.close(); + return id; + } catch (SQLException e) { + return -1; + } + } + + /** + * 获取领地权限快照(用于过户cooldown期间检测权限变更) + * 返回JSON字符串,包含owner、members、visitors等关键字段 + */ + public String getLandPermissionsSnapshot(String landName) { + AreaConfig ac = areas.get(landName); + if (ac == null) return null; + try { + StringBuilder sb = new StringBuilder(); + sb.append("{\"owner\":\"").append(ac.owner != null ? ac.owner : "").append("\""); + // 成员列表(areaPlayerWhitelist) + sb.append(",\"members\":["); + Set members = areaPlayerWhitelist.get(landName); + if (members != null && !members.isEmpty()) { + java.util.List sorted = new java.util.ArrayList<>(members); + java.util.Collections.sort(sorted); + boolean first = true; + for (String m : sorted) { + if (!first) sb.append(","); + sb.append("\"").append(m).append("\""); + first = false; + } + } + sb.append("]"); + sb.append("}"); + return sb.toString(); + } catch (Exception e) { + return null; + } + } + + /** + * PHP端修改领地所有者时调用 + */ + public void setLandOwnerFromWeb(String landName, String newOwner) { + if (dbConnection == null) return; + try { + // ★ 幂等检查:如果所有者已相同则跳过,避免ack失败后重复打印 + AreaConfig ac = getLand(landName); + if (ac != null && samePlayer(newOwner, ac.owner)) { + return; + } + + // 更新数据库 + PreparedStatement ps = dbConnection.prepareStatement("UPDATE area_lands SET owner = ? WHERE name = ?"); + ps.setString(1, newOwner); + ps.setString(2, landName); + ps.executeUpdate(); + ps.close(); + + // 更新内存中的AreaConfig + if (ac != null) { + ac.owner = newOwner; + } + + plugin.getLogger().info("[防护] PHP端更新领地所有者: " + landName + " → " + newOwner); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 更新领地所有者失败: " + e.getMessage()); + } + } + + /** + * PHP端清除成员权限时调用 + */ + public void clearPlayerPermFromWeb(String landName, String playerName) { + if (dbConnection == null) return; + try { + int landId = getLandIdFromDb(landName); + if (landId < 0) return; + + // 清除数据库中的自定义权限(按 normName 匹配到的所有行,避免大小写变种残留) + for (PermRow row : findPermRows(landId, playerName)) { + PreparedStatement ps = dbConnection.prepareStatement( + "UPDATE area_land_permissions SET permissions = '' WHERE id = ?"); + ps.setInt(1, row.id); + ps.executeUpdate(); + ps.close(); + } + + plugin.getLogger().info("[防护] PHP端清除成员权限: " + playerName + " @ " + landName); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 清除成员权限失败: " + e.getMessage()); + } + } + + /** + * ★ PHP端修改领地字段时调用(效果管理等) + * field: give_effects, clear_effects, clear_all_bad, deny_all_effects等 + * value: 新值(已转为Java格式) + */ + public void updateLandFieldFromWeb(String landName, String field, String value) { + if (dbConnection == null) return; + try { + // 白名单校验字段名(防SQL注入) + java.util.Set allowedFields = new java.util.HashSet<>(java.util.Arrays.asList( + "give_effects", "bad_effects", "clear_effects", "clear_all_bad", "deny_all_effects", + "deny_block_place", "deny_block_break", "deny_pvp", "deny_fall_damage", + "deny_hunger", "deny_all_damage", "deny_drop", "deny_mount", "deny_ender_pearl", + "deny_bow", "deny_potion", "deny_explosion", "deny_raid", "deny_fire_spread", + "deny_all_effects", "deny_item_frame", "deny_entity_interact", "deny_move", "deny_pickup", "deny_fire", + "confiscate_items", "deny_use_items", "punish_commands", + "peace_mode", "peace_mode_duration", "enforce_game_mode", + "enter_msg", "leave_msg", "confiscate_msg", + "deny_thrown_projectiles", "deny_glowing", "deny_redstone_interaction", + "deny_door_interaction", "deny_noteblock_jukebox", "deny_lead", + "deny_crop_harvest", "deny_wool_shear", "deny_animal_feeding", + "deny_container", "deny_mob_attack", "deny_fluid", + "is_public_building", "allow_visitor_teleport", + "deny_spawn_egg", "deny_wax", "deny_villager_trade", + "confiscate_count", "return_on_leave" + )); + if (!allowedFields.contains(field)) { + plugin.getLogger().warning("[防护] PHP端更新未知字段: " + field); + return; + } + + PreparedStatement ps = dbConnection.prepareStatement( + "UPDATE area_lands SET " + field + " = ? WHERE name = ?"); + ps.setString(1, value); + ps.setString(2, landName); + int affected = ps.executeUpdate(); + ps.close(); + + if (affected > 0) { + // ★ 直接更新内存 AreaConfig 字段(reloadAreaConfigFromDb 只重载 area_config 全局表,不碰 area_lands) + AreaConfig ac = getLand(landName); + if (ac != null) { + applyFieldToAreaConfig(ac, field, value); + } + plugin.getLogger().info("[防护] PHP端更新领地字段: " + landName + "." + field + " = " + value); + } + } catch (SQLException e) { + plugin.getLogger().warning("[防护] PHP端更新领地字段失败: " + e.getMessage()); + } + } + + /** + * ★ 领地改名(带冷却检查) + * @return null=成功, 非null=错误/冷却信息 + */ + public String renameLand(Player p, String oldName, String newName) { + // 0. 权限校验:只有领地主/领地管理员可改名(纵深防御) + AreaConfig ownerAc = areas.get(oldName); + if (ownerAc != null + && !isAreaAdmin(p) + && !samePlayer(p.getName(), ownerAc.owner)) { + return "需要领地所有者或管理员权限"; + } + // 1. 基本校验(格式 + 特殊字符,防止按名字落盘的文件被覆盖或路径穿越) + if (newName == null || newName.trim().isEmpty()) return "新名字不能为空"; + newName = newName.trim(); + String fmtErr = validateAreaName(newName); + if (fmtErr != null) return fmtErr; + if (newName.equalsIgnoreCase(oldName)) return "新名字和旧名字相同"; + + // 2. 重名检查(不区分大小写:大小写变体会指向同一份文件) + if (areaNameExists(newName)) return "已存在同名领地: " + newName; + + // 3. 冷却检查 + long now = System.currentTimeMillis(); + java.util.List timestamps = renameCooldownMap.computeIfAbsent( + p.getUniqueId(), k -> new java.util.ArrayList<>()); + + // 清除过期记录(超出1小时窗口的) + timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); + + if (timestamps.size() >= RENAME_LIMIT) { + long firstInWindow = timestamps.get(0); + long unlockTime = firstInWindow + RENAME_WINDOW_MS; + long remainSec = (unlockTime - now) / 1000; + long remainMin = remainSec / 60; + long remainSecR = remainSec % 60; + return "改名冷却中!本小时已改名" + RENAME_LIMIT + "次," + + remainMin + "分" + remainSecR + "秒后解锁"; + } + + // 4. 执行改名 + try { + if (dbConnection == null || dbConnection.isClosed()) return "数据库未连接"; + + // 4a. 更新 area_lands 表的 name + PreparedStatement ps = dbConnection.prepareStatement( + "UPDATE area_lands SET name = ? WHERE name = ?"); + ps.setString(1, newName); + ps.setString(2, oldName); + int affected = ps.executeUpdate(); + ps.close(); + + if (affected <= 0) return "数据库中未找到领地: " + oldName; + + // 4b. 更新 area_land_permissions 表的 land_id(如果用name关联) + // land_id 是 area_lands 的自增 id,改名不影响 id,所以无需更新 + + // 4c. 更新内存 + AreaConfig ac = areas.remove(oldName); + if (ac != null) { + ac.name = newName; + areas.put(newName, ac); + } + + // 4d. 更新内存中的成员权限缓存等(如果有以旧名字为key的缓存) + // areaPlayerWhitelist 也以 landName 为 key,需同步 + java.util.Set whitelist = areaPlayerWhitelist.remove(oldName); + if (whitelist != null) { + areaPlayerWhitelist.put(newName, whitelist); + } + + // 4e. 记录冷却 + timestamps.add(now); + + // 4f. 触发PHP同步 + try { + plugin.webManager.syncLandData(); + } catch (Exception ignored) {} + + plugin.getLogger().info("[防护] 领地改名: " + oldName + " → " + newName + " (by " + p.getName() + ")"); + return null; // 成功 + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 领地改名失败: " + e.getMessage()); + return "数据库错误: " + e.getMessage(); + } + } + + /** + * ★ 获取改名冷却信息(用于显示在菜单中) + * @return [剩余秒数, 已用次数] 或 null(无冷却) + */ + public long[] getRenameCooldown(Player p) { + long now = System.currentTimeMillis(); + java.util.List timestamps = renameCooldownMap.get(p.getUniqueId()); + if (timestamps == null || timestamps.isEmpty()) return null; + + // 清除过期 + timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); + + int used = timestamps.size(); + if (used == 0) return null; + + // ★ 仅在达到上限时才提示冷却,未达上限时只显示剩余次数 + if (used < RENAME_LIMIT) return null; + + long firstInWindow = timestamps.get(0); + long unlockTime = firstInWindow + RENAME_WINDOW_MS; + if (now >= unlockTime) return null; // 已解锁 + + long remainSec = (unlockTime - now) / 1000; + return new long[]{remainSec, used}; + } + + /** + * 获取剩余改名次数 + */ + public int getRemainingRenames(Player p) { + long now = System.currentTimeMillis(); + java.util.List timestamps = renameCooldownMap.get(p.getUniqueId()); + if (timestamps == null) return RENAME_LIMIT; + timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS); + return Math.max(0, RENAME_LIMIT - timestamps.size()); + } + + /** + * 将 PHP 端同步过来的单个字段值直接写入内存 AreaConfig 对象 + */ + private void applyFieldToAreaConfig(AreaConfig ac, String field, String value) { + switch (field) { + // boolean 权限字段 (DB int 0/1 → boolean) + case "deny_block_place": ac.denyBlockPlace = "1".equals(value); break; + case "deny_block_break": ac.denyBlockBreak = "1".equals(value); break; + case "deny_container": ac.denyContainer = "1".equals(value); break; + case "deny_sign_edit": ac.denySignEdit = "1".equals(value); break; + case "deny_pvp": ac.denyPVP = "1".equals(value); break; + case "deny_fall_damage": ac.denyFallDamage = "1".equals(value); break; + case "deny_hunger": ac.denyHunger = "1".equals(value); break; + case "deny_all_damage": ac.denyAllDamage = "1".equals(value); break; + case "deny_mount": ac.denyMount = "1".equals(value); break; + case "deny_ender_pearl": ac.denyEnderPearl = "1".equals(value); break; + case "deny_bow": ac.denyBow = "1".equals(value); break; + case "deny_potion": ac.denyPotion = "1".equals(value); break; + case "deny_explosion": ac.denyExplosion = "1".equals(value); break; + case "deny_move": ac.denyMove = "1".equals(value); break; + case "deny_fire": ac.denyFire = "1".equals(value); break; + case "deny_fire_spread": ac.denyFireSpread = "1".equals(value); break; + case "deny_all_effects": ac.denyAllEffects = "1".equals(value); break; + case "deny_raid": ac.denyRaid = "1".equals(value); break; + case "deny_thrown_projectiles": ac.denyThrownProjectiles = "1".equals(value); break; + case "deny_glowing": ac.denyGlowing = "1".equals(value); break; + case "deny_redstone_interaction": ac.denyRedstoneInteraction = "1".equals(value); break; + case "deny_door_interaction": ac.denyDoorInteraction = "1".equals(value); break; + case "deny_noteblock_jukebox": ac.denyNoteblockJukebox = "1".equals(value); break; + case "deny_lead": ac.denyLead = "1".equals(value); break; + case "deny_crop_harvest": ac.denyCropHarvest = "1".equals(value); break; + case "deny_wool_shear": ac.denyWoolShear = "1".equals(value); break; + case "deny_animal_feeding": ac.denyAnimalFeeding = "1".equals(value); break; + case "deny_spawn_egg": ac.denySpawnEgg = "1".equals(value); break; + case "deny_wax": ac.denyWax = "1".equals(value); break; + case "deny_mob_attack": ac.denyMobAttack = "1".equals(value); break; + case "deny_item_frame": ac.denyItemFrame = "1".equals(value); break; + case "deny_villager_trade": ac.denyVillagerTrade = "1".equals(value); break; + case "return_on_leave": ac.returnOnLeave = "1".equals(value); break; + case "confiscate_count": + try { ac.confiscateCount = Integer.parseInt(value); } catch (Exception ignored) {} + break; + case "deny_entity_interact": ac.denyEntityInteract = "1".equals(value); break; + case "deny_pickup": ac.allowPickup = "0".equals(value); break; // deny_pickup 反向 + case "deny_drop": ac.allowDrop = "0".equals(value); break; // deny_drop 反向 + case "is_public_building": ac.isPublicBuilding = "1".equals(value); break; + case "allow_visitor_teleport": ac.allowVisitorTeleport = "1".equals(value); break; + // boolean 配置字段 + case "peace_mode": ac.peaceMode = "1".equals(value); break; + case "clear_all_bad": ac.clearAllBadEffects = "1".equals(value); break; + // int 字段 + case "peace_mode_duration": + try { ac.peaceModeDuration = Integer.parseInt(value); } catch (Exception ignored) {} + break; + // String 字段 + case "enter_msg": ac.enterMsg = value; break; + case "leave_msg": ac.leaveMsg = value; break; + case "confiscate_msg": ac.confiscateMsg = value; break; + case "enforce_game_mode": ac.enforceGameMode = value; break; + // 复合字段(JSON 数组或管道格式 → List) + // ★ 2026-10-04:负面效果拆到 bad_effects,PHP 推来的字段名同样是 bad_effects + case "give_effects": + case "bad_effects": { + boolean isBad = "bad_effects".equals(field); + List parsed = parseEffectsString(value); + // ★ 数量上限截断:增益与负面共用同一份 max_effects 预算(按合计算) + try { + int maxEff = getMaxGiveEffects(ac.owner); + int otherCount = isBad ? ac.giveEffects.size() : ac.badEffects.size(); + int room = Math.max(0, maxEff - otherCount); + if (parsed.size() > room) { + plugin.getLogger().warning("[防护] 领地 " + + ac.name + (isBad ? " 负面效果 " : " 增益效果 ") + + parsed.size() + " 个超出上限 " + + maxEff + "(另一类已占 " + otherCount + + "),已截断保留前 " + room + " 个"); + parsed = new java.util.ArrayList<>(parsed.subList(0, room)); + } + // ★ 药效等级封顶(2026-10-04 任务4):逐个效果按「生存可获取上限 + 用户组配置」 + for (String[] ef : parsed) { + if (ef.length >= 2) { + try { + int lv = Integer.parseInt(ef[1]); + int capped = clampEffectLevel(lv, ef[0], ac.owner); + if (capped != lv) { + plugin.getLogger().info("[防护] 领地 " + ac.name + " 效果 " + + ef[0] + " 等级 " + lv + " 超出上限,已封顶为 " + capped); + ef[1] = String.valueOf(capped); + } + } catch (NumberFormatException ignored) {} + } + } + } catch (Exception ex) { + plugin.getLogger().warning("[防护] " + field + " 上限校验异常: " + + ex.getMessage()); + } + if (isBad) { + ac.badEffects = parsed; + } else { + ac.giveEffects = parsed; + } + break; + } + case "clear_effects": + ac.clearEffects.clear(); + if (value != null && !value.isEmpty()) { + ac.clearEffects.addAll(splitToList(value)); + } + break; + default: + plugin.getLogger().warning("[防护] applyFieldToAreaConfig: 未处理字段 " + field); + break; + } + } + + /** + * 从 ["name","level","dur"] 格式提取效果数组 + */ + private String[] parseEffectJson(String s) { + try { + String inner = s.trim(); + if (inner.startsWith("[") && inner.endsWith("]")) inner = inner.substring(1, inner.length() - 1); + String[] parts = inner.split(","); + String name = parts[0].trim().replaceAll("^\"|\"$", ""); + String lvl = parts.length > 1 ? parts[1].trim() : "1"; + String dur = parts.length > 2 ? parts[2].trim() : "60"; + return new String[]{name, lvl, dur}; + } catch (Exception e) { + return new String[]{"", "1", "60"}; + } + } + + /** + * PHP端更新访客/成员权限时调用 + * landName 可能是JSON数组格式 "[领地名]" 或纯领地名 + */ + /** + * 从Web端(PHP)同步访客权限到本地库。 + * @param role 角色标识。为空表示仅调整权限(不改动角色,已授权的管理员不会被降级); + * "admin"/"visitor" 表示显式切换角色(来自 change_visitor_role)。 + */ + public void updateVisitorPermFromWeb(String landName, String playerName, String permsJson, String role) { + if (dbConnection == null) return; + try { + // 解析可能的JSON数组格式 + String[] landNames = landName.startsWith("[") ? parseJsonArray(landName) : new String[]{landName}; + + // 防御:清除PHP双重JSON编码引入的反斜杠(JSON中 uXXXX 被双重转义后残留在领地名中) + for (int i = 0; i < landNames.length; i++) { + if (landNames[i].contains("\\")) { + landNames[i] = landNames[i].replace("\\", ""); + } + } + + for (String lname : landNames) { + int landId = getLandIdFromDb(lname); + if (landId < 0) continue; + + // ★ 角色同步铁律:admin 永远不允许被 PHP 的 perm_change 降级。 + // PHP 的 handleUpdateVisitorPerm 本身已拦截 admin/member, + // 但竞态(admin 在 Java 设置后、同步到 PHP 前,PHP 已发 perm_change) + // 仍可能导致此方法收到 role="visitor" 降级请求。 + // 解决:admin 只能通过 change_visitor_role(显式角色切换)改变, + // perm_change 只更新 permissions JSON,不动 role。 + String effectiveRole = "visitor"; + boolean isAdmin = false; + { + String localRole = getLocalRole(landId, playerName); + if (localRole != null && !localRole.isEmpty()) { + effectiveRole = localRole; + } + isAdmin = "admin".equalsIgnoreCase(effectiveRole); + } + + if (role != null && !role.isEmpty()) { + // ★ PHP显式携带role(来自changeVisitorRole):允许任何角色切换 + effectiveRole = role.toLowerCase(); + if ("admin".equalsIgnoreCase(role)) { + setLandAdmin(lname, playerName, true); + } else if ("member".equals(effectiveRole) || "visitor".equals(effectiveRole)) { + // 降级:取消管理员标记 + setLandAdmin(lname, playerName, false); + } + } else if (isAdmin) { + // 无显式role + 本地是admin:保留admin角色,只更新permissions + effectiveRole = "admin"; + } + // role 为空或 visitor + 非 admin:effectiveRole 保持 localRole 或默认 visitor + + plugin.getLogger().info("[防护][更新权限] 入参 land=" + lname + " player=" + playerName + + " role=" + role + " localRole=" + getLocalRole(landId, playerName) + + " effectiveRole=" + effectiveRole + + " permsLen=" + (permsJson == null ? "null" : String.valueOf(permsJson.length())) + + (role != null && !role.isEmpty() ? " (PHP显式携带role)" : " (沿用本地)")); + + // 更新数据库中的自定义权限(同时写回 effectiveRole,确保新记录角色正确) + // ★ 先按 normName 定位已有行再 UPDATE/INSERT,避免大小写/变种字符 + // 与 UNIQUE(land_id, player_name) 的 BINARY 约束不一致而产生重复行 + PermRow existing = findPermRow(landId, playerName); + if (existing != null) { + PreparedStatement ps = dbConnection.prepareStatement( + "UPDATE area_land_permissions SET permissions = ?, role = ?, granted_at = ? WHERE id = ?"); + ps.setString(1, permsJson); + ps.setString(2, effectiveRole); + ps.setLong(3, System.currentTimeMillis() / 1000); + ps.setInt(4, existing.id); + ps.executeUpdate(); + ps.close(); + } else { + PreparedStatement ps = dbConnection.prepareStatement( + "INSERT INTO area_land_permissions (land_id, player_name, role, permissions, granted_at) " + + "VALUES (?, ?, ?, ?, ?)"); + ps.setInt(1, landId); + ps.setString(2, playerName); + ps.setString(3, effectiveRole); + ps.setString(4, permsJson); + ps.setLong(5, System.currentTimeMillis() / 1000); + ps.executeUpdate(); + ps.close(); + } + } + + plugin.getLogger().info("[防护] PHP端更新权限: " + playerName + " @ " + Arrays.toString(landNames)); + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 更新权限失败: " + e.getMessage()); + } + } + + /** + * 查询本地 area_land_permissions 表中指定玩家在当前领地的角色 + */ + private String getLocalRole(int landId, String playerName) { + PermRow row = findPermRow(landId, playerName); + return row == null ? null : row.role; + } + + /** + * 解析JSON数组格式的领地名字符串 + */ + private String[] parseJsonArray(String json) { + try { + String inner = json.replaceAll("^\\[|\\]$", ""); + String[] parts = inner.split("\""); + java.util.List result = new java.util.ArrayList<>(); + for (String part : parts) { + String trimmed = part.trim().replaceAll("§[0-9a-fk-or]", ""); + if (!trimmed.isEmpty()) result.add(trimmed); + } + return result.toArray(new String[0]); + } catch (Exception e) { + return new String[]{json}; + } + } + + /** + * 格式化时长(秒 → 可读字符串) + */ + private String formatDuration(int seconds) { + if (seconds >= 86400) return (seconds / 86400) + "天"; + if (seconds >= 3600) return (seconds / 3600) + "小时"; + if (seconds >= 60) return (seconds / 60) + "分钟"; + return seconds + "秒"; + } + + /** + * 更新area_config表中的配置值 + */ + private void updateAreaConfig(String key, String value) { + if (dbConnection == null) return; + // ★ 去除前导零:01 → 1,防止静默失败 + try { value = String.valueOf(Integer.parseInt(value.trim())); } catch (Exception ignored) {} + try { + PreparedStatement ps = dbConnection.prepareStatement( + "INSERT INTO area_config (key, value) VALUES (?, ?) " + + "ON CONFLICT(key) DO UPDATE SET value = ?"); + ps.setString(1, key); + ps.setString(2, value); + ps.setString(3, value); + ps.executeUpdate(); + ps.close(); + // ★ 同步更新内存字段,使Web配置变更立即生效(无需等待下次轮询reload) + try { + switch (key) { + case "create_price_per_sqm": globalCreatePricePerSqm = Integer.parseInt(value); break; + case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(value); break; + case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(value); break; + case "default_height": globalDefaultHeight = Integer.parseInt(value); break; + case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(value); break; + case "max_free_area_sqm": globalMaxFreeAreaSqm = Integer.parseInt(value); break; + case "max_land_side_blocks": globalMaxLandSideBlocks = Integer.parseInt(value); break; + } + } catch (NumberFormatException ignored) {} + } catch (Exception e) { + plugin.getLogger().warning("[防护] 更新配置失败: " + e.getMessage()); + } + } + + /** + * ★ 获取全局家(home)数量上限 + */ + public int getGlobalMaxHomePerPlayer() { + return globalMaxHomePerPlayer; + } + + /** + * ★ 获取area_config表中的配置值 + */ + public String getAreaConfigValue(String key) { + if (dbConnection == null) return null; + try { + PreparedStatement ps = dbConnection.prepareStatement("SELECT value FROM area_config WHERE key = ?"); + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String val = rs.getString("value"); + rs.close(); + ps.close(); + return val; + } + rs.close(); + ps.close(); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 读取配置失败: " + e.getMessage()); + } + return null; + } + + /** + * ★ 设置area_config表中的配置值(公开方法) + */ + public void setAreaConfigValue(String key, String value) { + updateAreaConfig(key, value); + } + + /** + * ★ 获取全部area_config配置(用于同步到PHP) + */ + public Map getAllAreaConfigForSync() { + Map config = new LinkedHashMap<>(); + if (dbConnection == null) return config; + try { + ResultSet rs = dbConnection.createStatement().executeQuery("SELECT key, value FROM area_config"); + while (rs.next()) { + config.put(rs.getString("key"), rs.getString("value")); + } + rs.close(); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 读取配置同步数据失败: " + e.getMessage()); + } + return config; + } + + // ==================== 管理员配置输入(GUI→聊天栏) ==================== + + /** + * ★ 设置等待配置输入状态 + */ + public void setPendingConfigInput(java.util.UUID uuid, String configKey) { + pendingConfigInput.put(uuid, configKey); + } + + /** + * ★ 检查并处理配置输入(在onPlayerChat中调用) + * @return true 如果是配置输入,已处理 + */ + public boolean handleConfigInput(Player p, String message) { + String configKey = pendingConfigInput.remove(p.getUniqueId()); + if (configKey == null) return false; + + // ★ 智能数字解析:支持纯数字、中文数字、中文大写、罗马数字、英文数字 + Integer newValue = parseSmartNumber(message.trim()); + if (newValue == null) { + p.sendMessage("§c§l[配置] §f无法识别输入的数字: " + message); + p.sendMessage("§7支持: 纯数字123、中文一二三、大写壹佰贰拾叁、罗马I II III、英文one two three"); + return true; + } + + // ★ 退费比例范围钳制:0=关闭,100=全额 + if ("refund_ratio_per_cent".equals(configKey)) { + if (newValue > 100) newValue = 100; + if (newValue < 0) newValue = 0; + } + + // 读取当前值 + int currentValue = 0; + try { + String v = getAreaConfigValue(configKey); + if (v != null) currentValue = Integer.parseInt(v); + } catch (Exception ignored) {} + + // 应用新值(如果比原来大=加钱,比原来小=减钱,直接使用绝对值) + setAreaConfigValue(configKey, String.valueOf(newValue)); + + // 更新内存变量 + switch (configKey) { + case "create_price_per_sqm": globalCreatePricePerSqm = newValue; break; + case "max_lands_per_player": globalMaxLandsPerPlayer = newValue; break; + case "default_height": globalDefaultHeight = newValue; break; + case "refund_ratio_per_cent": globalRefundRatioPerCent = newValue; break; + case "max_free_area_sqm": globalMaxFreeAreaSqm = Math.max(1, newValue); break; + case "max_land_side_blocks": globalMaxLandSideBlocks = Math.max(1, newValue); break; + } + + String configName = getConfigNameByKey(configKey); + p.sendMessage("§a§l[配置] §f" + configName + " 已更新: §e" + currentValue + " → " + newValue); + + // 刷新GUI(必须切主线程:handleConfigInput来自AsyncPlayerChatEvent) + if (plugin.areaGUIManager != null) { + Sdf1_login.Main pl = plugin; + org.bukkit.entity.Player pp = p; + org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openAdminPanel(pp)); + } + return true; + } + + /** + * 设置等待添加成员输入状态 + */ + public void setPendingAddMemberInput(java.util.UUID uuid, String landName) { + pendingAddMemberInput.put(uuid, landName); + } + + /** + * 检查并处理添加成员输入 + * @return true 如果是添加成员输入,已处理 + */ + public boolean handleAddMemberInput(Player p, String message) { + String landName = pendingAddMemberInput.remove(p.getUniqueId()); + if (landName == null) return false; + + String playerName = message.trim(); + // 验证玩家名格式 + if (!playerName.matches("^[a-zA-Z0-9_]{3,16}$")) { + p.sendMessage("§c§l[添加成员] §f无效的玩家名格式(3-16位字母数字下划线): " + playerName); + p.sendMessage("§7请重新输入有效玩家名,或输入 cancel 取消"); + return true; + } + + // 检查玩家是否已存在(本地) + Player target = Bukkit.getPlayerExact(playerName); + if (target == null) { + // 玩家不在线,但仍然可以添加(因为可能在数据库中有记录) + p.sendMessage("§e§l[添加成员] §f玩家 " + playerName + " 当前不在线,但仍可添加为成员"); + } + + // 检查是否已是成员 + Set members = getLandMembers(landName); + if (members.contains(playerName)) { + p.sendMessage("§c§l[添加成员] §f" + playerName + " 已经是该领地成员"); + return true; + } + + // 检查是否是领地主 + AreaConfig ac = getLand(landName); + if (ac != null && ac.owner != null && samePlayer(ac.owner, playerName)) { + p.sendMessage("§c§l[添加成员] §f领地主不能作为成员添加"); + return true; + } + + // ★ 校验玩家是否在login.db中注册(防止添加不存在的玩家) + DatabaseManager dbMgr = plugin.getDb(); + if (dbMgr != null && !dbMgr.userExists(playerName)) { + p.sendMessage("§c§l[添加成员] §f玩家 " + playerName + " 不存在(未在login.db中注册)"); + p.sendMessage("§7请确认玩家名是否正确,或输入 cancel 取消"); + return true; + } + + // 添加成员 + addLandMember(landName, playerName); + insertLandPermission(landName, playerName, "visitor"); + p.sendMessage("§a§l[添加成员] §f已成功添加 " + playerName + " 为领地成员"); + + // 刷新GUI(必须切主线程) + if (plugin.areaGUIManager != null) { + Sdf1_login.Main pl = plugin; + org.bukkit.entity.Player pp = p; + String ln = landName; + org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openAddMember(pp, ln)); + } + return true; + } + + /** + * 设置等待效果管理输入状态 + * @param subPage 子页面索引(1=开关, 2=单清, 3=增益) + */ + public void setPendingClearEffectInput(java.util.UUID uuid, String landName, String inputType, int subPage) { + pendingEffectInput.put(uuid, new String[]{landName, inputType, String.valueOf(subPage)}); + } + + /** + * ★ 通用设置效果输入状态 + */ + public void setPendingEffectInput(java.util.UUID uuid, String landName, String inputType, int subPage) { + pendingEffectInput.put(uuid, new String[]{landName, inputType, String.valueOf(subPage)}); + } + + /** + * 检查并处理效果管理输入 + * @return true 如果是效果管理输入,已处理 + */ + public boolean handleEffectInput(Player p, String message) { + String[] landAndType = pendingEffectInput.remove(p.getUniqueId()); + if (landAndType == null) return false; + + String landName = landAndType[0]; + String inputType = landAndType[1]; + AreaProtection.AreaConfig land = getLand(landName); + if (land == null) { + p.sendMessage("§c领地不存在"); + return true; + } + + // 获取subPage(如果有) + int subPage = 1; + if (landAndType.length >= 3) { + try { + subPage = Integer.parseInt(landAndType[2]); + } catch (NumberFormatException ignored) { + } + } + + if ("clear".equals(inputType)) { + // 添加单清效果 + String effName = message.trim(); + if (effName.isEmpty()) { + p.sendMessage("§c§l[添加清除效果] §f效果名不能为空"); + return true; + } + // ★ 验证效果名是否有效 + PotionEffectType resolved = resolveEffectType(effName); + if (resolved == null) { + p.sendMessage("§c§l[添加清除效果] §f无效的效果名: §e" + effName); + p.sendMessage("§7支持的中文名: 缓慢、挖掘疲劳、瞬间伤害、反胃、失明、饥饿、虚弱、中毒、凋零、飘浮、霉运、黑暗、蓄风、盘丝、渗浆、寄生"); + p.sendMessage("§7中性: 不祥之兆、袭击之兆、试炼之兆"); + p.sendMessage("§7正面: 迅捷、急迫、力量、瞬间治疗、跳跃提升、生命恢复、抗性提升、抗火、水下呼吸、隐身、夜视、发光、生命提升、伤害吸收、饱和、幸运、村庄英雄、缓降、潮涌能量、海豚的恩惠"); + return true; + } + if (!land.clearEffects.contains(effName)) { + land.clearEffects.add(effName); + saveAreaToDb(land); + p.sendMessage("§a§l[添加清除效果] §f已添加: §e" + effName); + } else { + p.sendMessage("§e§l[添加清除效果] §f效果 §e" + effName + " §f已在列表中"); + } + } else if ("give".equals(inputType)) { + // 添加增益效果: 格式 效果名 [等级] [秒数] + String[] parts = message.trim().split("\\s+"); + if (parts.length < 1) { + p.sendMessage("§c§l[添加增益效果] §f请输入: 效果名 [等级] [秒数]"); + return true; + } + String effName = parts[0].trim(); + int effLv = 1; + int effDur = 300; // 默认5分钟 + try { + if (parts.length >= 2) effLv = Integer.parseInt(parts[1]); + if (parts.length >= 3) effDur = Integer.parseInt(parts[2]); + } catch (NumberFormatException ignored) { + p.sendMessage("§c§l[添加增益效果] §f等级和秒数必须是数字"); + return true; + } + // ★ 验证等级和秒数范围(等级上限按「该效果生存可获取上限 + 用户组配置」,2026-10-04 任务4) + int maxLvAdd = getEffectLevelCap(effName, land.owner); + if (effLv < 1 || effLv > maxLvAdd) { + p.sendMessage("§c§l[添加增益效果] §f§e" + effName + " §f等级范围: 1~" + maxLvAdd); + p.sendMessage("§7上限说明: " + effName + " 生存模式最高可获得 " + getSurvivalMaxLevel(effName) + " 级"); + p.sendMessage("§7你当前领地可用上限 " + maxLvAdd + " 级(受用户组配置约束)"); + return true; + } + if (effDur < 1 || effDur > 3600) { + p.sendMessage("§c§l[添加增益效果] §f持续时间范围: 1~3600秒"); + return true; + } + // ★ 验证效果名是否有效 + PotionEffectType resolved = resolveEffectType(effName); + if (resolved == null) { + p.sendMessage("§c§l[添加增益效果] §f无效的效果名: §e" + effName); + p.sendMessage("§7推荐增益: 迅捷、急迫、力量、瞬间治疗、跳跃提升、生命恢复、抗性提升、抗火、水下呼吸、隐身、夜视、发光、生命提升、伤害吸收、饱和、幸运、村庄英雄、缓降、潮涌能量、海豚的恩惠"); + return true; + } + // ★ 检查是否已存在同名增益 + for (String[] existing : land.giveEffects) { + if (existing[0].equals(effName)) { + p.sendMessage("§e§l[添加增益效果] §f增益 §e" + effName + " §f已存在,先移除旧的再添加"); + return true; + } + } + // ★ 数量上限(2026-10-04 任务3):普通玩家 5 个,用户组可覆盖 + // 2026-10-04 负面拆出后,增益与负面共用同一份 max_effects 预算,故按合计判 + int maxEffCnt = getMaxGiveEffects(land.owner); + if (land.giveEffects.size() + land.badEffects.size() >= maxEffCnt) { + p.sendMessage("§c§l[添加增益效果] §f效果已达上限 §e" + + maxEffCnt + " §f个(增益+负面合计,按用户组配置),请先移除再添加"); + return true; + } + String[] effRecord = {effName, String.valueOf(effLv), String.valueOf(effDur)}; + land.giveEffects.add(effRecord); + saveAreaToDb(land); + p.sendMessage("§a§l[添加增益效果] §f已添加: §a" + effName + " §fLv" + effLv + " " + effDur + "秒"); + } else if (inputType.startsWith("editGive_")) { + // ★ 编辑增益效果等级/时长: editGive_level_3 或 editGive_duration_3 + String[] parts = inputType.split("_"); + if (parts.length < 3) return true; + String editField = parts[1]; // level 或 duration + int idx; + try { idx = Integer.parseInt(parts[2]); } catch (Exception e) { return true; } + if (idx < 1 || idx > land.giveEffects.size()) { + p.sendMessage("§c序号超出范围"); + return true; + } + if (message.trim().equalsIgnoreCase("取消")) { + p.sendMessage("§7已取消编辑"); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3); + return true; + } + String[] eff = land.giveEffects.get(idx - 1); + if ("level".equals(editField)) { + int newLevel; + try { newLevel = Integer.parseInt(message.trim()); } catch (Exception e) { + p.sendMessage("§c请输入有效数字"); + return true; + } + int maxLvEdit = getEffectLevelCap(eff[0], land.owner); + if (newLevel < 1 || newLevel > maxLvEdit) { + p.sendMessage("§c等级范围 1~" + maxLvEdit); + p.sendMessage("§7上限说明: " + eff[0] + " 生存模式最高可获得 " + + getSurvivalMaxLevel(eff[0]) + " 级"); + p.sendMessage("§7你当前领地可用上限 " + maxLvEdit + " 级(受用户组配置约束)"); + return true; + } + if (eff.length < 2) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, eff.length); eff = tmp; land.giveEffects.set(idx - 1, eff); } + eff[1] = String.valueOf(newLevel); + p.sendMessage("§a§l[编辑增益] §f已将 §e" + eff[0] + " §f等级修改为 " + newLevel); + } else { + int newDur; + try { newDur = Integer.parseInt(message.trim()); } catch (Exception e) { + p.sendMessage("§c请输入有效数字"); + return true; + } + if (newDur < 1 || newDur > 86400) { p.sendMessage("§c时长范围1~86400秒"); return true; } + if (eff.length < 3) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, Math.min(eff.length, 3)); eff = tmp; land.giveEffects.set(idx - 1, eff); } + eff[2] = String.valueOf(newDur); + p.sendMessage("§a§l[编辑增益] §f已将 §e" + eff[0] + " §f时长修改为 " + newDur + "秒"); + } + saveAreaToDb(land); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3); + } else if (inputType.startsWith("editBad_")) { + // ★ 编辑负面效果等级/时长: editBad_level_3 或 editBad_duration_3 + // (2026-10-04 负面从增益拆出,回写落到 badEffects 而不是 giveEffects) + String[] parts = inputType.split("_"); + if (parts.length < 3) return true; + String editField = parts[1]; // level 或 duration + int idx; + try { idx = Integer.parseInt(parts[2]); } catch (Exception e) { return true; } + if (idx < 1 || idx > land.badEffects.size()) { + p.sendMessage("§c序号超出范围"); + return true; + } + if (message.trim().equalsIgnoreCase("取消")) { + p.sendMessage("§7已取消编辑"); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 6); + return true; + } + String[] eff = land.badEffects.get(idx - 1); + if ("level".equals(editField)) { + int newLevel; + try { newLevel = Integer.parseInt(message.trim()); } catch (Exception e) { + p.sendMessage("§c请输入有效数字"); + return true; + } + int maxLvEdit = getEffectLevelCap(eff[0], land.owner); + if (newLevel < 1 || newLevel > maxLvEdit) { + p.sendMessage("§c等级范围 1~" + maxLvEdit); + p.sendMessage("§7上限说明: " + eff[0] + " 生存模式最高可获得 " + + getSurvivalMaxLevel(eff[0]) + " 级"); + p.sendMessage("§7你当前领地可用上限 " + maxLvEdit + " 级(受用户组配置约束)"); + return true; + } + if (eff.length < 2) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, eff.length); eff = tmp; land.badEffects.set(idx - 1, eff); } + eff[1] = String.valueOf(newLevel); + p.sendMessage("§c§l[编辑负面] §f已将 §e" + eff[0] + " §f等级修改为 " + newLevel); + } else { + int newDur; + try { newDur = Integer.parseInt(message.trim()); } catch (Exception e) { + p.sendMessage("§c请输入有效数字"); + return true; + } + if (newDur < 1 || newDur > 86400) { p.sendMessage("§c时长范围1~86400秒"); return true; } + if (eff.length < 3) { String[] tmp = new String[3]; System.arraycopy(eff, 0, tmp, 0, Math.min(eff.length, 3)); eff = tmp; land.badEffects.set(idx - 1, eff); } + eff[2] = String.valueOf(newDur); + p.sendMessage("§c§l[编辑负面] §f已将 §e" + eff[0] + " §f时长修改为 " + newDur + "秒"); + } + saveAreaToDb(land); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 6); + } else if ("rename_land".equals(inputType)) { + // ★ 领地改名输入处理 + if (message.trim().equals("取消") || message.trim().equals("0")) { + p.sendMessage(Component.text("§e已取消改名,保持原样")); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, landName, 1); + return true; + } + // ★ URL过滤:包含链接则拒绝,不计入改名次数 + ChatFilterManager cf = plugin.getChatFilter(); + if (cf != null) { + java.util.List urls = cf.extractUrls(message); + if (!urls.isEmpty()) { + p.sendMessage(Component.text("§c领地名不能包含链接!")); + pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"}); + return true; + } + } + String newName = message.trim(); + String error = renameLand(p, landName, newName); + if (error != null) { + p.sendMessage(Component.text("§c改名失败: " + error)); + p.sendMessage(Component.text("§7请重新输入(或输入 §c取消§7)")); + pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"}); + return true; + } + p.sendMessage(Component.text("§a✓ 领地已改名: §f" + landName + " §a→ §f" + newName)); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, newName, 1); + return true; + } else if ("confiscate_add".equals(inputType)) { + // ★ 违禁品管理面板:添加访客禁带物品(2026-10-11) + if (message.trim().equals("取消")) { + p.sendMessage(Component.text("§e已取消添加,保持原样")); + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName); + return true; + } + String itemId = message.trim(); + if (itemId.isEmpty()) { + p.sendMessage(Component.text("§c§l[添加禁带物品] §f物品ID不能为空")); + pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"}); + return true; + } + // ★ 翻译中间人:支持中文名(钻石)与英文ID(diamond、minecraft:gold_ingot)(2026-10-11) + StringBuilder cnHint = new StringBuilder(); + org.bukkit.Material mat = ItemNameResolver.resolveWithHint(itemId, cnHint); + if (mat == null) { + p.sendMessage(Component.text("§c§l[添加禁带物品] §f无效的物品: §e" + itemId)); + p.sendMessage(Component.text("§7支持中文: 钻石、下界合金剑、黑曜石")); + p.sendMessage(Component.text("§7也支持英文: diamond、minecraft:gold_ingot、netherite_sword")); + pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"}); + return true; + } + // 统一为小写标准名,避免重复 + String stdId = mat.name().toLowerCase(); + if (land.confiscateItems.contains(stdId)) { + p.sendMessage(Component.text("§e§l[添加禁带物品] §f物品 §e" + stdId + " §f已在列表中")); + } else { + land.confiscateItems.add(stdId); + saveAreaToDb(land); + String matched = cnHint.length() > 0 + ? " §7(匹配到: " + cnHint + ")" : ""; + p.sendMessage(Component.text("§a§l[添加禁带物品] §f已添加访客禁带物品: §e" + stdId + matched)); + } + if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName); + return true; + } + + // 刷新GUI,回到原来的subPage(必须切主线程) + if (plugin.areaGUIManager != null) { + Sdf1_login.Main pl = plugin; + org.bukkit.entity.Player pp = p; + String ln = landName; + int sp = subPage; + org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openEffectsManagement(pp, ln, sp)); + } + return true; + } + + /** + * ★ 检查并处理过户领地输入(新所有者名) + * @return true 如果是过户输入,已处理 + */ + public boolean handleTransferInput(Player p, String message) { + String[] data = pendingTransferInput.remove(p.getUniqueId()); + if (data == null) return false; + + String areaName = data[0]; + if (message.trim().equalsIgnoreCase("取消")) { + p.sendMessage("§7已取消过户"); + return true; + } + + String newOwner = message.trim(); + // 验证格式 + if (!newOwner.matches("^[a-zA-Z0-9_]{3,16}$")) { + p.sendMessage("§c玩家名格式无效:仅允许英文字母、数字和下划线(3-16位)"); + p.sendMessage("§7请重新输入(或输入 §c取消§7)"); + pendingTransferInput.put(p.getUniqueId(), data); // 恢复等待状态 + return true; + } + + // 验证领地存在 + AreaConfig ac = areas.get(areaName); + if (ac == null) { + p.sendMessage("§c领地不存在: " + areaName); + return true; + } + if (!hasPermission(p, ac, PermissionLevel.OWNER)) { + p.sendMessage("§c需要领地所有者或管理员权限"); + return true; + } + if (samePlayer(newOwner, ac.owner)) { + p.sendMessage("§c不能转让给自己"); + return true; + } + + // 验证新所有者是否存在 + DatabaseManager dbMgr = plugin.getDb(); + if (dbMgr != null && !dbMgr.userExists(newOwner)) { + p.sendMessage("§c玩家 §e" + newOwner + " §c尚未注册"); + return true; + } + + // ★ 验证玩家注册时间必须超过5分钟(防止注册秒退玩家接收领地) + if (dbMgr != null) { + long registerTime = 0; + try { + Object regTimeObj = dbMgr.getField(newOwner, "register_time"); + if (regTimeObj instanceof Number) { + // ★ 第十一批:秒/毫秒混存归一化 + registerTime = DurationParser.toMs( + ((Number) regTimeObj).longValue()); + } + } catch (Exception e) { + // 忽略异常 + } + if (registerTime > 0) { + long now = System.currentTimeMillis(); + long fiveMinutesMs = 5 * 60 * 1000; + if (now - registerTime < fiveMinutesMs) { + long minutesLeft = (fiveMinutesMs - (now - registerTime)) / 60000; + p.sendMessage("§c玩家 §e" + newOwner + " §c注册时间不足5分钟(还差" + minutesLeft + "分钟)"); + return true; + } + } + } + + // 执行转让 + String oldOwner = ac.owner; + ac.owner = newOwner; + saveAreaToDb(ac); + + // 获取权限快照 + 追踪cooldown + if (plugin.webManager != null) { + String snapshot = getLandPermissionsSnapshot(areaName); + long expiresAt = System.currentTimeMillis() + 60000; + WebManager.TransferInfo info = new WebManager.TransferInfo( + areaName, oldOwner, newOwner, expiresAt, 0, 0, snapshot + ); + plugin.webManager.trackTransfer(areaName, info); + plugin.webManager.requestImmediateLandSync(); + } + + p.sendMessage("§a已将 §e" + areaName + " §a从 §e" + (oldOwner != null ? oldOwner : "无") + " §a转让给 §e" + newOwner + "§a,§e60秒冷却期内可取消"); + // ★ 可点击取消超链接 + if (p instanceof Player) { + Player pp = (Player) p; + pp.sendMessage(Component.empty() + .append(Component.text("§c§l[点击取消过户] ")) + .hoverEvent(HoverEvent.showText(Component.text("§c点击立即取消领地 §e" + areaName + " §c的过户"))) + .clickEvent(ClickEvent.runCommand("/protect canceltransfer " + areaName)) + ); + } + return true; + } + + /** + * ★ 检查并处理用户组配置编辑输入 + * @return true 如果是用户组编辑输入,已处理 + */ + public boolean handleGroupEditInput(Player p, String message) { + String[] data = pendingGroupEditInput.remove(p.getUniqueId()); + if (data == null) return false; + + String groupName = data[0]; + String field = data[1]; // "price", "maxlands", "priority" + + if (message.trim().equalsIgnoreCase("取消")) { + p.sendMessage("§7已取消编辑"); + if (plugin.areaGUIManager != null) { + Sdf1_login.Main pl = plugin; + org.bukkit.entity.Player pp = p; + String gn = groupName; + org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openGroupEditPanel(pp, gn)); + } + return true; + } + + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { + p.sendMessage("§c用户组系统未初始化"); + return true; + } + UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(groupName); + if (cfg == null) { + p.sendMessage("§c用户组不存在: " + groupName); + return true; + } + + // ★ 智能数字解析:支持中文、罗马、英文数字 + Integer parsed = parseSmartNumberBase(message.trim()); + if (parsed == null) { + p.sendMessage("§c无法解析数字: " + message.trim()); + p.sendMessage("§7支持: 纯数字123、中文一二三、大写壹佰贰拾叁、罗马I II III、英文one two three"); + p.sendMessage("§7或输入 §c取消§7 放弃编辑"); + pendingGroupEditInput.put(p.getUniqueId(), data); // 恢复等待 + return true; + } + + String fieldLabel; + switch (field) { + case "price": + if (parsed < 0 || parsed > 999999) { + p.sendMessage("§c价格范围: 0~999999(0=使用全局默认)"); + pendingGroupEditInput.put(p.getUniqueId(), data); + return true; + } + cfg.landPricePerSqm = parsed; + fieldLabel = "每㎡价格"; + break; + case "maxlands": + if (parsed < 0 || parsed > 999) { + p.sendMessage("§c最大领地数范围: 0~999(0=使用全局默认)"); + pendingGroupEditInput.put(p.getUniqueId(), data); + return true; + } + cfg.maxLands = parsed; + fieldLabel = "最大领地数"; + break; + case "priority": + if (parsed < 0 || parsed > 100) { + p.sendMessage("§c优先级范围: 0~100"); + pendingGroupEditInput.put(p.getUniqueId(), data); + return true; + } + cfg.priority = parsed; + fieldLabel = "优先级"; + break; + default: + return true; + } + + // 保存 + ugm.updateGroupConfig(cfg); + p.sendMessage("§a已将用户组 §e" + cfg.displayName + " §a的 §f" + fieldLabel + " §a修改为 §e" + parsed); + + // 刷新GUI(必须切主线程) + if (plugin.areaGUIManager != null) { + Sdf1_login.Main pl = plugin; + org.bukkit.entity.Player pp = p; + String gn = groupName; + org.bukkit.Bukkit.getScheduler().runTask(pl, () -> pl.areaGUIManager.openGroupEditPanel(pp, gn)); + } + return true; + } + + private String getConfigNameByKey(String key) { + switch (key) { + case "create_price_per_sqm": return "创建价格(每㎡)"; + case "max_lands_per_player": return "每人最大领地数"; + case "default_height": return "默认高度"; + case "peace_mode_max_duration": return "和平模式最大时长(秒)"; + case "refund_ratio_per_cent": return "删除退费比例(%)"; + case "max_free_area_sqm": return "单次扩建免审面积上限(㎡)"; + case "max_land_side_blocks": return "领地免审边长上限(格)"; + default: return key; + } + } + + /** + * ★ 智能数字解析:支持多种格式 + * 纯数字、中文数字、中文大写、罗马数字、英文数字 + */ + public static Integer parseSmartNumber(String input) { + if (input == null || input.isEmpty()) return null; + input = input.trim().toLowerCase(); + + // ★ 支持 +/- 前缀(相对增减) + if (input.startsWith("+") || input.startsWith("-")) { + // +/- 前缀需要有后续数字 + String numPart = input.substring(1).trim(); + if (numPart.isEmpty()) return null; + Integer delta = parseSmartNumberBase(numPart); + if (delta == null) return null; + return input.startsWith("-") ? -delta : delta; + } + + return parseSmartNumberBase(input); + } + + /** + * ★ 智能数字解析(带默认值和下限) + * @param input 用户输入 + * @param current 当前值(用于 +/- 计算) + * @param floor 最小值(封底) + */ + public static int parseSmartNumber(String input, int current, int floor) { + if (input == null || input.isEmpty()) return current; + input = input.trim().toLowerCase(); + + // ★ 支持 +/- 前缀(相对增减) + if (input.startsWith("+") || input.startsWith("-")) { + String numPart = input.substring(1).trim(); + if (numPart.isEmpty()) return current; + Integer delta = parseSmartNumberBase(numPart); + if (delta == null) return current; + int result = input.startsWith("-") ? current - delta : current + delta; + return Math.max(result, floor); + } + + // 纯数字(绝对值) + Integer abs = parseSmartNumberBase(input); + if (abs == null) return current; + return Math.max(abs, floor); + } + + public static Integer parseSmartNumberBase(String input) { + if (input == null || input.isEmpty()) return null; + input = input.trim().toLowerCase(); + + // 1. 纯数字 + try { + return Integer.parseInt(input); + } catch (Exception ignored) {} + + // 2. 中文数字(小写) + Integer result = parseChineseNumber(input); + if (result != null) return result; + + // 3. 罗马数字 + result = parseRomanNumber(input); + if (result != null) return result; + + // 4. 英文数字单词 + result = parseEnglishNumber(input); + if (result != null) return result; + + return null; + } + + /** + * 解析中文数字(一~九十九万九千九百九十九) + */ + private static Integer parseChineseNumber(String input) { + if (input.isEmpty()) return null; + + // 中文大写数字映射 + String[][] chineseMap = { + {"零", "0"}, {"一", "1"}, {"二", "2"}, {"三", "3"}, {"四", "4"}, + {"五", "5"}, {"六", "6"}, {"七", "7"}, {"八", "8"}, {"九", "9"}, + {"壹", "1"}, {"贰", "2"}, {"叁", "3"}, {"肆", "4"}, {"伍", "5"}, + {"陆", "6"}, {"柒", "7"}, {"捌", "8"}, {"玖", "9"}, + {"十", "10"}, {"百", "100"}, {"千", "1000"}, {"万", "10000"}, + {"拾", "10"}, {"佰", "100"}, {"仟", "1000"} + }; + + // 替换中文大写为小写 + String normalized = input; + for (String[] pair : chineseMap) { + normalized = normalized.replace(pair[0], pair[1]); + } + + // 检查是否包含数字 + if (!normalized.matches("[0-9零一二三四五六七八九十百千万]+")) { + return null; + } + + // 简单解析:直接替换后计算 + try { + // 处理 "十二" 这种格式 → "12" + normalized = normalized.replace("零", ""); + normalized = normalized.replace("一十", "10"); + normalized = normalized.replace("二十", "20"); + normalized = normalized.replace("三十", "30"); + normalized = normalized.replace("四十", "40"); + normalized = normalized.replace("五十", "50"); + normalized = normalized.replace("六十", "60"); + normalized = normalized.replace("七十", "70"); + normalized = normalized.replace("八十", "80"); + normalized = normalized.replace("九十", "90"); + + // 处理 "一百" 格式 + if (normalized.contains("百")) { + String[] parts = normalized.split("百"); + int hundreds = Integer.parseInt(parts[0]) * 100; + int tens = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; + return hundreds + tens; + } + + // 处理 "千" 格式 + if (normalized.contains("千")) { + String[] parts = normalized.split("千"); + int thousands = Integer.parseInt(parts[0]) * 1000; + int rest = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; + return thousands + rest; + } + + // 处理 "万" 格式 + if (normalized.contains("万")) { + String[] parts = normalized.split("万"); + int tenThousands = Integer.parseInt(parts[0]) * 10000; + int rest = parts.length > 1 && !parts[1].isEmpty() ? Integer.parseInt(parts[1]) : 0; + return tenThousands + rest; + } + + // 纯数字 + return Integer.parseInt(normalized); + } catch (Exception e) { + return null; + } + } + + /** + * 解析罗马数字 + */ + private static Integer parseRomanNumber(String input) { + input = input.toUpperCase(); + if (!input.matches("[IVXLCDM]+")) return null; + + int result = 0; + int prev = 0; + for (int i = input.length() - 1; i >= 0; i--) { + int val = 0; + switch (input.charAt(i)) { + case 'I': val = 1; break; + case 'V': val = 5; break; + case 'X': val = 10; break; + case 'L': val = 50; break; + case 'C': val = 100; break; + case 'D': val = 500; break; + case 'M': val = 1000; break; + } + if (val < prev) result -= val; + else result += val; + prev = val; + } + return result > 0 ? result : null; + } + + /** + * 解析英文数字单词 + */ + private static Integer parseEnglishNumber(String input) { + input = input.toLowerCase().trim(); + switch (input) { + case "zero": return 0; + case "one": return 1; + case "two": return 2; + case "three": return 3; + case "four": return 4; + case "five": return 5; + case "six": return 6; + case "seven": return 7; + case "eight": return 8; + case "nine": return 9; + case "ten": return 10; + case "eleven": return 11; + case "twelve": return 12; + case "thirteen": return 13; + case "fourteen": return 14; + case "fifteen": return 15; + case "sixteen": return 16; + case "seventeen": return 17; + case "eighteen": return 18; + case "nineteen": return 19; + case "twenty": return 20; + case "thirty": return 30; + case "forty": return 40; + case "fifty": return 50; + case "sixty": return 60; + case "seventy": return 70; + case "eighty": return 80; + case "ninety": return 90; + case "hundred": return 100; + case "thousand": return 1000; + default: return null; + } + } + + /** + * 检查玩家的访客权限是否有效(未过期) + */ + public boolean hasValidVisitorPermission(Player player, AreaConfig ac) { + if (dbConnection == null || ac == null) return false; + int landId = getLandIdFromDb(ac.name); + if (landId <= 0) return false; + // ★ 2026-10-10 起仅精确匹配:离线模式大小写不同=不同玩家, + // normName 模糊匹配会让大小写变种玩家冒充访客身份、绕过全部 deny* 限制。 + PermRow row = findPermRowExact(landId, player.getName()); + if (row == null || !"visitor".equalsIgnoreCase(row.role)) return false; + return row.expiresAt == 0 || row.expiresAt > System.currentTimeMillis(); + } + + /** 显示用户组管理面板(CLI可点击交互) */ + private void showGroupListPanel(CommandSender sender) { + UserGroupManager ugm = plugin.getUserGroup(); + if (ugm == null) { sender.sendMessage("§c用户组系统未初始化"); return; } + + Map groups = ugm.getGroupConfigs(); + sender.sendMessage("§a§l========== 用户组管理 =========="); + + if (groups.isEmpty()) { + sender.sendMessage("§7当前暂无用户组定义"); + sender.sendMessage("§7你可以通过以下方式创建用户组:"); + sender.sendMessage("§e /protect groupset <组名> [显示名] [颜色] [优先级] [价格] [上限]"); + sender.sendMessage("§7或通过PHP管理后台创建"); + } else { + sender.sendMessage("§f共 §a" + groups.size() + " §f个用户组:"); + sender.sendMessage(""); + for (UserGroupManager.UserGroupConfig cfg : groups.values()) { + String priceStr = cfg.landPricePerSqm >= 0 ? cfg.landPricePerSqm + "/㎡" : "全局默认"; + String maxStr = cfg.maxLands >= 0 ? String.valueOf(cfg.maxLands) : "全局默认"; + sender.sendMessage("§f" + cfg.displayColor + "§l" + cfg.displayName + + " §7(ID=" + cfg.name + ", 优先级=" + cfg.priority + ")"); + sender.sendMessage("§7 价格=" + priceStr + " 上限=" + maxStr + " 默认权限=" + cfg.defaultPerms); + // ★ 每个组提供编辑和删除操作入口 + if (sender instanceof Player) { + Player pp = (Player) sender; + // ★ 成员管理 + pp.sendMessage(Component.empty() + .append(Component.text(" §a[成员管理] ")) + .hoverEvent(HoverEvent.showText(Component.text("§a管理此用户组的成员"))) + .clickEvent(ClickEvent.runCommand("/protect groupmembers " + cfg.name)) + ); + pp.sendMessage(Component.empty() + .append(Component.text(" §e[编辑] ")) + .hoverEvent(HoverEvent.showText(Component.text("§e交互式编辑此用户组配置"))) + .clickEvent(ClickEvent.runCommand("/protect groupedit " + cfg.name)) + ); + if (!cfg.name.equals(UserGroupManager.DEFAULT_GROUP)) { + pp.sendMessage(Component.empty() + .append(Component.text(" §c[删除] ")) + .hoverEvent(HoverEvent.showText(Component.text("§c删除此用户组"))) + .clickEvent(ClickEvent.runCommand("/protect groupdelconfig " + cfg.name)) + ); + } + } + } + } + + // 操作入口 + sender.sendMessage(""); + if (sender instanceof Player) { + Player pp = (Player) sender; + // 创建新组 + pp.sendMessage(Component.empty() + .append(Component.text("§a[创建新用户组] §e点击交互式创建")) + .hoverEvent(HoverEvent.showText(Component.text("§e点击开始创建流程"))) + .clickEvent(ClickEvent.suggestCommand("/protect groupset ")) + ); + // 从PHP同步 + pp.sendMessage(Component.empty() + .append(Component.text("§b[从PHP同步] §e点击拉取PHP端用户组")) + .hoverEvent(HoverEvent.showText(Component.text("§e从管理后台同步用户组数据"))) + .clickEvent(ClickEvent.runCommand("/protect groupset sync")) + ); + // ★ 返回领地系统首页(CLI模式) + pp.sendMessage(Component.empty() + .append(Component.text("§7[返回领地首页] ")) + .hoverEvent(HoverEvent.showText(Component.text("§7返回领地系统主菜单"))) + .clickEvent(ClickEvent.runCommand("/protect")) + ); + } + sender.sendMessage("§7§l─────────────────────────────────"); + } + + /** + * ★ 用户组CLI交互式编辑面板 + * 每个字段显示当前值,点击可输入新值 + */ + private void showGroupEditCLI(Player p, UserGroupManager.UserGroupConfig cfg) { + p.sendMessage(Component.text("§6§l────────── 编辑用户组: " + cfg.displayName + " ──────────")); + + // 价格 + p.sendMessage(Component.empty() + .append(Component.text("§a每㎡价格: §f" + cfg.landPricePerSqm + " ")) + .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " price " + (cfg.landPricePerSqm + 1)))) + .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " price " + Math.max(0, cfg.landPricePerSqm - 1)))) + .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " price "))) + ); + p.sendMessage(Component.empty() + .append(Component.text(" §7点击§e[输入]§7后在聊天栏输入新值(支持中文/罗马数字)")) + ); + + // 最大领地数 + p.sendMessage(Component.empty() + .append(Component.text("§a最大领地数: §f" + cfg.maxLands + " ")) + .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " maxlands " + (cfg.maxLands + 1)))) + .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " maxlands " + Math.max(0, cfg.maxLands - 1)))) + .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " maxlands "))) + ); + + // 优先级 + p.sendMessage(Component.empty() + .append(Component.text("§a优先级: §f" + cfg.priority + " ")) + .append(Component.text("§a[+1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " priority " + (cfg.priority + 1)))) + .append(Component.text("§c[-1] ").clickEvent(ClickEvent.runCommand("/protect groupset " + cfg.name + " priority " + Math.max(0, cfg.priority - 1)))) + .append(Component.text("§e[输入] ").clickEvent(ClickEvent.suggestCommand("/protect groupset " + cfg.name + " priority "))) + ); + + p.sendMessage(""); + // 返回按钮 + p.sendMessage(Component.empty() + .append(Component.text("§c[返回用户组列表] ")) + .hoverEvent(HoverEvent.showText(Component.text("§c返回用户组管理列表"))) + .clickEvent(ClickEvent.runCommand("/protect grouplist")) + ); + p.sendMessage(Component.text("§6§l─────────────────────────────────")); + } + + private void showHelp(CommandSender s) { + s.sendMessage("§e§l==== 区域防护 ===="); + s.sendMessage("§a§l点击下方命令可直接执行:"); + + // 可点击的帮助消息 + sendClickableHelp(s, "/protect menu", "打开GUI菜单"); + sendClickableHelp(s, "/protect 工具", "获取选地工具"); + sendClickableHelp(s, "/protect 创建", "创建区域(先选点)"); + sendClickableHelp(s, "/protect 列表", "区域列表"); + sendClickableHelp(s, "/protect 重载", "重载配置"); + sendClickableHelp(s, "/protect 删除", "删除当前所在区域"); + sendClickableHelp(s, "/protect info", "当前领地信息"); + sendClickableHelp(s, "/protect add", "玩家加白(需在领地内)"); + sendClickableHelp(s, "/protect remove", "玩家删白(需在领地内)"); + sendClickableHelp(s, "/protect additem", "物品加黑"); + sendClickableHelp(s, "/protect removeitem", "物品删黑"); + sendClickableHelp(s, "/protect on", "显示边框"); + sendClickableHelp(s, "/protect off", "关闭边框"); + sendClickableHelp(s, "/protect tempon", "临时边框(15秒)"); + sendClickableHelp(s, "/protect list", "列出白名单(需在领地内)"); + sendClickableHelp(s, "/protect listitem", "列出物品黑名单"); + sendClickableHelp(s, "/protect config", "查看/修改全局配置"); + sendClickableHelp(s, "/protect expand [格数]", "扩建当前领地(超限转审批)"); + s.sendMessage("§e§l---- 面积审批(管理员,需在现场) ----"); + sendClickableHelp(s, "/protect accept <领地|申请人>", "批准超限扩建/创建"); + sendClickableHelp(s, "/protect deny <领地|申请人>", "驳回超限扩建/创建"); + + s.sendMessage("§e§l---- 权限管理 ----"); + sendClickableHelp(s, "/protect setowner", "设置当前领地所有者(管理员)"); + sendClickableHelp(s, "/protect addvisitor", "添加访客(需在领地内)"); + sendClickableHelp(s, "/protect removevisitor", "移除访客(需在领地内)"); + sendClickableHelp(s, "/protect listvisitors", "列出访客(需在领地内)"); + sendClickableHelp(s, "/protect transfer", "转让领地(需在领地内)"); + sendClickableHelp(s, "/protect settp", "设置领地传送点"); + sendClickableHelp(s, "/protect tp <领地>", "传送到领地传送点"); + + s.sendMessage("§e§l---- 权限商店 ----"); + sendClickableHelp(s, "/protect shop list", "查看在售权限"); + sendClickableHelp(s, "/protect shop my", "查看我的权限"); + sendClickableHelp(s, "/protect info", "当前领地信息"); + + s.sendMessage("§e§l---- 设置 ----"); + sendClickableHelp(s, "/protect uimode", "切换UI模式(GUI/CLI)"); + + s.sendMessage("§b§l欢迎游玩草原探险服务器"); + s.sendMessage("§b§l服务器ip:mc2.ypshidifu.cn 端口30679"); + } + + /** + * 发送可点击的帮助消息 + * 玩家点击后自动执行命令 + */ + private void sendClickableHelp(CommandSender s, String command, String description) { + if (!(s instanceof Player)) { + // 控制台直接显示纯文本 + s.sendMessage("§a" + command + " §7" + description); + return; + } + + Player p = (Player) s; + // 使用Adventure API创建可点击消息 + net.kyori.adventure.text.Component msg = net.kyori.adventure.text.Component.empty() + .append(net.kyori.adventure.text.Component.text("§a§l" + command + " ")) + .append(net.kyori.adventure.text.Component.text("§7" + description) + .hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText( + net.kyori.adventure.text.Component.text("§e点击执行该命令"))) + .clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand(command))); + p.sendMessage(msg); + } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return whiteContains(globalPlayerWhitelist, playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + // ★ 2026-10-10 精确存储原始拼写(离线模式大小写不同=不同玩家) + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (!ok) ok = globalPlayerWhitelist.remove(playerName.toLowerCase()); + if (!ok) ok = globalPlayerWhitelist.remove(normName(playerName)); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + return whiteContains(areaPlayerWhitelist.get(areaName), playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + // ★ 2026-10-10 精确存储原始拼写(离线模式大小写不同=不同玩家) + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = false; + if (wl != null) { + // ★ 精确 / 旧版默认Locale小写 / normName 三种格式全部尝试移除 + ok = wl.remove(playerName); + if (!ok) ok = wl.remove(playerName.toLowerCase()); + if (!ok) ok = wl.remove(normName(playerName)); + if (ok) saveWhitelists(); + } + + // ★ 无论内存白名单是否有该区域条目,都同步删除 area_land_permissions 记录: + // 旧版在 wl == null 时直接返回,DB 中残留的 admin/visitor 行会让玩家 + // 继续被 getPermissionLevel 判为 ADMIN/VISITOR,从而绕过 denyMove。 + if (dbConnection != null) { + try { + int landId = getLandIdFromDb(areaName); + if (landId > 0) { + // 按 normName 匹配删除所有大小写/变种变体行,确保身份一并清除 + for (PermRow row : findPermRows(landId, playerName)) { + PreparedStatement ps = dbConnection.prepareStatement( + "DELETE FROM area_land_permissions WHERE id = ?"); + ps.setInt(1, row.id); + ps.executeUpdate(); + ps.close(); + } + } + } catch (SQLException e) { + plugin.getLogger().warning("[防护] 移除成员时清理权限失败: " + e.getMessage()); + } + } + return ok; + } + + + + public int handleConfiscate(Player p, AreaConfig ac) { + // 管理员和领地所有者免检 + if (hasPermission(p, ac, PermissionLevel.OWNER)) return 0; + Set allItems = new HashSet<>(ac.confiscateItems); + allItems.addAll(globalItemBlacklist); + Set areaList = areaItemBlacklist.get(ac.name); + if (areaList != null) allItems.addAll(areaList); + + if (allItems.isEmpty()) return 0; + + int totalRemoved = 0; + String confiscatedNames = ""; + // ★ 违禁品管理面板(2026-10-11):捕获被没收的物品实体,供退出领地时归还 + java.util.List captured = new ArrayList<>(); + + for (String itemId : allItems) { + // 过滤掉区域名和无效ID + Material mat = Material.matchMaterial(itemId); + if (mat == null) { + plugin.getLogger().warning( + "[防护] 跳过无效物品: " + itemId); + continue; + } + ItemStack[] all = p.getInventory().getContents(); + int count = 0; + for (int i = 0; i < all.length; i++) { + if (all[i] != null && all[i].getType() == mat) { + count += all[i].getAmount(); + captured.add(all[i].clone()); + all[i] = null; + } + } + p.getInventory().setContents(all); + + ItemStack off = p.getInventory().getItemInOffHand(); + if (off != null && off.getType() == mat) { + count += off.getAmount(); + captured.add(off.clone()); + p.getInventory().setItemInOffHand(null); + } + + if (count > 0) { + totalRemoved += count; + if (!confiscatedNames.isEmpty()) + confiscatedNames += "§7、§e"; + confiscatedNames += "§e" + matname(mat) + + " x" + count; + } + } + + if (totalRemoved > 0) { + // ★ 没收计数 +1 并落库(领地主可在违禁品面板查看累计没收次数) + ac.confiscateCount++; + saveAreaToDb(ac); + // ★ 暂存被没收物品:退出领地时按 returnOnLeave 决定是否归还 + java.util.Map> byPlayer = + confiscatedStore.computeIfAbsent(p.getUniqueId(), + k -> new ConcurrentHashMap<>()); + java.util.List store = byPlayer.computeIfAbsent(ac.name, + k -> new ArrayList<>()); + store.addAll(captured); + + String msg = (ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty()) + ? ac.confiscateMsg + : "你携带了违禁品,已被没收"; + // ★ 替换变量 + msg = msg.replace("{player}", p.getName()) + .replace("{area}", ac.name) + .replace("{count}", String.valueOf(totalRemoved)) + .replace("{items}", confiscatedNames); + p.sendMessage("§c§l[区域防护] §f" + msg); + p.sendMessage("§7没收物品: " + confiscatedNames); + + if (ac.enableAnnounce + && ac.announceTemplate != null + && !ac.announceTemplate.isEmpty()) { + String ann = ac.announceTemplate + .replace("{player}", p.getName()) + .replace("{area}", ac.name) + .replace("{count}", String.valueOf(totalRemoved)) + .replace("{items}", confiscatedNames); + Bukkit.broadcastMessage("§c§l[区域防护] §f" + ann); + } + for (String cmd : ac.punishCommands) { + String finalCmd = cmd + .replace("{player}", p.getName()) + .replace("{area}", ac.name) + .replace("{count}", String.valueOf(totalRemoved)); + Bukkit.getScheduler().runTask(plugin, + () -> Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), finalCmd)); + } + } + return totalRemoved; + } + public void clearBlacklists() { + globalItemBlacklist.clear(); + areaItemBlacklist.clear(); + plugin.getLogger().info("[防护] 已清空所有黑名单"); + } + + /** + * ★ 违禁品归还(2026-10-11 违禁品管理面板)。 + * + *

玩家离开领地时调用:若该领地 {@code returnOnLeave=true},把此前 + * {@link #handleConfiscate} 暂存的没收物品退还给玩家(背包满则掉落脚下); + * 若为 false(默认),直接清空暂存(不退还)。两种情况都会清掉暂存记录。 + */ + public void handleConfiscateReturn(Player p, String areaName) { + if (areaName == null) return; + java.util.Map> byPlayer = + confiscatedStore.get(p.getUniqueId()); + if (byPlayer == null) return; + java.util.List store = byPlayer.remove(areaName); + if (byPlayer.isEmpty()) confiscatedStore.remove(p.getUniqueId()); + if (store == null || store.isEmpty()) return; + + AreaConfig ac = areas.get(areaName); + if (ac == null || !ac.returnOnLeave) { + // 默认不退还:仅清空暂存 + return; + } + + int returned = 0; + StringBuilder names = new StringBuilder(); + for (ItemStack it : store) { + if (it == null || it.getType().isAir()) continue; + java.util.HashMap left = + p.getInventory().addItem(it); + if (!left.isEmpty()) { + // 背包放不下:剩余部分掉落脚下 + for (ItemStack drop : left.values()) { + p.getWorld().dropItemNaturally(p.getLocation(), drop); + } + } + returned += it.getAmount(); + if (names.length() > 0) names.append("§7、§e"); + names.append("§e").append(matname(it.getType())) + .append(" x").append(it.getAmount()); + } + if (returned > 0) { + p.sendMessage("§a§l[区域防护] §f你离开领地 §e" + areaName + + "§f,已退还没收物品: " + names); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onEntityDamage( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + // ★ 和平模式攻击免疫:敌对生物(含投射物)对玩家的攻击无效 + if (entity instanceof Player) { + boolean hostileAttack = false; + Entity damager = e.getDamager(); + if (isHostile(damager.getType().name())) { + hostileAttack = true; + } else if (damager instanceof org.bukkit.entity.Projectile) { + // 投射物(箭/三叉戟等):检查shooter是否敌对生物 + org.bukkit.entity.Projectile proj = (org.bukkit.entity.Projectile) damager; + if (proj.getShooter() instanceof Entity) { + hostileAttack = isHostile(((Entity) proj.getShooter()).getType().name()); + } + } + if (hostileAttack) { + Player victim = (Player) entity; + AreaConfig pac = getArea( + victim.getWorld().getName(), + victim.getLocation().getBlockX(), + victim.getLocation().getBlockY(), + victim.getLocation().getBlockZ()); + if (pac != null && pac.peaceMode) { + e.setCancelled(true); + return; + } + } + } + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + // ★ 统一白名单检查(支持per-player权限) + AreaConfig ac = findFrameArea(entity); + if (ac == null) return; + // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (!getEffectiveDeny(p, ac, "denyItemFrame")) return; + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== 攻击生物逻辑 ===== + if (e.getDamager() instanceof Player && !(e.getEntity() instanceof Player)) { + Player mobAttacker = (Player) e.getDamager(); + AreaConfig acMob = getArea( + mobAttacker.getWorld().getName(), + mobAttacker.getLocation().getBlockX(), + mobAttacker.getLocation().getBlockY(), + mobAttacker.getLocation().getBlockZ()); + if (acMob != null && getEffectiveDeny(mobAttacker, acMob, "denyMobAttack")) { + // ★ 公共建筑设施:访客允许攻击敌对生物 + if (!acMob.isPublicBuilding || samePlayer(mobAttacker.getName(), acMob.owner) || isAreaAdmin(mobAttacker)) { + e.setCancelled(true); + mobAttacker.sendMessage("§c§l[区域防护] §f此区域禁止攻击生物"); + return; + } + } + } + + // ===== PVP 逻辑 ===== + if (e.getDamager() instanceof Player && e.getEntity() instanceof Player) { + Player attacker = (Player) e.getDamager(); + Player victim = (Player) e.getEntity(); + + // 攻击者和受害者领地位置 + AreaConfig acAttacker = getArea( + attacker.getWorld().getName(), + attacker.getLocation().getBlockX(), + attacker.getLocation().getBlockY(), + attacker.getLocation().getBlockZ()); + AreaConfig acVictim = getArea( + victim.getWorld().getName(), + victim.getLocation().getBlockX(), + victim.getLocation().getBlockY(), + victim.getLocation().getBlockZ()); + + // ★ denyAllDamage:先检查(无论攻击者/受害者领地都拦) + // 受害者领地禁止所有伤害 → 阻断 + if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyAllDamage")) { + e.setCancelled(true); + attacker.sendMessage("§c§l[区域防护] §f目标领地为绝对安全区"); + return; + } + // 攻击者领地禁止所有伤害 → 阻断 + if (acAttacker != null && getEffectiveDeny(attacker, acAttacker, "denyAllDamage")) { + e.setCancelled(true); + attacker.sendMessage("§c§l[区域防护] §f此区域禁止一切伤害行为"); + return; + } + + // ★ denyPVP:只要任意一方领地开启PVP禁止 → 完全阻断 + // 受害者领地禁止PVP → 阻断 + if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyPVP")) { + e.setCancelled(true); + attacker.sendMessage("§c§l[区域防护] §f该领地禁止PVP"); + return; + } + // 攻击者领地禁止PVP → 阻断 + if (acAttacker != null && getEffectiveDeny(attacker, acAttacker, "denyPVP")) { + e.setCancelled(true); + attacker.sendMessage("§c§l[区域防护] §f此区域禁止PVP"); + return; + } + } + } + + // 展示框被点击(旋转/放入/取出) + @EventHandler + public void onItemFrameInteract( + PlayerInteractAtEntityEvent e) { + Entity clicked = e.getRightClicked(); + String typeName = clicked.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + Player p = e.getPlayer(); + + // ★ 使用统一的权限检查方法(支持per-player权限) + AreaConfig ac = findFrameArea(clicked); + if (ac == null) return; + // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止交互实体"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止交互实体"); + return; + } + // ★ 25批订正倒反天罡:denyItemFrame/denyContainer 走 getEffectiveDeny 豁免链—— + // 领地主/授权成员/服务器管理员豁免,普通访客按领地默认拦截; + // 此前裸字段短路,连领地主交互自己领地的展示框都被拦。 + // 不提前 return——复用下方统一的 取消+下tick旋转恢复+消息 流程。 + if (!getEffectiveDeny(p, ac, "denyItemFrame") && !getEffectiveDeny(p, ac, "denyContainer")) return; + + // 非白名单 → 拦截 + // 记录当前旋转角度 + final Rotation savedRotation; + final Entity frame = clicked; + if (clicked instanceof org.bukkit.entity.ItemFrame) { + savedRotation = + ((org.bukkit.entity.ItemFrame) clicked) + .getRotation(); + } else { + savedRotation = null; + } + + e.setCancelled(true); + + // 下一tick强制恢复旋转 + if (savedRotation != null) { + final Rotation restore = savedRotation; + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (frame.isValid() + && frame instanceof org.bukkit.entity.ItemFrame) { + ((org.bukkit.entity.ItemFrame) frame) + .setRotation(restore); + } + }, 1L); + } + + p.sendMessage("§c§l[区域防护] §f禁止交互展示框"); + + // 驱逐远程敌对生物 + banRangedHostiles(p, ac); + } + + + + // 展示框保护:检查展示框本身+周围方块所属区域 + + // ===== 展示框攻击拦截(左键/投射物破坏)===== + @EventHandler(priority = EventPriority.HIGHEST) + public void onEntityDamageByEntity( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + // PVP逻辑在onEntityDamage()中统一处理,此处仅处理展示框攻击 + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + // 展示框被攻击 + // ★ 防爆模式:非玩家 damager(苦力怕/凋零/闪电/投射物实体等)→ denyItemFrame 区域一律取消 + if (!(e.getDamager() instanceof Player)) { + AreaConfig guardAc = findFrameArea(entity); + if (guardAc != null && guardAc.denyItemFrame) { + e.setCancelled(true); + } + return; + } + Player p = (Player) e.getDamager(); + + AreaConfig ac = findFrameArea(entity); + if (ac == null) return; + + // ★ 25批订正倒反天罡:玩家攻击走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, + // 普通访客按领地默认拦截;非玩家 damager(苦力怕/凋零/闪电等)走上方裸字段硬保护(24批防爆)。 + if (getEffectiveDeny(p, ac, "denyItemFrame")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + // ★ 三级:展示框同时受denyItemFrame和denyContainer控制 + if (!getEffectiveDeny(p, ac, "denyItemFrame") && !getEffectiveDeny(p, ac, "denyContainer")) return; + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // 船、矿车、盔甲架等实体破坏拦截(受 L1 破坏总开关 + L2 实体交互管控) + if (entity instanceof org.bukkit.entity.Vehicle + || entity instanceof org.bukkit.entity.ArmorStand) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + // ★ 取区域方式与放置一致:优先玩家位置,实体位置兜底 + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + Location loc = entity.getLocation(); + ac = getArea( + p.getWorld().getName(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ()); + } + if (ac == null) return; + // L1: 破坏总开关 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + // L2: 实体交互 + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + } + return; + } + } + + // ===== ★ 载具破坏拦截(船/矿车等):VehicleDestroyEvent 是破坏的权威事件 ===== + // 仅靠 EntityDamageByEntityEvent 取消伤害,在部分版本/路径下不足以阻止载具被移除并掉落, + // 此处直接拦截破坏事件本身,确保 L1破坏总开关 / L2实体交互 对载具稳定生效。 + @EventHandler(priority = EventPriority.HIGHEST) + public void onVehicleDestroy(org.bukkit.event.vehicle.VehicleDestroyEvent e) { + org.bukkit.entity.Vehicle v = e.getVehicle(); + Entity attacker = e.getAttacker(); + if (!(attacker instanceof Player)) return; // 仅拦截玩家主动破坏,环境破坏(如撞击)不在此处理 + Player p = (Player) attacker; + // ★ 取区域方式与放置一致:优先载具坐标,玩家坐标兜底 + AreaConfig ac = getArea( + v.getWorld().getName(), + v.getLocation().getBlockX(), + v.getLocation().getBlockY(), + v.getLocation().getBlockZ()); + if (ac == null) { + ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + } + if (ac == null) return; + // 层级:L1 破坏总开关 → L2 实体交互 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏载具"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏载具"); + } + } + + // ===== ★ 载具乘坐拦截:VehicleEnterEvent 是骑乘的权威事件 ===== + // 当玩家右键已放置的船/矿车进入时触发,需检查实体交互权限。 + @EventHandler(priority = EventPriority.HIGHEST) + public void onVehicleEnter(org.bukkit.event.vehicle.VehicleEnterEvent e) { + Entity entered = e.getEntered(); + if (!(entered instanceof Player)) return; // 只处理玩家乘坐 + Player p = (Player) entered; + org.bukkit.entity.Vehicle v = e.getVehicle(); + // ★ 区域取法与放置/破坏一致:优先载具坐标,玩家坐标兜底 + AreaConfig ac = getArea( + v.getWorld().getName(), + v.getLocation().getBlockX(), + v.getLocation().getBlockY(), + v.getLocation().getBlockZ()); + if (ac == null) { + ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + } + if (ac == null) return; + // 层级:L2 实体交互 → L3 骑乘 + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止乘坐载具"); + return; + } + if (getEffectiveDeny(p, ac, "denyMount")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止乘坐载具"); + } + } + + // ===== ★ 悬挂物破坏拦截(展示框/画等):HangingBreakEvent 是破坏的权威事件 ===== + // ★ 展示框防爆:denyItemFrame 开启 → 非个人破坏(苦力怕/TNT/凋零之首爆炸、闪电、 + // 非玩家投射物、环境等)一律硬拦截(24批防爆,无豁免链); + // 玩家破坏走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, + // 普通访客按领地默认拦截(25批订正「倒反天罡」:此前连领地主手动破坏都被拦)。 + @EventHandler(priority = EventPriority.HIGHEST) + public void onHangingBreak(org.bukkit.event.hanging.HangingBreakEvent e) { + org.bukkit.entity.Hanging hanging = e.getEntity(); + AreaConfig ac = findFrameArea(hanging); + if (ac == null) return; + // ★ HangingBreakEvent 本身无 getRemover(),玩家/实体破坏走子类 HangingBreakByEntityEvent + Entity remover = null; + if (e instanceof org.bukkit.event.hanging.HangingBreakByEntityEvent) { + remover = ((org.bukkit.event.hanging.HangingBreakByEntityEvent) e).getRemover(); + } + // ★ 防爆模式:非玩家破坏(爆炸/闪电/环境/非玩家投射物)→ denyItemFrame 区域一律取消 + if (!(remover instanceof Player)) { + if (ac.denyItemFrame) { + e.setCancelled(true); + } + return; // 玩家主动破坏;箭矢命中由 onProjectileHit 处理 + } + Player p = (Player) remover; + // ★ 25批订正倒反天罡:玩家破坏走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, + // 普通访客按领地默认拦截;非玩家破坏走上方裸字段硬保护(24批防爆)。 + if (getEffectiveDeny(p, ac, "denyItemFrame")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框 + if (getEffectiveDeny(p, ac, "denyBlockBreak")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (getEffectiveDeny(p, ac, "denyEntityInteract")) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (!getEffectiveDeny(p, ac, "denyItemFrame")) return; + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + } + + private AreaConfig findFrameArea(Entity frame) { + Location loc = frame.getLocation(); + int bx = loc.getBlockX(); + int by = loc.getBlockY(); + int bz = loc.getBlockZ(); + String world = loc.getWorld().getName(); + + // ★ 按位置返回区域(不再要求 denyItemFrame 为真,以支持 L1/L2 先行判定) + AreaConfig ac = getArea(world, bx, by, bz); + if (ac != null) return ac; + + int[][] offsets = { + {0, 0, 1}, {0, 0, -1}, + {1, 0, 0}, {-1, 0, 0}, + {0, 1, 0}, {0, -1, 0} + }; + for (int[] off : offsets) { + ac = getArea(world, + bx + off[0], by + off[1], bz + off[2]); + if (ac != null) return ac; + } + return null; + } + + // ===== 投射物击中展示框(弓箭/弩/发射器箭/骷髅箭等)===== + @EventHandler + public void onProjectileHit( + org.bukkit.event.entity.ProjectileHitEvent e) { + Entity hit = e.getHitEntity(); + if (hit == null) return; + String typeName = hit.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + // ★ 使用统一的权限检查方法(支持per-player权限) + AreaConfig ac = findFrameArea(hit); + if (ac == null) return; + + // ★ 防爆模式:非玩家 shooter(发射器箭/骷髅箭等)→ denyItemFrame 区域一律取消 + if (!(e.getEntity().getShooter() instanceof Player)) { + if (ac.denyItemFrame) { + e.setCancelled(true); + } + return; + } + Player shooter = (Player) e.getEntity().getShooter(); + + // ★ 25批订正倒反天罡:玩家射击走 getEffectiveDeny 豁免链——领地主/授权成员/服务器管理员豁免, + // 普通访客按领地默认拦截;非玩家 shooter(发射器箭/骷髅箭等)走上方裸字段硬保护(24批防爆)。 + if (getEffectiveDeny(shooter, ac, "denyItemFrame")) { + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + // 层级:L1 破坏总开关 → L2 实体交互 → L3 展示框/容器 + if (getEffectiveDeny(shooter, ac, "denyBlockBreak")) { + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + if (getEffectiveDeny(shooter, ac, "denyEntityInteract")) { + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止破坏实体"); + return; + } + // ★ 三级:展示框同时受denyItemFrame和denyContainer控制 + if (!getEffectiveDeny(shooter, ac, "denyItemFrame") && !getEffectiveDeny(shooter, ac, "denyContainer")) return; + + e.setCancelled(true); + shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + } + + + // ===== 区域外火焰不能蔓延进区域 ===== + @EventHandler + public void onBlockSpread(BlockSpreadEvent e) { + Block block = e.getBlock(); + AreaConfig tgtArea = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (tgtArea != null && tgtArea.denyFireSpread) { + e.setCancelled(true); + } + } + + + // ===== 火焰弹/烈焰球拦截 ===== + @EventHandler + public void onProjectileHitFireball(ProjectileHitEvent e) { + if (!(e.getEntity() instanceof org.bukkit.entity.Fireball)) return; + org.bukkit.entity.Fireball fb = (org.bukkit.entity.Fireball) e.getEntity(); + AreaConfig ac = getArea(fb.getWorld().getName(), + fb.getLocation().getBlockX(), fb.getLocation().getBlockY(), fb.getLocation().getBlockZ()); + if (ac != null && ac.denyFire) { + fb.remove(); + // 如果射手是玩家,发消息 + if (fb.getShooter() instanceof Player) { + ((Player) fb.getShooter()).sendMessage("§c§l[区域防护] §f此区域禁止投掷火焰"); + } + } + } + + // ===== TNT自爆防漏网:EntityExplodeEvent ===== + @EventHandler + public void onEntityExplode(EntityExplodeEvent e) { + org.bukkit.entity.Entity ent = e.getEntity(); + if (ent != null) { + // 如果是TNT引爆,检查引爆位置 + AreaConfig ac = getArea(ent.getWorld().getName(), + (int) ent.getLocation().getBlockX(), + (int) ent.getLocation().getBlockY(), + (int) ent.getLocation().getBlockZ()); + // 如果爆炸源在禁止爆炸的区域,取消所有爆炸 + if (ac != null && ac.denyExplosion) { + e.blockList().clear(); + return; + } + } + // 检查每个被爆炸的方块是否在禁止爆炸的区域 + Iterator it = e.blockList().iterator(); + while (it.hasNext()) { + Block b = it.next(); + AreaConfig a = getArea(b.getWorld().getName(), b.getX(), b.getY(), b.getZ()); + if (a != null && a.denyExplosion) { + it.remove(); + } + } + } + + // ===== 区域内方块着火(TNT、岩浆等)也阻止 ===== + @EventHandler + public void onBlockBurn(BlockBurnEvent e) { + Block block = e.getBlock(); + AreaConfig ac = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + } + } + + // ===== PlayerInteractEvent: 打火石/火焰弹右键使用拦截(带豁免) ===== + // 注:火床放置拦截在onBlockPlace事件中处理 + @EventHandler + public void onPlayerInteractFire(PlayerInteractEvent e) { + Player p = e.getPlayer(); + Action action = e.getAction(); + if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) return; + + Material mat = e.getItem() == null ? null : e.getItem().getType(); + if (mat != Material.FLINT_AND_STEEL && mat != Material.FIRE_CHARGE + && mat != Material.CAMPFIRE && mat != Material.SOUL_CAMPFIRE + && mat != Material.LAVA_BUCKET) { + return; + } + + AreaConfig srcArea = getArea(p.getWorld().getName(), + p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); + Block clickedBlock = e.getClickedBlock(); + AreaConfig tgtArea = null; + if (clickedBlock != null) { + tgtArea = getArea(clickedBlock.getWorld().getName(), + clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); + } + + // 使用统一的豁免逻辑(OWNER/ADMIN自动通过,visitor按领地默认权限) + boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); + boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); + if (srcDenied || tgtDenied) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 火床放置拦截 ===== + @EventHandler + public void onPlaceCampfire(BlockPlaceEvent e) { + Material placed = e.getItemInHand().getType(); + if (placed != Material.CAMPFIRE && placed != Material.SOUL_CAMPFIRE) return; + + Player p = e.getPlayer(); + AreaConfig ac = getArea(p.getWorld().getName(), + p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); + + // OWNER/ADMIN 豁免 + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + // visitor: 用领地默认权限 + if (ac != null && ac.denyFire) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止放置火床"); + } + } + + // ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 ===== + @EventHandler + public void onIgnite(BlockIgniteEvent e) { + Player p = e.getPlayer(); + Block tgtBlock = e.getBlock(); + + // 非玩家触发(如火蔓延、TNT引爆)-> 不拦截,由BlockBurnEvent处理 + if (p == null) return; + + AreaConfig srcArea = getArea(p.getWorld().getName(), + p.getLocation().getBlockX(), p.getLocation().getBlockY(), p.getLocation().getBlockZ()); + AreaConfig tgtArea = getArea(tgtBlock.getWorld().getName(), + tgtBlock.getX(), tgtBlock.getY(), tgtBlock.getZ()); + + // 使用统一豁免 + boolean srcDenied = getEffectiveDeny(p, srcArea, "denyFire"); + boolean tgtDenied = getEffectiveDeny(p, tgtArea, "denyFire"); + if (srcDenied || tgtDenied) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== ★ 拴绳使用(给生物拴绳或拉拽)===== + @EventHandler + public void onLeash(PlayerInteractEntityEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = e.getPlayer(); + Entity entity = e.getRightClicked(); + // 检查玩家所在位置或实体位置 + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + ac = getArea( + entity.getWorld().getName(), + entity.getLocation().getBlockX(), + entity.getLocation().getBlockY(), + entity.getLocation().getBlockZ()); + } + if (ac == null || !ac.denyLead) return; + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + // 只拦截拴绳操作(主手持拴绳) + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand != null && hand.getType() == Material.LEAD) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止在此区域使用拴绳"); + } + } + + // ===== ★ 投喂动物 ===== + @EventHandler + public void onPlayerFeedAnimal(EntityBreedEvent e) { + if (!(e.getBreeder() instanceof Player)) return; + Player p = (Player) e.getBreeder(); + Entity entity = e.getEntity(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) { + ac = getArea( + entity.getWorld().getName(), + entity.getLocation().getBlockX(), + entity.getLocation().getBlockY(), + entity.getLocation().getBlockZ()); + } + if (ac == null || !ac.denyAnimalFeeding) return; + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止在此区域繁殖动物"); + } + + // ===== ★ 袭击侦听(玩家触发袭击时检测)===== + @EventHandler + public void onRaidTrigger(org.bukkit.event.raid.RaidTriggerEvent e) { + Player p = e.getPlayer(); + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null || !ac.denyRaid) return; + if (hasPermission(p, ac, PermissionLevel.OWNER)) return; + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止触发袭击事件"); + } + + // ===== ★ 玩家发光检测(通过定期扫描清除区域内发光效果)===== + // 在onEnable中注册repeating task,此处提供public方法供Main调用 + public void checkGlowingPlayers() { + for (Player p : Bukkit.getOnlinePlayers()) { + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null && getEffectiveDeny(p, ac, "denyGlowing")) { + if (p.hasPotionEffect(PotionEffectType.GLOWING)) { + p.removePotionEffect(PotionEffectType.GLOWING); + p.sendMessage("§c§l[区域防护] §f此区域禁止使用发光效果"); + } + } + } + } + + + private void savePeaceWhitelist(AreaConfig ac) { + File file = new File(rootDir, ac.name + ".txt"); + if (!file.exists()) return; + + try { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader( + new FileReader(file, StandardCharsets.UTF_8)); + String line; + boolean found = false; + + while ((line = reader.readLine()) != null) { + String trimmed = line.trim(); + if (trimmed.contains("和平") + && trimmed.contains("白名单")) { + StringBuilder sb = new StringBuilder(); + sb.append("和平模式白名单:"); + boolean first = true; + for (String name : ac.peaceWhitelist) { + if (!first) sb.append(","); + sb.append(name); + first = false; + } + lines.add(sb.toString()); + found = true; + } else { + lines.add(line); + } + } + reader.close(); + + if (!found && !ac.peaceWhitelist.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("和平模式白名单:"); + boolean first = true; + for (String name : ac.peaceWhitelist) { + if (!first) sb.append(","); + sb.append(name); + first = false; + } + lines.add(sb.toString()); + } + + FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8); + for (String l : lines) { + writer.write(l + "\n"); + } + writer.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[防护] 保存和平白名单失败: " + e.getMessage()); + } + } + + + @EventHandler + public void onPaintingPlace( + org.bukkit.event.player.PlayerBucketEmptyEvent e) { + // 暂不处理 + } + + + // ==================== 区域配置文件 ==================== + + public static class LandConfig { + public int 创建价格 = 1000; + public int 默认高度范围最小 = 0; + public int 默认高度范围最大 = 255; + public int 默认和平模式持续秒数 = 5; + public int 权限商店最低价格 = 1000; + public int 权限商店默认时长秒 = 86400; + public int 权限商店最大时长秒 = 86400; + public String 进入提示 = "进入领地: {area}"; + public String 离开提示 = "离开领地: {area}"; + } + + private final LandConfig landConfig = new LandConfig(); + + public LandConfig getLandConfig() { return landConfig; } + + private void loadAreaConfig() { + // ★ 配置文件放在area/子目录,避免插件根目录凌乱 + File areaDir = new File(plugin.getDataFolder(), "area"); + if (!areaDir.exists()) { + areaDir.mkdirs(); + } + // ★ 自动迁移:如果根目录有旧配置文件,移到area/目录 + File oldConfigFile = new File(plugin.getDataFolder(), "区域防护配置.txt"); + File configFile = new File(areaDir, "区域防护配置.txt"); + if (!configFile.exists() && oldConfigFile.exists()) { + try { + oldConfigFile.renameTo(configFile); + plugin.getLogger().info("[防护] 已将区域防护配置迁移到 area/ 目录"); + } catch (Exception e) { + // 迁移失败,使用新位置 + } + } + if (!configFile.exists()) { + writeDefaultAreaConfig(configFile); + } + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq).trim(); + String value = line.substring(eq + 1).trim(); + switch (key) { + case "创建价格": landConfig.创建价格 = Integer.parseInt(value); break; + case "默认高度范围最小": landConfig.默认高度范围最小 = Integer.parseInt(value); break; + case "默认高度范围最大": landConfig.默认高度范围最大 = Integer.parseInt(value); break; + case "默认和平模式持续秒数": landConfig.默认和平模式持续秒数 = Integer.parseInt(value); break; + case "权限商店最低价格": landConfig.权限商店最低价格 = Integer.parseInt(value); break; + case "权限商店默认时长秒": landConfig.权限商店默认时长秒 = Integer.parseInt(value); break; + case "权限商店最大时长秒": landConfig.权限商店最大时长秒 = Integer.parseInt(value); break; + case "进入提示": landConfig.进入提示 = value; break; + case "离开提示": landConfig.离开提示 = value; break; + } + } + } catch (Exception e) { + plugin.getLogger().warning("[防护] 加载区域配置文件失败: " + e.getMessage()); + } + } + + private void writeDefaultAreaConfig(File configFile) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(configFile), StandardCharsets.UTF_8))) { + pw.println("# 区域防护配置文件"); + pw.println("创建价格=1000"); + pw.println("默认高度范围最小=0"); + pw.println("默认高度范围最大=255"); + pw.println("默认和平模式持续秒数=5"); + pw.println("权限商店最低价格=1000"); + pw.println("权限商店默认时长秒=86400"); + pw.println("权限商店最大时长秒=86400"); + pw.println("进入提示=进入领地: {area}"); + pw.println("离开提示=离开领地: {area}"); + } catch (Exception e) { + plugin.getLogger().warning("[防护] 写入默认区域配置失败: " + e.getMessage()); + } + } + + // ==================== 数据类 ==================== + + public static class AreaConfig { + public String name = ""; + public String owner = ""; // 领地所有者 + public String world = ""; + public int x1, z1, x2, z2; + public int yMin = 0, yMax = 255; + public List confiscateItems = new ArrayList<>(); + // ★ 违禁品没收计数:领地累计没收次数(2026-10-11 违禁品管理面板) + public int confiscateCount = 0; + // ★ 退出领地是否归还没收物(默认 false=不退,由领地主控制) + public boolean returnOnLeave = false; + public List denyUseItems = new ArrayList<>(); + public List clearEffects = new ArrayList<>(); // 清除单个效果名 + public boolean clearAllBadEffects = false; // 清除所有负面+中性 + public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + /** + * ★ 负面效果独立列表(2026-10-04 用户要求从增益里拆出来) + * + *

结构同 {@link #giveEffects}:[效果名, 等级, 秒数]。原实现把所有效果 + * (含缓慢/中毒/凋零等负面)全塞在 {@code giveEffects} 里,GUI 上也只能一把选, + * 负面与正面混排。拆开后 GUI 分两个子菜单、CLI 分两套指令、上限口径也各归各。

+ */ + public List badEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + public List punishCommands = new ArrayList<>(); + public boolean denyBlockPlace = false; + public boolean denyBlockBreak = false; + public boolean denySignEdit = false; // ★ 独立告示牌编辑权限 + public boolean denyContainer = false; // ★ 独立容器管理权限 + public boolean denyPVP = false; + public boolean denyFallDamage = false; + public boolean denyHunger = false; + public boolean denyAllDamage = false; + public boolean denyMount = false; + public boolean allowDrop = false; + public boolean denyEnderPearl = false; + public boolean denyBow = false; + public boolean denyPotion = false; + public boolean denyExplosion = false; + public boolean denyMove = false; + public boolean allowPickup = false; + public boolean denyFire = false; + // ★ 新增投掷物权限(三叉戟、雪球、风蛋、箭) + public boolean denyThrownProjectiles = false; + // ★ 玩家发光控制 + public boolean denyGlowing = false; + // ★ 红石电路交互(按钮、压力板、中继器、比较器) + public boolean denyRedstoneInteraction = false; + // ★ 门禁交互(门/栅栏门) + public boolean denyDoorInteraction = false; + // ★ 音频交互(音符盒、唱片机) + public boolean denyNoteblockJukebox = false; + // ★ 拴绳使用 + public boolean denyLead = false; + // ★ 农作物状态检测(播种&收获) + public boolean denyCropHarvest = false; + // ★ 耕地破坏(玩家跳跃踩踏耕地 → 变为泥土) + public boolean denyFarmlandTrample = false; + // ★ 末影人传送控制 + public boolean denyEnderTeleport = false; + // ★ 采集羊毛和乐魂挽具 + public boolean denyWoolShear = false; + // ★ 投喂动物 + public boolean denyAnimalFeeding = false; + // ★ 生物蛋(右键刷怪蛋生成实体)——上级权限:放置方块(放置未开启时单独开启本项不生效) + public boolean denySpawnEgg = false; + // ★ 涂蜡/刮蜡(蜜脾涂蜡、斧头刮蜡)——独立一级权限(不受上级开关约束):开启后普通访客禁涂/禁刮,领地主/插件管理员/已授权玩家豁免 + public boolean denyWax = false; + // ★ 玩家攻击生物 + public boolean denyMobAttack = false; + // ★ 村民交易(右键村民开交易窗 / 交易窗内完成交易)——独立一级权限: + // 开启后普通访客禁止与领地内村民交易,领地主/插件管理员/已授权玩家豁免 + public boolean denyVillagerTrade = false; + public boolean enableAnnounce = false; + public String announceTemplate = ""; + public String txtContent = ""; + public String confiscateMsg = ""; + public String enterMsg = ""; + public String leaveMsg = ""; + public boolean denyRaid = false; + public boolean denyFireSpread = false; + public boolean denyAllEffects = false;; + public boolean peaceMode = false; + public int peaceModeDuration = 5000; // 和平模式生物保护期(毫秒),默认5秒,最大3600秒 + public boolean denyItemFrame = false; + // ★ 二级权限:实体交互(受denyBlockPlace/denyBlockBreak一级权限影响) + // 其下三级:denyItemFrame 等具体实体权限 + public boolean denyEntityInteract = false; + public Set peaceWhitelist = new HashSet<>(); + public String enforceGameMode = null; // 强制游戏模式 + public Set modeExempt = new HashSet<>(); // 模式排除名单 + // ★ 传送点坐标(玩家自定义,防止卡墙) + public double warpX = 0, warpY = 0, warpZ = 0; + public float warpYaw = 0, warpPitch = 0; + public String warpWorld = ""; + // ★ 公共建筑标记 + public boolean isPublicBuilding = false; + // ★ 访客传送权限(默认关闭:未授权访客不能传送进入领地) + public boolean allowVisitorTeleport = false; + // ★ 创建领地时实际扣款金额(删除领地按比例退费的基数;0=无扣款) + public int createCost = 0; + // ★ 第57批续21 子领地:父领地(主领地)的 area_lands.id;0=主领地(无父) + // 子领地必须完整包含在某主领地内(1级,不可再嵌套),owner 沿用主领地主, + // 权限行挂在父领地 land_id 下(area_land_permissions),可单独收紧/放行。 + public int parentId = 0; + + + + + private static final Map EFFECT_MAP + = new HashMap<>(); + static { + // 负面效果 + EFFECT_MAP.put("缓慢", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); + EFFECT_MAP.put("瞬间伤害", PotionEffectType.INSTANT_DAMAGE); + EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); + EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); + EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("中毒", PotionEffectType.POISON); + EFFECT_MAP.put("凋零", PotionEffectType.WITHER); + EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); + EFFECT_MAP.put("霉运", PotionEffectType.UNLUCK); + EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); + EFFECT_MAP.put("蓄风", PotionEffectType.WIND_CHARGED); + EFFECT_MAP.put("盘丝", PotionEffectType.WEAVING); + EFFECT_MAP.put("渗浆", PotionEffectType.OOZING); + EFFECT_MAP.put("寄生", PotionEffectType.INFESTED); + // 中性效果 + EFFECT_MAP.put("不祥之兆", PotionEffectType.BAD_OMEN); + EFFECT_MAP.put("不祥征兆", PotionEffectType.BAD_OMEN); + EFFECT_MAP.put("袭击之兆", PotionEffectType.RAID_OMEN); + EFFECT_MAP.put("袭击征兆", PotionEffectType.RAID_OMEN); + EFFECT_MAP.put("试炼之兆", PotionEffectType.TRIAL_OMEN); + EFFECT_MAP.put("试炼征兆", PotionEffectType.TRIAL_OMEN); + // 正面效果 + EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); + EFFECT_MAP.put("急迫", PotionEffectType.HASTE); + EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); + EFFECT_MAP.put("瞬间治疗", PotionEffectType.INSTANT_HEALTH); + EFFECT_MAP.put("跳跃提升", PotionEffectType.JUMP_BOOST); + EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("抗性提升", PotionEffectType.RESISTANCE); + EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); + EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); + EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); + EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); + EFFECT_MAP.put("发光", PotionEffectType.GLOWING); + EFFECT_MAP.put("生命提升", PotionEffectType.HEALTH_BOOST); + EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); + EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); + EFFECT_MAP.put("幸运", PotionEffectType.LUCK); + EFFECT_MAP.put("村庄英雄", PotionEffectType.HERO_OF_THE_VILLAGE); + EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); + EFFECT_MAP.put("潮涌能量", PotionEffectType.CONDUIT_POWER); + EFFECT_MAP.put("海豚的恩惠", PotionEffectType.DOLPHINS_GRACE); + + + // 带效果二字 + // 补充带"效果"后缀的别名 + EFFECT_MAP.put("黑暗效果", PotionEffectType.DARKNESS); + EFFECT_MAP.put("不祥之兆效果", PotionEffectType.BAD_OMEN); + EFFECT_MAP.put("袭击之兆效果", PotionEffectType.RAID_OMEN); + EFFECT_MAP.put("试炼之兆效果", PotionEffectType.TRIAL_OMEN); + EFFECT_MAP.put("缓慢效果", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("中毒效果", PotionEffectType.POISON); + EFFECT_MAP.put("凋零效果", PotionEffectType.WITHER); + EFFECT_MAP.put("失明效果", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("饥饿效果", PotionEffectType.HUNGER); + EFFECT_MAP.put("虚弱效果", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("反胃效果", PotionEffectType.NAUSEA); + EFFECT_MAP.put("飘浮效果", PotionEffectType.LEVITATION); + EFFECT_MAP.put("发光效果", PotionEffectType.GLOWING); + EFFECT_MAP.put("霉运效果", PotionEffectType.UNLUCK); + EFFECT_MAP.put("蓄风效果", PotionEffectType.WIND_CHARGED); + EFFECT_MAP.put("盘丝效果", PotionEffectType.WEAVING); + EFFECT_MAP.put("渗浆效果", PotionEffectType.OOZING); + EFFECT_MAP.put("寄生效果", PotionEffectType.INFESTED); + EFFECT_MAP.put("挖掘疲劳效果", PotionEffectType.MINING_FATIGUE); + EFFECT_MAP.put("瞬间伤害效果", PotionEffectType.INSTANT_DAMAGE); + EFFECT_MAP.put("村庄英雄效果", PotionEffectType.HERO_OF_THE_VILLAGE); + } + + + public int ruleCount() { + int c = 0; + if (!confiscateItems.isEmpty()) c++; + if (denyBlockPlace) c++; + if (denyBlockBreak) c++; + if (denyPVP) c++; + if (denyFallDamage) c++; + if (denyHunger) c++; + if (denyAllDamage) c++; + if (allowDrop) c++; + if (denyMount) c++; + if (denyEnderPearl) c++; + if (denyBow) c++; + if (denyRaid) c++; + if (denyPotion) c++; + if (denyExplosion) c++; + if (denyFireSpread) c++; + if (denyAllEffects) c++; + if (!clearEffects.isEmpty()) c++; + if (clearAllBadEffects) c++; + if (!giveEffects.isEmpty()) c++; + if (enableAnnounce) c++; + if (!punishCommands.isEmpty()) c++; + if (!denyUseItems.isEmpty()) c++; + if (peaceMode) c++; + if (denyItemFrame) c++; + if (denyEntityInteract) c++; + if (denyThrownProjectiles) c++; + if (denyGlowing) c++; + if (denyRedstoneInteraction) c++; + if (denyDoorInteraction) c++; + if (denyNoteblockJukebox) c++; + if (denyLead) c++; + if (denyCropHarvest) c++; + if (denyWoolShear) c++; + if (denyAnimalFeeding) c++; + if (denySpawnEgg) c++; + if (denyWax) c++; + if (denyVillagerTrade) c++; + if (!peaceWhitelist.isEmpty()) c++; + + return c; + } + } + + // ==================== 快速创建领地(无选点,以自身为中心) ==================== + + /** + * ★ 快速创建领地:以玩家为中心3×3,自动命名+冲突数字0~999 + * @return true成功创建,false失败 + */ + 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(); + int maxLands = (ugm != null) ? ugm.getPlayerMaxLands(p.getName(), globalMaxLandsPerPlayer) : globalMaxLandsPerPlayer; + long landCount = areas.values().stream().filter(a -> samePlayer(p.getName(), a.owner)).count(); + // DB兜底 + if (landCount < maxLands) { + try { + java.sql.PreparedStatement cs = dbConnection.prepareStatement("SELECT COUNT(*) as cnt FROM area_lands WHERE owner = ?"); + cs.setString(1, p.getName()); + java.sql.ResultSet cr = cs.executeQuery(); + if (cr.next()) { long dbc = cr.getLong("cnt"); if (dbc > landCount) landCount = dbc; } + cr.close(); cs.close(); + } catch (Exception ignored) {} + } + if (landCount >= maxLands) { + p.sendMessage("§c§l[防护] §f已达领地上限!每人最多 §e" + maxLands + " §f个领地"); + return false; + } + + // ★ 以玩家为中心3×3 + int cx = p.getLocation().getBlockX(); + int cz = p.getLocation().getBlockZ(); + int x1 = cx - 1, z1 = cz - 1; + int x2 = cx + 1, z2 = cz + 1; + int area = 9; // 3×3 + + // ★ 第57批续21 先查 3×3 是否完全包含于某主领地 → 领地主身份合格则自动转 confirm_create 建子领地 + AreaConfig qMain = findContainingMainLand(p.getWorld().getName(), x1, z1, x2, z2); + if (qMain != null) { + if (isQualifiedLandManager(p, qMain)) { + p.sendMessage("§a§l[防护] §f检测到 3×3 区域在主领地 §e" + qMain.name + + " §f内,已为你转为创建子领地"); + p.performCommand("protect confirm_create " + p.getName()); + return true; + } else { + p.sendMessage("§c§l[防护] §f该区域已在领地 §e" + qMain.name + + " §f内,你需具备该领地的领主/管理员身份才能创建子领地"); + return false; + } + } + // ★ 部分重叠(跨边界)仍拒绝 + for (AreaConfig other : areas.values()) { + if (other.parentId != 0) continue; + if (!other.world.equalsIgnoreCase(p.getWorld().getName())) continue; + int omx = Math.min(other.x1, other.x2), omx2 = Math.max(other.x1, other.x2); + int omz = Math.min(other.z1, other.z2), omz2 = Math.max(other.z1, other.z2); + if (x1 <= omx2 && x2 >= omx && z1 <= omz2 && z2 >= omz) { + p.sendMessage("§c§l[防护] §f该区域与领地 §e" + other.name + " §f重叠,请走远点再试"); + return false; + } + } + + // ★ 计算费用 + UserGroupManager ug = plugin.getUserGroup(); + int pricePerSqm = (ug != null) ? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm; + int totalCost = area * pricePerSqm; + + // ★ 检查余额 + BondManager bm = plugin.getBonds(); + if (bm != null && totalCost > 0) { + int bal = bm.getBonds(p.getName()); + if (bal < totalCost) { + p.sendMessage("§c§l[防护] §f余额不足!需要 §e" + totalCost + " §f债券,当前 §a" + bal); + return false; + } + } + + // ★ 自动命名:玩家名,冲突加数字0~999(重名判定不区分大小写) + String autoName = p.getName(); + if (areaNameExists(autoName)) { + boolean found = false; + for (int i = 0; i <= 999; i++) { + String candidate = p.getName() + i; + if (!areaNameExists(candidate)) { autoName = candidate; found = true; break; } + } + if (!found) { + p.sendMessage("§c§l[防护] §f无法找到可用的领地名(已用尽0~999后缀)"); + return false; + } + } + + // ★ 扣费 + int chargedCost = 0; + if (bm != null && totalCost > 0) { + String src = (pricePerSqm != globalCreatePricePerSqm) ? "(用户组优惠价)" : ""; + if (!bm.deductBonds(p.getName(), totalCost, "land_create", p.getName(), p.getName(), + "快速创建领地: " + autoName + " (" + area + "㎡×" + pricePerSqm + ")")) { + p.sendMessage("§c§l[防护] §f扣费失败,请稍后重试"); + return false; + } + p.sendMessage("§a§l[防护] §f创建领地扣除 §e" + totalCost + " §f债券(" + area + "㎡×" + pricePerSqm + "/㎡)" + src); + chargedCost = totalCost; + } + + // ★ 创建领地 + AreaConfig ac = new AreaConfig(); + ac.name = autoName; + ac.owner = p.getName(); + ac.world = p.getWorld().getName(); + ac.x1 = x1; ac.z1 = z1; ac.x2 = x2; ac.z2 = z2; + ac.yMin = 0; ac.yMax = 255; + ac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数) + saveAreaToDb(ac); + areas.put(autoName, ac); + loadAllAreas(); + + p.sendMessage("§a§l[防护] §f区域 §e" + autoName + " §f已创建(" + area + "㎡,以你为中心3×3)"); + return true; + } } \ No newline at end of file