diff --git a/backup/20260605_152529/.gitattributes b/backup/20260605_152529/.gitattributes new file mode 100644 index 0000000..d49c874 --- /dev/null +++ b/backup/20260605_152529/.gitattributes @@ -0,0 +1,5 @@ +*.md text=auto encoding=UTF-8 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary \ No newline at end of file diff --git a/backup/20260605_152529/.gitignore b/backup/20260605_152529/.gitignore new file mode 100644 index 0000000..fe405f6 --- /dev/null +++ b/backup/20260605_152529/.gitignore @@ -0,0 +1,46 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +# 开发辅助文件(本地保留,不推送到仓库) +txt/ +*.xlsx +*.patch +新建*.txt +群戏归类.txt \ No newline at end of file diff --git a/backup/20260605_152529/AreaProtection.java.backup b/backup/20260605_152529/AreaProtection.java.backup new file mode 100644 index 0000000..bc53d53 --- /dev/null +++ b/backup/20260605_152529/AreaProtection.java.backup @@ -0,0 +1,4907 @@ +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.entity.Entity; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import java.sql.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +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 java.util.Set; +import java.util.HashSet; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.entity.Item; + + + + +public class AreaProtection implements Listener { + + 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 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<>(); + + // 选地 + private static final Material WAND = Material.BREEZE_ROD; + + private final Map pos1 = + new ConcurrentHashMap<>(); + private final Map pos2 = + new ConcurrentHashMap<>(); + private final Set selecting = + ConcurrentHashMap.newKeySet(); + 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<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + + + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + 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 (p.isOp()) { + plugin.getLogger().info( + "[防护-调试] 豁免原因: OP, 玩家=" + name); + return true; + }*/ + if (globalPlayerWhitelist.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 全局白名单, 玩家=" + + name);*/ + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (aw != null && aw.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 区域白名单, 玩家=" + + name + " 区域=" + areaName);*/ + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 模式排除名单, 玩家=" + + name);*/ + return true; + } + /* plugin.getLogger().info( + "[防护-调试] 无豁免, 玩家=" + name);*/ + 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(); + loadAllAreas(); + loadWhitelists(); + recoverPendingEffects(); + } + 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); + } + + +// ==================== 数据库迁移 ==================== + + /** + * 如果旧位置有 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("========================================"); + } + +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // & → § + 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", "\n"); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + int i = 0; + + while (i < msg.length()) { + 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(); + if (!stack.isEmpty()) { + // 重置后重新应用外层格式 + sb.append("§r"); + for (char c : stack) { + sb.append("§").append(c); + } + } else { + sb.append("§r"); + } + } + 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": return 'l'; + case "i": return 'o'; + case "u": return 'n'; + case "s": return 'm'; + default: return 0; + } + } + +// =============== 加载 ==================== + + public void loadAllAreas() { + areas.clear(); + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files == null) return; + for (File f : files) { + try { + String name = f.getName() + .replace(".txt", ""); + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载失败: " + f.getName()); + } + } + } + + + 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)"); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 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 == '\u200B' // 零宽空格 + || c == '\u200C' // 零宽非连接 + || c == '\u200D' // 零宽连接 + || c == '\uFEFF' // BOM + || c == '\u00A0' // 不间断空格 + || c == '\u2028' // 行分隔符 + || c == '\u2029' // 段分隔符 + || c == '\u200E' // LTR标记 + || c == '\u200F' // RTL标记 + || c == '\u2060' // 字连接符 + || (c >= '\u2000' && c <= '\u200A')) { // 各种空格 + continue; + } + + // 全角→半角映射 + if (c >= '\uFF01' && c <= '\uFF5E') { + c = (char) (c - 0xFEE0); + } + + // 全角空格→半角空格 + if (c == '\u3000') { + 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.denyDrop = 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("和平模式") + && !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() + 5000); + + /* 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); + /* plugin.getLogger().info("[和平] " + p.getName() + + " 附近实体总数=" + nearby.size());*/ + for (Entity ent : nearby) { + String typeName = ent.getType().name(); + /* plugin.getLogger().info("[和平] 实体: " + + typeName + " isHostile=" + isHostile(typeName));*/ + } + int scanned = 0; + int waiting = 0; + int saved = 0; + int banished = 0; + + for (Entity ent : p.getNearbyEntities(48, 48, 48)) { + if (!isHostile(ent.getType().name())) continue; + scanned++; + + UUID entUid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(entUid); + String customName = ent.getCustomName(); + + // 有保护期记录 + if (protectUntil != null) { + long remain = protectUntil - now; + + if (remain > 0) { + // 保护期内,精确剩余秒数 + int secondsLeft = (int) Math.ceil(remain / 1000.0); + waiting++; + + // 保护期内也检查名字(玩家可能刚贴上) + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + protectedEntities.remove(entUid); + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 白名单匹配,永久保留");*/ + continue; + } + + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 保护期剩余约" + secondsLeft + "秒");*/ + continue; + } + + // 保护期到了,最终检查 + protectedEntities.remove(entUid); + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + " 5秒到期");*/ + + // 到期时再查一次白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 到期但白名单匹配,保留");*/ + continue; + } + + // 没名字或不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名=[" + customName + "] 传送虚空");*/ + continue; + } + + // 没有保护期记录(非刷怪蛋生成的或记录被清了) + // 也检查一下白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 无保护期但白名单匹配,保留");*/ + continue; + } + + // 没有保护期且不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 无保护期,命名=[" + customName + "] 传送虚空");*/ + } + + /* plugin.getLogger().info("[和平] " + + " 扫描=" + scanned + + " 等待=" + waiting + + " 保留=" + saved + + " 传送=" + banished + + " 白名单=" + ac.peaceWhitelist);*/ + } + + 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" + )); + + 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; + } + + // ==================== 白名单检查 ==================== + + private boolean isPlayerWhitelisted(String player, + AreaConfig ac) { + if (globalPlayerWhitelist.contains( + player.toLowerCase())) + return true; + Set areaList = + areaPlayerWhitelist.get(ac.name); + return areaList != null + && areaList.contains(player.toLowerCase()); + } + + 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 taskId = Bukkit.getScheduler() + .runTaskTimer(plugin, new Runnable() { + @Override + public void run() { + if (!p.isOnline()) return; + Location loc = p.getLocation(); + int px = loc.getBlockX(); + int py = loc.getBlockY(); + int pz = loc.getBlockZ(); + 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); + for (int dy = 5; dy <= 30; dy += 5) { + for (int x = minX; x <= maxX; x += 4) { + for (int z = minZ; z <= maxZ; z += 4) { + if (x == minX || x == maxX + || z == minZ || z == maxZ) { + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + } + } + } + } + } + }, 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; + + Location from = event.getFrom(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 防抖 + 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) { + // ★ 每次移动都清除指定效果 + clearBadEffects(p, 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); + + // 离开提示 + if (oldAc != null && oldAc.leaveMsg != null + && !oldAc.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(oldAc.leaveMsg)); + } + + // 收标 + 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 (marked != null && !marked.isEmpty()) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★创建延时任务: " + + toClear.size() + "个");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-移动-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + plugin.getLogger().info( + "[防护-移动-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size()); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + /* plugin.getLogger().info( + "[防护-移动-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] + cleared" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + 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) { + if (newAc.enterMsg != null + && !newAc.enterMsg.isEmpty()) { + p.sendMessage( + formatAreaMsg(newAc.enterMsg)); + } + 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()); + } + } + } + +// ===== 清除所有效果 ===== + if (newAc.clearAllBadEffects) { + for (PotionEffect eff : p.getActivePotionEffects()) { + p.removePotionEffect(eff.getType()); + } + plugin.getLogger().info( + "[防护] ★清除所有效果, 玩家=" + p.getName()); + } + + 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); + } + } +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + 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 || ac.giveEffects.isEmpty()) return; + if (ac.giveEffects.isEmpty()) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + for (String[] eff : ac.giveEffects) { + 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); + } + + // 给效果 + 贴标 + List applied = new ArrayList<>(); + for (String[] eff : ac.giveEffects) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + int lv = Integer.parseInt(eff[1]) - 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) { + } + } + + // 贴标 + // 新代码(合并,不覆盖): + 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());*/ + } + } + + /** + * 离开区域时,延时15秒清理效果 + * 流程:收标 → 15秒后清效果 → 确认清空 → 清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); + + // 启动15秒延时 + 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); + } + } + + // 确认清空后清DB + removePlayerEffects(uid, areaName); + plugin.getLogger().info( + "[防护] 15秒已到, 清除 " + + (marked != null ? marked.size() : 0) + + " 个效果 + DB, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + }.runTaskLater(plugin, 300L); // 15秒 = 300 tick + + pendingClearTask.put(uid, task); + } + public Set getAllAreaNames() { + return areas.keySet(); + } + + /** + * 15秒内进了新区域: + * 清除旧区域有、新区域没有的效果 + */ + 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) { + for (String[] eff : newAc.giveEffects) { + 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 都跳过 + */ + private boolean isExemptFromConfiscation( + Player p, String areaName) { + String name = p.getName(); + + if (globalPlayerWhitelist.contains(name)) + return true; + + Set areaWl = + areaPlayerWhitelist.get(areaName); + if (areaWl != null && areaWl.contains(name)) + return true; + + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) + return true; + + return false; + } + + /** + * 没收玩家背包中的违禁品 + */ + private void confiscateItems(Player p, + AreaConfig ac) { + boolean confiscated = false; + + for (int i = 0; + i < p.getInventory().getSize(); i++) { + ItemStack item = p.getInventory().getItem(i); + if (item == null) continue; + + String typeName = item.getType().name(); + if (ac.confiscateItems + .contains(typeName)) { + p.getInventory().setItem(i, null); + confiscated = true; + } + } + + if (confiscated) { + String msg = ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty() + ? ac.confiscateMsg + : "§c你的违禁品已被没收"; + p.sendMessage(msg); + + // 通报批评 + if (ac.enableAnnounce + && ac.announceTemplate != null) { + String text = ac.announceTemplate + .replace("{player}", + p.getName()); + Bukkit.broadcastMessage(text); + } + } + } + + + + private String getChineseGameMode(GameMode mode) { + switch (mode) { + case SURVIVAL: return "生存模式"; + case CREATIVE: return "创造模式"; + case ADVENTURE: return "冒险模式"; + case SPECTATOR: return "旁观模式"; + default: return mode.name(); + } + } + + private void handleEnter(Player p, AreaConfig ac) { + UUID uid = p.getUniqueId(); + + // 清除负面效果 + clearBadEffects(p, ac); + + // 给予效果 + List given = new ArrayList<>(); + for (String[] parts : ac.giveEffects) { + 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) {} + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + given.add(type); + } + } + + removePlayerEffects(uid, ac.name); + if (!given.isEmpty()) { + // savePlayerEffects(uid, p.getName(), ac.name, given); + } + + // 白名单检查(最先执行) + if (isPlayerWhitelisted(p.getName(), ac)) 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; + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return true; + } + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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; + + // 去掉"效果"后缀 + String clean = name; + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + + // 硬编码中文名→英文ID映射(不依赖任何外部Map) + switch (clean) { + // 负面 + case "缓慢": + return resolveEffectType("slowness"); + case "挖掘疲劳": + return resolveEffectType("mining_fatigue"); + case "瞬间伤害": + return resolveEffectType("instant_damage"); + case "反胃": + return resolveEffectType("nausea"); + case "失明": + return resolveEffectType("blindness"); + case "饥饿": + return resolveEffectType("hunger"); + case "虚弱": + return resolveEffectType("weakness"); + case "中毒": + return resolveEffectType("poison"); + case "凋零": + return resolveEffectType("wither"); + case "飘浮": + return resolveEffectType("levitation"); + case "霉运": + return resolveEffectType("unluck"); + case "黑暗": + return resolveEffectType("darkness"); + case "蓄风": + return resolveEffectType("wind_charged"); + case "盘丝": + return resolveEffectType("weaving"); + case "渗浆": + return resolveEffectType("oozing"); + case "寄生": + return resolveEffectType("infested"); + // 中性 + case "不祥之兆": + case "不祥征兆": + return resolveEffectType("bad_omen"); + case "袭击之兆": + case "袭击征兆": + return resolveEffectType("raid_omen"); + case "试炼之兆": + case "试炼征兆": + return resolveEffectType("trial_omen"); + // 正面 + case "迅捷": + return resolveEffectType("speed"); + case "急迫": + return resolveEffectType("haste"); + case "力量": + return resolveEffectType("strength"); + case "瞬间治疗": + return resolveEffectType("instant_health"); + case "跳跃提升": + return resolveEffectType("jump_boost"); + case "生命恢复": + return resolveEffectType("regeneration"); + case "抗性提升": + return resolveEffectType("resistance"); + case "抗火": + return resolveEffectType("fire_resistance"); + case "水下呼吸": + return resolveEffectType("water_breathing"); + case "隐身": + return resolveEffectType("invisibility"); + case "夜视": + return resolveEffectType("night_vision"); + case "发光": + return resolveEffectType("glowing"); + case "生命提升": + return resolveEffectType("health_boost"); + case "伤害吸收": + return resolveEffectType("absorption"); + case "饱和": + return resolveEffectType("saturation"); + case "幸运": + return resolveEffectType("luck"); + case "村庄英雄": + return resolveEffectType("hero_of_the_village"); + case "缓降": + return resolveEffectType("slow_falling"); + case "潮涌能量": + return resolveEffectType("conduit_power"); + case "海豚的恩惠": + return resolveEffectType("dolphins_grace"); + case "致命中毒": + return resolveEffectType("poison"); + default: + break; + } + + +// 兜底:直接用原版英文ID查(★ 注意是 PotionEffectType.getByName,不是 resolveEffectType) + PotionEffectType fallback = PotionEffectType.getByName(clean); + if (fallback != null) return fallback; + fallback = PotionEffectType.getByName(clean.toLowerCase()); + if (fallback != null) return fallback; + + plugin.getLogger().warning("[防护] 无法识别效果: " + name); + 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); + } + + // 创建15秒延时清理 + if (marked != null && !marked.isEmpty()) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★创建延时任务: " + + toClear.size() + "个效果");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-传送-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); +/* + plugin.getLogger().info( + "[防护-传送-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size() + + " 区域=" + clearArea);*/ + + 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); + /* plugin.getLogger().info( + "[防护-传送-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] ★★★完成: 清除" + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + pendingClearTask.put(uid, task); + + } + + // 离开提示 + if (fromArea != null && fromArea.leaveMsg != null + && !fromArea.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(fromArea.leaveMsg)); + } + } + + // ===== 进入新区域 ===== + 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); + } + } + + // 进入提示 + if (toArea != null && toArea.enterMsg != null + && !toArea.enterMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(toArea.enterMsg)); + } + + // 发放效果 + if (toArea != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] 调用applyRegionEffects");*/ + applyRegionEffects(p, uid, toName, toArea); +// 清除效果 + clearBadEffects(p, toArea); + + if (toArea.confiscateItems != null + && !toArea.confiscateItems.isEmpty() + && !isExemptFromConfiscation( + p, toName)) { + confiscateItems(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 (isPlayerWhitelisted(p.getName(), ac)) 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(); + } + + 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) { + for (String[] parts : ac.giveEffects) { + 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) {} + + 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 && ac.denyBlockPlace + && !isPlayerWhitelisted(p.getName(), ac)) { + 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 && ac.denyBlockBreak + && !isPlayerWhitelisted(p.getName(), ac)) { + 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; + // 保护性规则:对所有人无条件生效 + if (ac.denyAllDamage) { e.setCancelled(true); return; } + if (ac.denyFallDamage + && e.getCause() == EntityDamageEvent.DamageCause.FALL) + e.setCancelled(true); + } + + + @EventHandler + public void onPVP(EntityDamageByEntityEvent e) { + if (!(e.getDamager() instanceof Player)) return; + 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 && ac.denyPVP + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + + @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 && 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 && ac.denyDrop + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); + } + } + + // 替换整个 onInteract 方法 + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand == null) return; + + // ===== 选区工具 ===== + if (hand.getType() == Material.BREEZE_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); + 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 { + pos2.put(uid, block.getLocation()); + p.sendMessage("§a§l[防护] §f位置2: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + } + } + return; + } + } + + // ===== 放置实体类物品时不检查(展示框、矿车、船、地图等)===== + Material mat = hand.getType(); + if (mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME + || 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.MAP + || mat == Material.ARMOR_STAND) { + return; + } + + // ===== 区域规则检查 ===== + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + + // 末影珍珠禁止 + if (ac.denyEnderPearl + && hand.getType() == Material.ENDER_PEARL) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); + return; + } + + // 弓箭禁止 + if (ac.denyBow + && (hand.getType() == Material.BOW + || hand.getType() == Material.CROSSBOW)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); + return; + } + + // 白名单跳过 + if (isPlayerWhitelisted(p.getName(), ac)) return; + + // 禁止使用物品 + String typeId = hand.getType().name(); + if (ac.denyUseItems.contains(typeId)) { + 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(); + } + } + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + + 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("#")) { + 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("#")) { + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + 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 && ac.denyPotion + && !isPlayerWhitelisted(shooter.getName(), ac)) { + e.setCancelled(true); + shooter.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; + } + + /** + * 判断字符串是否是区域名(精准或模糊) + */ + 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", "创建", "删除", "重载", + "工具", "expand", "contraction", + "on", "off", "tempon", "modeexempt" + )); + + 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) { + showHelp(sender); + return true; + } + String sub = args[0].toLowerCase(); +// ===== 诊断命令 ===== + 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; + ItemStack tool = new ItemStack(WAND); + ItemMeta meta = tool.getItemMeta(); + meta.setDisplayName("§a§l区域选择工具"); + meta.setLore(Arrays.asList( + "§7左键: 位置1", "§7右键: 位置2")); + tool.setItemMeta(meta); + p.getInventory().addItem(tool); + p.sendMessage("§a§l[防护] §f已获取选择工具"); + return true; + } + + // ===== 创建 ===== + if (sub.equals("创建") || sub.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 创建 <名>"); + return true; + } + Player p = (Player) sender; + UUID u = p.getUniqueId(); + if (!pos1.containsKey(u) + || !pos2.containsKey(u)) { + p.sendMessage("§c请先用工具选点"); + return true; + } + String areaName = args[1]; + File f = new File(rootDir, areaName + ".txt"); + if (f.exists()) { + p.sendMessage("§c区域已存在"); + return true; + } + Location l1 = pos1.get(u); + Location l2 = pos2.get(u); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 区域: " + areaName); + pw.println("起点: " + l1.getWorld().getName() + + "," + l1.getBlockX() + + "," + l1.getBlockZ()); + pw.println("终点: " + l2.getWorld().getName() + + "," + l2.getBlockX() + + "," + l2.getBlockZ()); + pw.println("高度范围: 0-255"); + pw.println(); + pw.println("# 禁止放置方块"); + pw.println("# 禁止破坏方块"); + pw.println("# 禁止PVP"); + pw.println("# 效果: 夜视 1 999"); + pw.println("# 进入提示: 欢迎来到保护区"); + pw.println("# 离开提示: 已离开保护区"); + pw.close(); + p.sendMessage("§a§l[防护] §f区域 " + + areaName + " 已创建"); + loadAllAreas(); + } catch (IOException ex) { + p.sendMessage("§c创建失败"); + } + 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; + } + ac.peaceWhitelist.add(parsed[1]); + saveAreaConfig(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; + } + ac.peaceWhitelist.remove(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.add(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.remove(parsed[1]); + saveAreaConfig(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; + } + + // ===== 重载 ===== + if (sub.equals("重载") || sub.equals("reload")) { + reload(); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); + return true; + } + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { + if (args.length == 2) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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; + } + + // ===== 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; + } + 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); + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格"); + 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; + } + 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); + 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; + } + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格"); + return true; + } + + // ===== 删除 ===== + if (sub.equals("删除") || sub.equals("delete")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 删除 <名>"); + return true; + } + File f = new File(rootDir, args[1] + ".txt"); + if (f.exists()) { + f.delete(); + areas.remove(args[1]); + sender.sendMessage("§a已删除: " + args[1]); + } else { + sender.sendMessage("§c不存在: " + args[1]); + } + return true; + } + + // ===== add 加白名单 ===== + if (sub.equals("add")) { + if (args.length == 2) { + String name = args[1]; + 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]; + 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) { + String name = args[1]; + if (!globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); + return true; + } + globalPlayerWhitelist.remove(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]; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.remove(playerName); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); + } else { + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + wl.remove(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; + } + + showHelp(sender); + return true; + } + + + private String[] parseAreaAndTarget(String arg1, String arg2) { + String r1 = resolveAreaName(arg1); + if (r1 != null) { + return new String[]{r1, arg2}; + } + String r2 = resolveAreaName(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) {} + } + + private void showHelp(CommandSender s) { + s.sendMessage("§e§l==== 区域防护 ===="); + s.sendMessage("§a/protect 工具 §7选地工具"); + s.sendMessage("§a/protect 创建 <名> §7创建区域"); + s.sendMessage("§a/protect 列表 §7区域列表"); + s.sendMessage("§a/protect 重载 §7重载配置"); + s.sendMessage("§a/protect 删除 <名> §7删除区域"); + s.sendMessage("§a/protect add [区域] <玩家> §7玩家加白"); + s.sendMessage("§a/protect remove [区域] <玩家> §7玩家删白"); + s.sendMessage("§a/protect additem [区域] <物品> §7物品加黑"); + s.sendMessage("§a/protect removeitem [区域] <物品> §7物品删黑"); + s.sendMessage("§a/protect on §7显示边框"); + s.sendMessage("§a/protect off §7关闭边框"); + s.sendMessage("§a/protect tempon §7临时边框(15秒)"); + s.sendMessage("§a/protect list §7列出全局白名单"); + s.sendMessage("§a/protect list <区域> §7列出区域白名单"); + s.sendMessage("§a/protect listitem §7列出全局物品黑名单"); + s.sendMessage("§a/protect listitem <区域> §7列出区域物品黑名单"); + + } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return globalPlayerWhitelist.contains(playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + return wl != null && wl.contains(playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = wl != null && wl.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + + + public int handleConfiscate(Player p, AreaConfig ac) { + if (isPlayerWhitelisted(p.getName(), ac)) 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 = ""; + + 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(); + all[i] = null; + } + } + p.getInventory().setContents(all); + + ItemStack off = p.getInventory().getItemInOffHand(); + if (off != null && off.getType() == mat) { + count += off.getAmount(); + p.getInventory().setItemInOffHand(null); + } + + if (count > 0) { + totalRemoved += count; + if (!confiscatedNames.isEmpty()) + confiscatedNames += "§7、§e"; + confiscatedNames += "§e" + matname(mat) + + " x" + count; + } + } + + if (totalRemoved > 0) { + String msg = (ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty()) + ? ac.confiscateMsg + : "你携带了违禁品,已被没收"; + 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("[防护] 已清空所有黑名单"); + } + + @EventHandler + public void onEntityDamage( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑 ===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + // 展示框被点击(旋转/放入/取出) + @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(); + + // 全局白放行 + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(clicked); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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 + public void onEntityDamageByEntity( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + // 展示框被攻击 + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑(原有的,不动)===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + 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(); + + AreaConfig ac = getArea(world, bx, by, bz); + if (ac != null && ac.denyItemFrame) 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 && ac.denyItemFrame) return ac; + } + return null; + } + + // ===== 投射物击中展示框(弓箭/弩)===== + @EventHandler + public void onProjectileHit( + org.bukkit.event.entity.ProjectileHitEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + Entity hit = e.getHitEntity(); + if (hit == null) return; + String typeName = hit.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(shooter.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(hit); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(shooter.getName().toLowerCase())) { + 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 onIgnite(BlockIgniteEvent e) { + Player p = e.getPlayer(); + if (p == null) return; // 非玩家点燃的(闪电等) + Block block = e.getBlock(); + AreaConfig ac = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 区域内方块着火(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); + } + + + 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 AreaConfig { + public String name = ""; + public String world = ""; + public int x1, z1, x2, z2; + public int yMin = 0, yMax = 255; + public List confiscateItems = new ArrayList<>(); + public List denyUseItems = new ArrayList<>(); + public List clearEffects = new ArrayList<>(); // 清除单个效果名 + public boolean clearAllBadEffects = false; // 清除所有负面+中性 + public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + public List punishCommands = new ArrayList<>(); + public boolean denyBlockPlace = false; + public boolean denyBlockBreak = false; + public boolean denyPVP = false; + public boolean denyFallDamage = false; + public boolean denyHunger = false; + public boolean denyAllDamage = false; + public boolean denyMount = false; + public boolean denyDrop = false; + public boolean denyEnderPearl = false; + public boolean denyBow = false; + public boolean denyPotion = false; + public boolean denyExplosion = false; + public boolean enableAnnounce = false; + public String announceTemplate = ""; + 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 boolean denyItemFrame = false; + public Set peaceWhitelist = new HashSet<>(); + public String enforceGameMode = null; // 强制游戏模式 + public Set modeExempt = new HashSet<>(); // 模式排除名单 + + + + + 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 (denyDrop) 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 (!peaceWhitelist.isEmpty()) c++; + + return c; + } + } + } \ No newline at end of file diff --git a/backup/20260605_152529/README.md b/backup/20260605_152529/README.md new file mode 100644 index 0000000..e74ef6a --- /dev/null +++ b/backup/20260605_152529/README.md @@ -0,0 +1,569 @@ +# Sdf1系列登录插件使用说明 + +# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改! + +## 1. 功能介绍 + +| 名字 | 描述/作用 | 施工状态 | +| :--- | :--- | :--- | +| 登录 | 用户登录/注册 | 已完成 | +| 世界垃圾箱 | 清理服务器凋落物 | 已完成 | +| 工单系统 | 用户提交工单 | 已完成 | +| 签到系统 | 签到积分换奖励 | 已完成 | +| 新人礼包 | 新玩家7日礼包 | 已完成 | +| 挂机检测 | 挂机检测踢出 | 已完成 | +| 自定义菜单 | 可以自己diy指令 | 已完成 | +| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 | +| 商城系统| 内置游戏商人可供销售商品 | 已完工 | +| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 | + + +## 2.安装说明: + +| 前置插件 | 选装类型 | 功能作用 | +| :--- | :--- | :--- | +| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 +| EssentialsX | 可选 | 主流经济系统 +| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统 + + +### 2.1 下载 +您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。 + +### 2.2. 配置文件 + +#### 2.2.1 消息.txt +此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)
+同时兼容` `标签,对段落加粗,`\n 和
`进行段落换行 + + +```txt +# ========================================== +# Sdf1_login 消息配置文件 +# ========================================== +# 颜色代码使用 § 符号 +# §0黑色 §1深蓝 §2深绿 §3深青 +# §4深红 §5深紫 §6金色 §7灰色 +# §8深灰 §9蓝色 §a绿色 §b浅蓝 +# §c红色 §d粉色 §e黄色 §f白色 +# §l粗体 §n下划线 §o斜体 §r重置 +# +# 占位符用 {xxx} 表示,运行时自动替换 +# 支持 粗体 标签 +# 支持 \n 或
换行 +# 修改后执行 /sdf1_login reload 重载 +# ========================================== + +# ---- 登录注册相关 ---- +# login_timeout: 登录超时被踢出时的提示 +login_timeout=§c登录超时,请重新登录 +# not_registered: 未注册玩家收到的提示 +not_registered=§e您尚未注册,请使用 /reg <密码> +# not_logged_in: 已注册但未登录的提示 +not_logged_in=§e您尚未登录,请使用 /login <密码> +# already_registered: 已注册玩家再次注册时的提示 +already_registered=§c您已注册过账号 +# already_logged_in: 已登录玩家再次登录时的提示 +already_logged_in=§c您已登录 +# reg_success: 注册成功提示 +reg_success=§a注册成功!欢迎来到服务器 +# login_success: 登录成功提示 +login_success=§a登录成功! +# login_failed: 密码错误提示 +login_failed=§c密码错误,请重试 + +# ---- 密码相关 ---- +# password_wrong: 旧密码验证失败 +password_wrong=§c密码错误 +# password_changed: 密码修改成功 +password_changed=§a密码修改成功! +# password_format_error: 密码格式不符合要求 +password_format_error=§c密码需6位以上,含大小写字母和数字 +# password_same: 新密码与旧密码相同 +password_same=§c新密码不能与旧密码相同 +# reset_no_email: 找回密码时未绑定邮箱 +reset_no_email=§c您未绑定邮箱,无法找回密码 +# reset_sent: 临时密码已发送到邮箱 +reset_sent=§a临时密码已发送到您的邮箱,请查收 + +# ---- 签到积分相关 ---- +# checkin_already: 今日已签到 +checkin_already=§c您今日已签到 +# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率 +checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi} +# points_insufficient: 积分不足 +points_insufficient=§c积分不足,当前: {points} +# points_purchase_success: 积分购买成功 +points_purchase_success=§a购买成功!消耗 {count} 积分 + +# ---- 挂机检测相关 ---- +# afk_set_enabled: 挂机检测开启 +afk_set_enabled=§a挂机检测已开启 +# afk_set_disabled: 挂机检测关闭 +afk_set_disabled=§c挂机检测已关闭 +# afk_kick: 因挂机被踢出 +afk_kick=§c因挂机过久被踢出 + +# ---- 邮箱相关 ---- +# email_set: 邮箱设置成功 +email_set=§a邮箱设置成功:{email} +# email_format_error: 邮箱格式错误 +email_format_error=§c邮箱格式不正确 + +# ---- 邀请相关 ---- +# invite_code_generated: 邀请码生成 +invite_code_generated=§a邀请码:{code} +# invite_used: 使用邀请码成功 +invite_used=§a邀请成功,邀请人:{player} +# invite_self: 使用自己的邀请码 +invite_self=§c不能使用自己的邀请码 +# invite_not_found: 邀请码不存在 +invite_not_found=§c邀请码不存在 +# invite_referral_bonus: 被邀请人签到后邀请人获得奖励 +invite_referral_bonus=§a邀请奖励!获得 {points} 积分 + +# ---- 新人礼包相关 ---- +# gift_not_ready: 礼包条件未满足 +gift_not_ready=§c礼包条件未满足 +# gift_claimed: 礼包领取成功 +gift_claimed=§a礼包领取成功! + +# ---- 审批与IP限制 ---- +# need_approval: 注册需审批 +need_approval=§e注册需审批 (工单#{id}) +# ip_max_accounts: IP注册已达上限 +ip_max_accounts=§cIP注册已达上限 + +# ---- 管理员操作 ---- +# admin_delete_confirm: 管理员删除账号确认提示 +admin_delete_confirm=§c请在聊天中输入玩家名确认删除 + +# ---- 聊天过滤 ---- +# chat_muted: 禁言玩家尝试聊天 +chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天 +# chat_url_blocked: 发送违规链接被拦截 +# {url}=检测到的链接地址 +chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url} +# chat_url_violation: 当前违规次数 +# {count}=当前违规次数 +chat_url_violation=§7当前违规次数: §e{count} +# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数 +chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次) +# chat_url_broadcast: 全服通报,{player}=玩家名 +chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚 + +# ---- 通用 ---- +# 未知参数: 未知命令或权限不足时的提示 +未知参数=§c未知参数或权限不足 +# only_player: 仅玩家可用的命令 +# only_player=§c此命令仅玩家可用 + +``` + +### 2.2.2 挂机白名单.txt +当挂机踢出开启时,该配置下的玩家不受挂机提出影响 + +### 2.2.3 SMTP设置.txt + +此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。
可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效 + +```txt +# ===== SMTP 邮件服务器配置 ===== +smtp地址=smtp.example.com +smtp端口=465 +smtp账号=mail@example.com +smtp密码=example.com +发件人名称=Sdf1_login +smtp加密=true +验证码接收邮箱=example@example.com + +``` + +### 2.2.4 插件设置.txt + +此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等 + +```txt +# ===== Sdf1_login 插件设置 ===== +管理标签=admin +管理密码=qweasd +每IP最大账号数=3 +审批模式=manual +自动审批延迟分钟=30 +登录超时秒数=60 +挂机踢出=false +挂机超时秒数=300 +服务商积分倍率=2.0 +基础经济奖励=100 +经济奖励每评分=50.0 +报单人积分奖励=10 +# ---- 签到盲盒奖励 ---- +签到固定金额=0 +签到最小金额=10 +签到最大金额=100 +签到给积分=true +签到积分数量=10 + + +``` + +### 2.2.5 chat.txt +该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。
+可用的处罚规则: +- warn: 警告 +- mute: 禁言 +- kick: 踢出 +- ban: 封禁 +- banip:封禁IP + +```txt +# Sdf1_chat 聊天过滤配置 +通知管理员: true +全服通报: true +禁言时长: 300 + +白名单: +*.minecraft.net +baidu.com +github.com + +处罚规则: +1:warn:0 +2:warn:0 +3:mute:300 +5:mute:600 +8:kick:0 +10:ban:0 + +白名单玩家: + +``` +### 2.2.6 radio_config.txt +```txt +# ============ Sdf1 Radio Config ============ +# +# resource-pack-url +# 填外部地址(HTTP或HTTPS均可) +# 例: https://example.com/radiopack.zip +# 留空 = 使用内置HTTP服务 +# +# http-port +# 内置HTTP端口(仅resource-pack-url为空时生效) +# +# ============ Settings =============== + +resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc +http-port=443 +``` +- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了 + +### 2.2.7 菜单.txt +- 自定义菜单配置文件 + +```json +# 服务器菜单 +# 使用类json格式 +// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持的块注释 + +{ + 标题:主城传送 + 指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录 + 权限类型:玩家 + 图标:COMPASS +} + +{ + 标题:随机传送 + 指令:/rtp + 权限类型:玩家 + 图标:ENDER_PEARL +} + +{ + 标题:擂台比武 + 指令:/dom tp PVP + 权限类型:玩家 + 图标:netherite_spear +} + +{ + 标题:领地操作 + 指令:/dom + 权限类型:玩家 + 图标:paper +} +{ + 标题:逛商店 + 指令:/dom tp shop + 权限类型:玩家 + 图标:honeycomb +} + +{ + 标题:传送开关 + 指令:/tptoggle + 权限类型:玩家 + 图标:redstone_torch +} + +{ + 标题:返回上一个位置 + 指令:/back + 权限类型:玩家 + 图标:red_bed +} +{ + 标题:前往小黑塔 + 指令:/dom tp 小黑塔 + 权限类型:玩家 + 图标:dragon_head +} +{ + 标题:云背包 + 指令:/cy + 权限类型:玩家 + 图标:potato +} +{ + 标题:自杀 + 指令:/suicide + 权限类型:玩家 + 图标:wooden_axe +} +{ + 标题:游戏商人 + 指令:/shop + 权限类型:玩家 + 图标:shield + 格子:28 +} +``` +### 2.2.8 商品配置 +- 配置此处的文件,负责展示游戏内所销售的商品。
+- 一个文件名,对应一个商品分类,使用md文件格式 +``` +# 建筑材料 + +| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 | +| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 | +| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 | +| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 | +| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 | +| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 | +| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 | +| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 | + +``` + +## 2.3 商城部分介绍 +1. 自助退款,买多买错,不用慌,一件退单很安心
+2. 购物车,买的多怕买错,先加购物车,后续统一结算
+3. 害怕订单找不到?一键查看历史订单
+ ![](img/shop4.png) +4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖 +![](img/shop7.png)
+![](img/shop6.png) +5. 想查看玩家消费记录?小手一点,明明白白
+![](img/shop5.png) + + + + +## 2.4 打印流水部分介绍 +1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天
+2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删 + + + +## 3. 指令参数及介绍 + +- Sdf1_login 主指令
+- Sdf1_login reload 重载配置文件
+- reg 注册账号
+- login 登录账号
+- l 登录账号
+- sdf1_login del 删除玩家账号
+- sdf1_login radio 停止播放广播
+- sdf1_login radio reload 重载广播并重新打包资源包
+- menu 开/关 是否开启菜单
+- printer 打印流水数据,支持不带参数&带玩家名为参数
+- sdf1_login printer 作用同printer
+- sdf1_login shop 商城 +- protect 区域防护 + + + +## 4. 自定义菜单物品 +- 打开主界面,选择"我的"进入
+![](img/menu1.png) +- 将你的自定义物品与雪球交换
+![](img/menu2.png) +- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了 + +## 5. pvp战神榜 +- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域
+![](img/pvp1.png) +- 并且有着网游同款的击杀通报
+![](img/pvp3.png) +- 播报逻辑是:
+- 3杀
+- 5杀
+- 5杀以上每杀一次都是一条播报
+- 终结者通报,触发连杀的玩家被终结也会有全服通报 + +## 6. 区域防护 +- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。
+- 还能够保护指定区域的玩家不受任何伤害。
+ +| 指令 | 参数1| 参数2 | 参数3 | 结果 | +| :---: | :---: | :---: | :---: | :---: | +| protect | 无 | 无 | 无 | 帮助信息 | +| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 | +| protect | add | 玩家名 | 无| 加白指定玩家为全局 | +| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 | +| protect | additem | 物品名 | 无 | 加黑指定物品到全局 | +| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 | +| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 | +| protect | remove | 玩家名字 | 无 | 移除玩家为全白 | +| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 | +| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑 +| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑 +| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 | +| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 | +| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 | + +示范: +- `/protect add 末地保护区 players` 添加players玩家到末地保护区
+- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品
+- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。
+- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写 + + +| 参数 | 效果 | +| :---: | :---: | +| 和平模式 | 所有敌对生物全部进虚空 | +| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 | +| 清除XX效果 | 立即清除玩家身上的效果 +| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果 + +## 生物属性对照表 +| 中文名称 | 类型 | 附加属性 | +| :---: | :----: | :----: | +| 溺尸 | 近战/远程 | 亡灵、水生 | +| 骷髅 | 远程 | 亡灵 | +| 流浪者 | 远程 | 亡灵、寒冷群系变种 | +| 沼骸 | 远程 | 亡灵、沼泽变种 | +| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 | +| 掠夺者 | 远程 | 灾厄村民、袭击成员 | +| 卫道士 | 近战 | 灾厄村民、袭击成员 | +| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 | +| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 | +| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 | +| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 | +| 守卫者 | 近战/激光远程 | 海底神殿、水下 | + + +配置文件解析:
+- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的注释`和`Java/* */块注释` +```txt +# 区域: 主城区 +起点: world,-130,-713 +终点: world,-427,-560 +高度范围: 0-255 + +没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个) + // 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块 +// 禁止破坏方块 # 同上 +禁止PVP # 玩家之间不能PVP +禁止摔伤 # 摔落无法造成任何伤害 +禁止饥饿 # 不会消耗饱食度 +禁止一切伤害 # 任何怪物都无法造成伤害 +# 禁止丢弃物品 +# 禁止末影珍珠 +# 禁止使用弓箭 +# 禁止骑乘 +// 和平模式 // 所有的敌对生物全部传送进虚空 +禁止袭击 +禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外 +// 清除所有效果 # 清理所有负面效果 +效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间 +效果 生命提升 255 999999 +效果 抗性提升 255 999999 +效果 抗火 255 999999 +# 禁止药水 +禁止爆炸 +禁止使用物品: TNT +# 效果: 夜视 1 999 +进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示 +离开提示: 已离开保护区 + 没收提示: 你的违禁品已被没收 + 通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域 +# 惩罚命令: ban {player} 60s 违规 + + +``` + +## 运行截图 + +主界面 +![](img/login.png) + +邀请数据 +![](img/login2.png) + +积分商店 +![](img/login3.png) + +任务中心 +![](img/login4.png) + +垃圾箱 +![](img/login5.png) + +管理员面板 +![](img/login6.png) + +管理面板-用户管理 +![](img/login7.png) + + +工单中心 +![](img/login8.png) + +工单管理 +![](img/login9.png) + +我的工单 +![](img/login10.png) + + +提交工单 +![](img/login11.png) + +服务商列表 +![](img/login12.png) + +服务商抢单大厅 +![](img/login13.png) + +服务商-接单界面 +![](img/login14.png) + +管理员-工单详情页面 +![](img/login15.png) + +商城主界面 +![](img/shop1.png) + +商城购物车 +![](img/cart1.png) + +商城退单 +![](img/shop2.png) + + +## 4. 下载地址 +- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login) +- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login) diff --git a/backup/20260605_152529/pom.xml b/backup/20260605_152529/pom.xml new file mode 100644 index 0000000..9d5c185 --- /dev/null +++ b/backup/20260605_152529/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + sdf1 + Sdf1_login + 1.0 + jar + + + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT + provided + + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + org.xerial + sqlite-jdbc + 3.46.0.0 + provided + + + + + + Sdf1_login + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + + diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/AFKManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/AFKManager.java new file mode 100644 index 0000000..f714c1b --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/AFKManager.java @@ -0,0 +1,197 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; + +public class AFKManager { + + // 玩家最后活动时间(空闲才开始计时) + private final Map lastAction = + new HashMap<>(); + // 每个玩家当前的警告阶段 + private final Map lastStage = + new HashMap<>(); + // 是否已进入挂机检测(false=还在活动,不计时) + private final Map afkTracking = + new HashMap<>(); + + private final ConfigManager config; + private final JavaPlugin plugin; + + public AFKManager(ConfigManager config, + JavaPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public void startCheck() { + new BukkitRunnable() { + public void run() { + tick(); + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void tick() { + if (!config.afkEnabled) return; + long now = System.currentTimeMillis(); + + for (Player p : Bukkit.getOnlinePlayers()) { + String name = p.getName(); + UUID uuid = p.getUniqueId(); + + if (!isLoggedIn(name)) continue; + if (config.getAfkWhitelist().contains(name)) + continue; + + // 确保有记录 + if (!lastAction.containsKey(uuid)) { + lastAction.put(uuid, now); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + long last = lastAction.get(uuid); + long idle = (now - last) / 1000; + int timeout = config.afkTimeout; + + // 还在活动范围内:不进入挂机检测 + if (idle < 60) { + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + // 超过60秒无活动:开始挂机检测 + afkTracking.put(uuid, true); + long remain = timeout - idle; + + // 已超时:踢出 + if (remain <= 0) { + p.kickPlayer("§c§l你已挂机 " + + fmtSec(idle) + ",被踢出"); + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + continue; + } + + int stage = lastStage.getOrDefault(uuid, 0); + + // 阶段3:最后10秒 + if (remain <= 10) { + if (stage < 3) { + lastStage.put(uuid, 3); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + remain + " 秒踢出", + 0, 22, 3); + playSound(p); + continue; + } + + // 阶段2:1分钟~10秒 + if (remain <= 60) { + if (stage < 2) { + lastStage.put(uuid, 2); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + fmtSec(remain) + " 踢出", + 5, 25, 5); + playSound(p); + continue; + } + + // 阶段1:3分钟提醒 + if (remain <= 180 && stage < 1) { + lastStage.put(uuid, 1); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§f还剩 " + fmtSec(remain) + " 踢出", + 10, 60, 20); + for (int i = 0; i < 3; i++) { + final int delay = i * 10; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + if (p.isOnline()) playSound(p); + }, delay); + } + } + + if (remain > 180) { + lastStage.remove(uuid); + } + } + } + + /** + * 玩家有活动时调用(移动/交互/放置/挖掘等) + * 重置计时,退出挂机检测 + */ + public void recordAction(UUID uuid) { + lastAction.put(uuid, System.currentTimeMillis()); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + } + + /** + * 玩家下线时调用 + * 彻底停止计时,清理所有数据 + */ + public void remove(UUID uuid) { + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + } + + /** + * 获取玩家当前是否在挂机检测中 + */ + public boolean isAfkTracking(UUID uuid) { + return afkTracking.getOrDefault(uuid, false); + } + + private boolean isLoggedIn(String name) { + return ((Main) plugin).getLoggedIn() + .contains(name); + } + + private void playSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private String fmtSec(long sec) { + if (sec < 60) return sec + "秒"; + long m = sec / 60; + long s = sec % 60; + return m + "分" + (s > 0 ? s + "秒" : ""); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/AccountRequestManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/AccountRequestManager.java new file mode 100644 index 0000000..ae5eaad --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/AccountRequestManager.java @@ -0,0 +1,146 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AccountRequestManager { + + public enum ApprovalMode { AUTO, MANUAL, HYBRID } + + public static class Request { + public final int id; + public final String requester; + public final String target; + public final String ip; + public final long time; + public String status; + public String denyReason; + + public Request(int id, String requester, String target, String ip) { + this.id = id; + this.requester = requester; + this.target = target; + this.ip = ip; + this.time = System.currentTimeMillis(); + this.status = "PENDING"; + this.denyReason = ""; + } + } + + private final Main plugin; + private final Map requests = new ConcurrentHashMap<>(); + private final Map pendingByPlayer = new ConcurrentHashMap<>(); + private int nextId = 1; + private ApprovalMode mode = ApprovalMode.MANUAL; + private int autoDelayMinutes = 60; + + public AccountRequestManager(Main plugin) { + this.plugin = plugin; + } + + public ApprovalMode getMode() { return mode; } + + public void setMode(String m) { + try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } + catch (Exception e) { this.mode = ApprovalMode.MANUAL; } + } + + public void setAutoDelayMinutes(int min) { + this.autoDelayMinutes = min; + } + + public Request createRequest(String requester, String target, String ip) { + int id = nextId++; + Request req = new Request(id, requester, target, ip); + requests.put(id, req); + pendingByPlayer.put(requester, id); + + if (mode == ApprovalMode.AUTO) { + scheduleAutoApprove(id); + } else if (mode == ApprovalMode.HYBRID) { + String targetIp = (String) plugin.getDb().getField(target, "ip_address"); + if (ip != null && ip.equals(targetIp)) { + scheduleAutoApprove(id); + } + } + return req; + } + + private void scheduleAutoApprove(int id) { + long delayTicks = (long) autoDelayMinutes * 60L * 20L; + Request req = requests.get(id); + if (req == null) return; + String requesterName = req.requester; + String targetName = req.target; + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Request r = requests.get(id); + if (r != null && "PENDING".equals(r.status)) { + approveRequest(id); + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); + Player requester = Bukkit.getPlayer(requesterName); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); + } + } + }, delayTicks); + } + + public boolean approveRequest(int id) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "APPROVED"; + plugin.getDb().deleteUser(req.target); + pendingByPlayer.remove(req.requester); + // 清背包+传送出生点+踢出 + Player target = Bukkit.getPlayer(req.target); + if (target != null && target.isOnline()) { + target.getInventory().clear(); + target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); + target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); + target.teleport(target.getWorld().getSpawnLocation()); + target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); + } + return true; + } + + + public boolean denyRequest(int id, String reason) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "DENIED"; + req.denyReason = reason; + pendingByPlayer.remove(req.requester); + return true; + } + + public List getPendingRequests() { + List list = new ArrayList<>(); + for (Request r : requests.values()) { + if ("PENDING".equals(r.status)) list.add(r); + } + return list; + } + + public Request getRequest(int id) { + return requests.get(id); + } + + public boolean hasPending(String player) { + return pendingByPlayer.containsKey(player); + } + + public void cancelPending(String player) { + Integer id = pendingByPlayer.remove(player); + if (id != null) { + Request r = requests.get(id); + if (r != null) r.status = "CANCELLED"; + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/AreaProtection.java b/backup/20260605_152529/src/main/java/Sdf1_login/AreaProtection.java new file mode 100644 index 0000000..bc53d53 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/AreaProtection.java @@ -0,0 +1,4907 @@ +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.entity.Entity; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import java.sql.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +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 java.util.Set; +import java.util.HashSet; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.entity.Item; + + + + +public class AreaProtection implements Listener { + + 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 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<>(); + + // 选地 + private static final Material WAND = Material.BREEZE_ROD; + + private final Map pos1 = + new ConcurrentHashMap<>(); + private final Map pos2 = + new ConcurrentHashMap<>(); + private final Set selecting = + ConcurrentHashMap.newKeySet(); + 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<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + + + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + 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 (p.isOp()) { + plugin.getLogger().info( + "[防护-调试] 豁免原因: OP, 玩家=" + name); + return true; + }*/ + if (globalPlayerWhitelist.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 全局白名单, 玩家=" + + name);*/ + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (aw != null && aw.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 区域白名单, 玩家=" + + name + " 区域=" + areaName);*/ + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 模式排除名单, 玩家=" + + name);*/ + return true; + } + /* plugin.getLogger().info( + "[防护-调试] 无豁免, 玩家=" + name);*/ + 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(); + loadAllAreas(); + loadWhitelists(); + recoverPendingEffects(); + } + 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); + } + + +// ==================== 数据库迁移 ==================== + + /** + * 如果旧位置有 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("========================================"); + } + +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // & → § + 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", "\n"); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + int i = 0; + + while (i < msg.length()) { + 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(); + if (!stack.isEmpty()) { + // 重置后重新应用外层格式 + sb.append("§r"); + for (char c : stack) { + sb.append("§").append(c); + } + } else { + sb.append("§r"); + } + } + 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": return 'l'; + case "i": return 'o'; + case "u": return 'n'; + case "s": return 'm'; + default: return 0; + } + } + +// =============== 加载 ==================== + + public void loadAllAreas() { + areas.clear(); + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files == null) return; + for (File f : files) { + try { + String name = f.getName() + .replace(".txt", ""); + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载失败: " + f.getName()); + } + } + } + + + 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)"); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 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 == '\u200B' // 零宽空格 + || c == '\u200C' // 零宽非连接 + || c == '\u200D' // 零宽连接 + || c == '\uFEFF' // BOM + || c == '\u00A0' // 不间断空格 + || c == '\u2028' // 行分隔符 + || c == '\u2029' // 段分隔符 + || c == '\u200E' // LTR标记 + || c == '\u200F' // RTL标记 + || c == '\u2060' // 字连接符 + || (c >= '\u2000' && c <= '\u200A')) { // 各种空格 + continue; + } + + // 全角→半角映射 + if (c >= '\uFF01' && c <= '\uFF5E') { + c = (char) (c - 0xFEE0); + } + + // 全角空格→半角空格 + if (c == '\u3000') { + 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.denyDrop = 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("和平模式") + && !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() + 5000); + + /* 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); + /* plugin.getLogger().info("[和平] " + p.getName() + + " 附近实体总数=" + nearby.size());*/ + for (Entity ent : nearby) { + String typeName = ent.getType().name(); + /* plugin.getLogger().info("[和平] 实体: " + + typeName + " isHostile=" + isHostile(typeName));*/ + } + int scanned = 0; + int waiting = 0; + int saved = 0; + int banished = 0; + + for (Entity ent : p.getNearbyEntities(48, 48, 48)) { + if (!isHostile(ent.getType().name())) continue; + scanned++; + + UUID entUid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(entUid); + String customName = ent.getCustomName(); + + // 有保护期记录 + if (protectUntil != null) { + long remain = protectUntil - now; + + if (remain > 0) { + // 保护期内,精确剩余秒数 + int secondsLeft = (int) Math.ceil(remain / 1000.0); + waiting++; + + // 保护期内也检查名字(玩家可能刚贴上) + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + protectedEntities.remove(entUid); + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 白名单匹配,永久保留");*/ + continue; + } + + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 保护期剩余约" + secondsLeft + "秒");*/ + continue; + } + + // 保护期到了,最终检查 + protectedEntities.remove(entUid); + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + " 5秒到期");*/ + + // 到期时再查一次白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 到期但白名单匹配,保留");*/ + continue; + } + + // 没名字或不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名=[" + customName + "] 传送虚空");*/ + continue; + } + + // 没有保护期记录(非刷怪蛋生成的或记录被清了) + // 也检查一下白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 无保护期但白名单匹配,保留");*/ + continue; + } + + // 没有保护期且不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 无保护期,命名=[" + customName + "] 传送虚空");*/ + } + + /* plugin.getLogger().info("[和平] " + + " 扫描=" + scanned + + " 等待=" + waiting + + " 保留=" + saved + + " 传送=" + banished + + " 白名单=" + ac.peaceWhitelist);*/ + } + + 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" + )); + + 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; + } + + // ==================== 白名单检查 ==================== + + private boolean isPlayerWhitelisted(String player, + AreaConfig ac) { + if (globalPlayerWhitelist.contains( + player.toLowerCase())) + return true; + Set areaList = + areaPlayerWhitelist.get(ac.name); + return areaList != null + && areaList.contains(player.toLowerCase()); + } + + 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 taskId = Bukkit.getScheduler() + .runTaskTimer(plugin, new Runnable() { + @Override + public void run() { + if (!p.isOnline()) return; + Location loc = p.getLocation(); + int px = loc.getBlockX(); + int py = loc.getBlockY(); + int pz = loc.getBlockZ(); + 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); + for (int dy = 5; dy <= 30; dy += 5) { + for (int x = minX; x <= maxX; x += 4) { + for (int z = minZ; z <= maxZ; z += 4) { + if (x == minX || x == maxX + || z == minZ || z == maxZ) { + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + } + } + } + } + } + }, 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; + + Location from = event.getFrom(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 防抖 + 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) { + // ★ 每次移动都清除指定效果 + clearBadEffects(p, 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); + + // 离开提示 + if (oldAc != null && oldAc.leaveMsg != null + && !oldAc.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(oldAc.leaveMsg)); + } + + // 收标 + 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 (marked != null && !marked.isEmpty()) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★创建延时任务: " + + toClear.size() + "个");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-移动-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + plugin.getLogger().info( + "[防护-移动-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size()); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + /* plugin.getLogger().info( + "[防护-移动-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] + cleared" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + 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) { + if (newAc.enterMsg != null + && !newAc.enterMsg.isEmpty()) { + p.sendMessage( + formatAreaMsg(newAc.enterMsg)); + } + 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()); + } + } + } + +// ===== 清除所有效果 ===== + if (newAc.clearAllBadEffects) { + for (PotionEffect eff : p.getActivePotionEffects()) { + p.removePotionEffect(eff.getType()); + } + plugin.getLogger().info( + "[防护] ★清除所有效果, 玩家=" + p.getName()); + } + + 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); + } + } +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + 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 || ac.giveEffects.isEmpty()) return; + if (ac.giveEffects.isEmpty()) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + for (String[] eff : ac.giveEffects) { + 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); + } + + // 给效果 + 贴标 + List applied = new ArrayList<>(); + for (String[] eff : ac.giveEffects) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + int lv = Integer.parseInt(eff[1]) - 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) { + } + } + + // 贴标 + // 新代码(合并,不覆盖): + 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());*/ + } + } + + /** + * 离开区域时,延时15秒清理效果 + * 流程:收标 → 15秒后清效果 → 确认清空 → 清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); + + // 启动15秒延时 + 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); + } + } + + // 确认清空后清DB + removePlayerEffects(uid, areaName); + plugin.getLogger().info( + "[防护] 15秒已到, 清除 " + + (marked != null ? marked.size() : 0) + + " 个效果 + DB, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + }.runTaskLater(plugin, 300L); // 15秒 = 300 tick + + pendingClearTask.put(uid, task); + } + public Set getAllAreaNames() { + return areas.keySet(); + } + + /** + * 15秒内进了新区域: + * 清除旧区域有、新区域没有的效果 + */ + 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) { + for (String[] eff : newAc.giveEffects) { + 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 都跳过 + */ + private boolean isExemptFromConfiscation( + Player p, String areaName) { + String name = p.getName(); + + if (globalPlayerWhitelist.contains(name)) + return true; + + Set areaWl = + areaPlayerWhitelist.get(areaName); + if (areaWl != null && areaWl.contains(name)) + return true; + + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) + return true; + + return false; + } + + /** + * 没收玩家背包中的违禁品 + */ + private void confiscateItems(Player p, + AreaConfig ac) { + boolean confiscated = false; + + for (int i = 0; + i < p.getInventory().getSize(); i++) { + ItemStack item = p.getInventory().getItem(i); + if (item == null) continue; + + String typeName = item.getType().name(); + if (ac.confiscateItems + .contains(typeName)) { + p.getInventory().setItem(i, null); + confiscated = true; + } + } + + if (confiscated) { + String msg = ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty() + ? ac.confiscateMsg + : "§c你的违禁品已被没收"; + p.sendMessage(msg); + + // 通报批评 + if (ac.enableAnnounce + && ac.announceTemplate != null) { + String text = ac.announceTemplate + .replace("{player}", + p.getName()); + Bukkit.broadcastMessage(text); + } + } + } + + + + private String getChineseGameMode(GameMode mode) { + switch (mode) { + case SURVIVAL: return "生存模式"; + case CREATIVE: return "创造模式"; + case ADVENTURE: return "冒险模式"; + case SPECTATOR: return "旁观模式"; + default: return mode.name(); + } + } + + private void handleEnter(Player p, AreaConfig ac) { + UUID uid = p.getUniqueId(); + + // 清除负面效果 + clearBadEffects(p, ac); + + // 给予效果 + List given = new ArrayList<>(); + for (String[] parts : ac.giveEffects) { + 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) {} + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + given.add(type); + } + } + + removePlayerEffects(uid, ac.name); + if (!given.isEmpty()) { + // savePlayerEffects(uid, p.getName(), ac.name, given); + } + + // 白名单检查(最先执行) + if (isPlayerWhitelisted(p.getName(), ac)) 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; + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return true; + } + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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; + + // 去掉"效果"后缀 + String clean = name; + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + + // 硬编码中文名→英文ID映射(不依赖任何外部Map) + switch (clean) { + // 负面 + case "缓慢": + return resolveEffectType("slowness"); + case "挖掘疲劳": + return resolveEffectType("mining_fatigue"); + case "瞬间伤害": + return resolveEffectType("instant_damage"); + case "反胃": + return resolveEffectType("nausea"); + case "失明": + return resolveEffectType("blindness"); + case "饥饿": + return resolveEffectType("hunger"); + case "虚弱": + return resolveEffectType("weakness"); + case "中毒": + return resolveEffectType("poison"); + case "凋零": + return resolveEffectType("wither"); + case "飘浮": + return resolveEffectType("levitation"); + case "霉运": + return resolveEffectType("unluck"); + case "黑暗": + return resolveEffectType("darkness"); + case "蓄风": + return resolveEffectType("wind_charged"); + case "盘丝": + return resolveEffectType("weaving"); + case "渗浆": + return resolveEffectType("oozing"); + case "寄生": + return resolveEffectType("infested"); + // 中性 + case "不祥之兆": + case "不祥征兆": + return resolveEffectType("bad_omen"); + case "袭击之兆": + case "袭击征兆": + return resolveEffectType("raid_omen"); + case "试炼之兆": + case "试炼征兆": + return resolveEffectType("trial_omen"); + // 正面 + case "迅捷": + return resolveEffectType("speed"); + case "急迫": + return resolveEffectType("haste"); + case "力量": + return resolveEffectType("strength"); + case "瞬间治疗": + return resolveEffectType("instant_health"); + case "跳跃提升": + return resolveEffectType("jump_boost"); + case "生命恢复": + return resolveEffectType("regeneration"); + case "抗性提升": + return resolveEffectType("resistance"); + case "抗火": + return resolveEffectType("fire_resistance"); + case "水下呼吸": + return resolveEffectType("water_breathing"); + case "隐身": + return resolveEffectType("invisibility"); + case "夜视": + return resolveEffectType("night_vision"); + case "发光": + return resolveEffectType("glowing"); + case "生命提升": + return resolveEffectType("health_boost"); + case "伤害吸收": + return resolveEffectType("absorption"); + case "饱和": + return resolveEffectType("saturation"); + case "幸运": + return resolveEffectType("luck"); + case "村庄英雄": + return resolveEffectType("hero_of_the_village"); + case "缓降": + return resolveEffectType("slow_falling"); + case "潮涌能量": + return resolveEffectType("conduit_power"); + case "海豚的恩惠": + return resolveEffectType("dolphins_grace"); + case "致命中毒": + return resolveEffectType("poison"); + default: + break; + } + + +// 兜底:直接用原版英文ID查(★ 注意是 PotionEffectType.getByName,不是 resolveEffectType) + PotionEffectType fallback = PotionEffectType.getByName(clean); + if (fallback != null) return fallback; + fallback = PotionEffectType.getByName(clean.toLowerCase()); + if (fallback != null) return fallback; + + plugin.getLogger().warning("[防护] 无法识别效果: " + name); + 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); + } + + // 创建15秒延时清理 + if (marked != null && !marked.isEmpty()) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★创建延时任务: " + + toClear.size() + "个效果");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-传送-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); +/* + plugin.getLogger().info( + "[防护-传送-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size() + + " 区域=" + clearArea);*/ + + 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); + /* plugin.getLogger().info( + "[防护-传送-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] ★★★完成: 清除" + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + pendingClearTask.put(uid, task); + + } + + // 离开提示 + if (fromArea != null && fromArea.leaveMsg != null + && !fromArea.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(fromArea.leaveMsg)); + } + } + + // ===== 进入新区域 ===== + 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); + } + } + + // 进入提示 + if (toArea != null && toArea.enterMsg != null + && !toArea.enterMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(toArea.enterMsg)); + } + + // 发放效果 + if (toArea != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] 调用applyRegionEffects");*/ + applyRegionEffects(p, uid, toName, toArea); +// 清除效果 + clearBadEffects(p, toArea); + + if (toArea.confiscateItems != null + && !toArea.confiscateItems.isEmpty() + && !isExemptFromConfiscation( + p, toName)) { + confiscateItems(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 (isPlayerWhitelisted(p.getName(), ac)) 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(); + } + + 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) { + for (String[] parts : ac.giveEffects) { + 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) {} + + 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 && ac.denyBlockPlace + && !isPlayerWhitelisted(p.getName(), ac)) { + 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 && ac.denyBlockBreak + && !isPlayerWhitelisted(p.getName(), ac)) { + 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; + // 保护性规则:对所有人无条件生效 + if (ac.denyAllDamage) { e.setCancelled(true); return; } + if (ac.denyFallDamage + && e.getCause() == EntityDamageEvent.DamageCause.FALL) + e.setCancelled(true); + } + + + @EventHandler + public void onPVP(EntityDamageByEntityEvent e) { + if (!(e.getDamager() instanceof Player)) return; + 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 && ac.denyPVP + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + + @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 && 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 && ac.denyDrop + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); + } + } + + // 替换整个 onInteract 方法 + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand == null) return; + + // ===== 选区工具 ===== + if (hand.getType() == Material.BREEZE_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); + 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 { + pos2.put(uid, block.getLocation()); + p.sendMessage("§a§l[防护] §f位置2: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + } + } + return; + } + } + + // ===== 放置实体类物品时不检查(展示框、矿车、船、地图等)===== + Material mat = hand.getType(); + if (mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME + || 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.MAP + || mat == Material.ARMOR_STAND) { + return; + } + + // ===== 区域规则检查 ===== + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + + // 末影珍珠禁止 + if (ac.denyEnderPearl + && hand.getType() == Material.ENDER_PEARL) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); + return; + } + + // 弓箭禁止 + if (ac.denyBow + && (hand.getType() == Material.BOW + || hand.getType() == Material.CROSSBOW)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); + return; + } + + // 白名单跳过 + if (isPlayerWhitelisted(p.getName(), ac)) return; + + // 禁止使用物品 + String typeId = hand.getType().name(); + if (ac.denyUseItems.contains(typeId)) { + 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(); + } + } + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + + 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("#")) { + 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("#")) { + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + 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 && ac.denyPotion + && !isPlayerWhitelisted(shooter.getName(), ac)) { + e.setCancelled(true); + shooter.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; + } + + /** + * 判断字符串是否是区域名(精准或模糊) + */ + 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", "创建", "删除", "重载", + "工具", "expand", "contraction", + "on", "off", "tempon", "modeexempt" + )); + + 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) { + showHelp(sender); + return true; + } + String sub = args[0].toLowerCase(); +// ===== 诊断命令 ===== + 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; + ItemStack tool = new ItemStack(WAND); + ItemMeta meta = tool.getItemMeta(); + meta.setDisplayName("§a§l区域选择工具"); + meta.setLore(Arrays.asList( + "§7左键: 位置1", "§7右键: 位置2")); + tool.setItemMeta(meta); + p.getInventory().addItem(tool); + p.sendMessage("§a§l[防护] §f已获取选择工具"); + return true; + } + + // ===== 创建 ===== + if (sub.equals("创建") || sub.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 创建 <名>"); + return true; + } + Player p = (Player) sender; + UUID u = p.getUniqueId(); + if (!pos1.containsKey(u) + || !pos2.containsKey(u)) { + p.sendMessage("§c请先用工具选点"); + return true; + } + String areaName = args[1]; + File f = new File(rootDir, areaName + ".txt"); + if (f.exists()) { + p.sendMessage("§c区域已存在"); + return true; + } + Location l1 = pos1.get(u); + Location l2 = pos2.get(u); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 区域: " + areaName); + pw.println("起点: " + l1.getWorld().getName() + + "," + l1.getBlockX() + + "," + l1.getBlockZ()); + pw.println("终点: " + l2.getWorld().getName() + + "," + l2.getBlockX() + + "," + l2.getBlockZ()); + pw.println("高度范围: 0-255"); + pw.println(); + pw.println("# 禁止放置方块"); + pw.println("# 禁止破坏方块"); + pw.println("# 禁止PVP"); + pw.println("# 效果: 夜视 1 999"); + pw.println("# 进入提示: 欢迎来到保护区"); + pw.println("# 离开提示: 已离开保护区"); + pw.close(); + p.sendMessage("§a§l[防护] §f区域 " + + areaName + " 已创建"); + loadAllAreas(); + } catch (IOException ex) { + p.sendMessage("§c创建失败"); + } + 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; + } + ac.peaceWhitelist.add(parsed[1]); + saveAreaConfig(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; + } + ac.peaceWhitelist.remove(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.add(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.remove(parsed[1]); + saveAreaConfig(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; + } + + // ===== 重载 ===== + if (sub.equals("重载") || sub.equals("reload")) { + reload(); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); + return true; + } + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { + if (args.length == 2) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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; + } + + // ===== 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; + } + 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); + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格"); + 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; + } + 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); + 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; + } + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格"); + return true; + } + + // ===== 删除 ===== + if (sub.equals("删除") || sub.equals("delete")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 删除 <名>"); + return true; + } + File f = new File(rootDir, args[1] + ".txt"); + if (f.exists()) { + f.delete(); + areas.remove(args[1]); + sender.sendMessage("§a已删除: " + args[1]); + } else { + sender.sendMessage("§c不存在: " + args[1]); + } + return true; + } + + // ===== add 加白名单 ===== + if (sub.equals("add")) { + if (args.length == 2) { + String name = args[1]; + 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]; + 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) { + String name = args[1]; + if (!globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); + return true; + } + globalPlayerWhitelist.remove(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]; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.remove(playerName); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); + } else { + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + wl.remove(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; + } + + showHelp(sender); + return true; + } + + + private String[] parseAreaAndTarget(String arg1, String arg2) { + String r1 = resolveAreaName(arg1); + if (r1 != null) { + return new String[]{r1, arg2}; + } + String r2 = resolveAreaName(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) {} + } + + private void showHelp(CommandSender s) { + s.sendMessage("§e§l==== 区域防护 ===="); + s.sendMessage("§a/protect 工具 §7选地工具"); + s.sendMessage("§a/protect 创建 <名> §7创建区域"); + s.sendMessage("§a/protect 列表 §7区域列表"); + s.sendMessage("§a/protect 重载 §7重载配置"); + s.sendMessage("§a/protect 删除 <名> §7删除区域"); + s.sendMessage("§a/protect add [区域] <玩家> §7玩家加白"); + s.sendMessage("§a/protect remove [区域] <玩家> §7玩家删白"); + s.sendMessage("§a/protect additem [区域] <物品> §7物品加黑"); + s.sendMessage("§a/protect removeitem [区域] <物品> §7物品删黑"); + s.sendMessage("§a/protect on §7显示边框"); + s.sendMessage("§a/protect off §7关闭边框"); + s.sendMessage("§a/protect tempon §7临时边框(15秒)"); + s.sendMessage("§a/protect list §7列出全局白名单"); + s.sendMessage("§a/protect list <区域> §7列出区域白名单"); + s.sendMessage("§a/protect listitem §7列出全局物品黑名单"); + s.sendMessage("§a/protect listitem <区域> §7列出区域物品黑名单"); + + } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return globalPlayerWhitelist.contains(playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + return wl != null && wl.contains(playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = wl != null && wl.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + + + public int handleConfiscate(Player p, AreaConfig ac) { + if (isPlayerWhitelisted(p.getName(), ac)) 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 = ""; + + 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(); + all[i] = null; + } + } + p.getInventory().setContents(all); + + ItemStack off = p.getInventory().getItemInOffHand(); + if (off != null && off.getType() == mat) { + count += off.getAmount(); + p.getInventory().setItemInOffHand(null); + } + + if (count > 0) { + totalRemoved += count; + if (!confiscatedNames.isEmpty()) + confiscatedNames += "§7、§e"; + confiscatedNames += "§e" + matname(mat) + + " x" + count; + } + } + + if (totalRemoved > 0) { + String msg = (ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty()) + ? ac.confiscateMsg + : "你携带了违禁品,已被没收"; + 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("[防护] 已清空所有黑名单"); + } + + @EventHandler + public void onEntityDamage( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑 ===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + // 展示框被点击(旋转/放入/取出) + @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(); + + // 全局白放行 + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(clicked); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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 + public void onEntityDamageByEntity( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + // 展示框被攻击 + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑(原有的,不动)===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + 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(); + + AreaConfig ac = getArea(world, bx, by, bz); + if (ac != null && ac.denyItemFrame) 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 && ac.denyItemFrame) return ac; + } + return null; + } + + // ===== 投射物击中展示框(弓箭/弩)===== + @EventHandler + public void onProjectileHit( + org.bukkit.event.entity.ProjectileHitEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + Entity hit = e.getHitEntity(); + if (hit == null) return; + String typeName = hit.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(shooter.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(hit); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(shooter.getName().toLowerCase())) { + 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 onIgnite(BlockIgniteEvent e) { + Player p = e.getPlayer(); + if (p == null) return; // 非玩家点燃的(闪电等) + Block block = e.getBlock(); + AreaConfig ac = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 区域内方块着火(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); + } + + + 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 AreaConfig { + public String name = ""; + public String world = ""; + public int x1, z1, x2, z2; + public int yMin = 0, yMax = 255; + public List confiscateItems = new ArrayList<>(); + public List denyUseItems = new ArrayList<>(); + public List clearEffects = new ArrayList<>(); // 清除单个效果名 + public boolean clearAllBadEffects = false; // 清除所有负面+中性 + public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + public List punishCommands = new ArrayList<>(); + public boolean denyBlockPlace = false; + public boolean denyBlockBreak = false; + public boolean denyPVP = false; + public boolean denyFallDamage = false; + public boolean denyHunger = false; + public boolean denyAllDamage = false; + public boolean denyMount = false; + public boolean denyDrop = false; + public boolean denyEnderPearl = false; + public boolean denyBow = false; + public boolean denyPotion = false; + public boolean denyExplosion = false; + public boolean enableAnnounce = false; + public String announceTemplate = ""; + 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 boolean denyItemFrame = false; + public Set peaceWhitelist = new HashSet<>(); + public String enforceGameMode = null; // 强制游戏模式 + public Set modeExempt = new HashSet<>(); // 模式排除名单 + + + + + 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 (denyDrop) 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 (!peaceWhitelist.isEmpty()) c++; + + return c; + } + } + } \ No newline at end of file diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/BondManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/BondManager.java new file mode 100644 index 0000000..ea78152 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/BondManager.java @@ -0,0 +1,697 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import java.io.File; +import java.sql.*; +import java.util.*; + +public class BondManager { + + private final Main plugin; + private Connection db; + + // ===== 交易类型常量 ===== + public static final String TX_ADMIN_GIVE = "admin_give"; + public static final String TX_ADMIN_DEDUCT = "admin_deduct"; + public static final String TX_TRANSFER_OUT = "transfer_out"; + public static final String TX_TRANSFER_IN = "transfer_in"; + public static final String TX_REDEEM = "redeem"; + public static final String TX_SHOP_BUY = "shop_buy"; + public static final String TX_DAILY_SIGN = "daily_sign"; + public static final String TX_FREEZE = "freeze"; + public static final String TX_UNFREEZE = "unfreeze"; + + + public static final int MAX_WATER_DAYS = 14; + public static final int ADMIN_MAX_WATER_DAYS = 30; + + public BondManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + // ======================== 数据库初始化 ======================== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 债券余额(含冻结状态) + st.execute("CREATE TABLE IF NOT EXISTS bonds (" + + "player_name TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "status TEXT DEFAULT 'normal')"); + + // 兼容旧表 + try { + st.execute("ALTER TABLE bonds " + + "ADD COLUMN status " + + "TEXT DEFAULT 'normal'"); + } catch (SQLException ignored) {} + + // 旧流水(保留兼容) + st.execute("CREATE TABLE IF NOT EXISTS bond_log (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "reason TEXT DEFAULT ''," + + "time INTEGER DEFAULT 0)"); + + // 新交易流水表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "bond_transaction (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "type TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "target_player TEXT DEFAULT ''," + + "operator TEXT DEFAULT ''," + + "reason TEXT DEFAULT ''," + + "balance_after INTEGER DEFAULT 0," + + "time INTEGER DEFAULT 0)"); + + try { + st.execute("CREATE INDEX " + + "IF NOT EXISTS idx_tx_pt " + + "ON bond_transaction(" + + "player_name, time)"); + } catch (SQLException ignored) {} + try { + st.execute("ALTER TABLE bond_transaction " + + "ADD COLUMN balance_before " + + "INTEGER DEFAULT 0"); + } catch (SQLException ignored) {} + + + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[Bond] DB init failed: " + + e.getMessage(), e); + } + } + + // ======================== 账户状态 ======================== + + public boolean isFrozen(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return "frozen".equals(s); + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return false; + } + + public String getAccountStatus(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return s != null ? s : "normal"; + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return "normal"; + } + + public void freezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='frozen' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + public void unfreezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='normal' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + private void ensureAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR IGNORE INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,0,'normal')"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + // ======================== 余额查询 ======================== + + public int getBonds(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + int v = 0; + if (rs.next()) v = rs.getInt("amount"); + rs.close(); ps.close(); + return v; + } catch (SQLException e) { return 0; } + } + + /** 该玩家是否在 bond 表中有真实记录 */ + public boolean hasBondAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM bonds " + + "WHERE player_name=? " + + "AND amount > 0"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next(); + rs.close(); ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ======================== 增加债券 ======================== + + public int addBonds(String player, int amount) { + return addBondsInternal(player, amount, + TX_ADMIN_GIVE, "", "", + "管理员发放"); + } + + public int addBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return -1; + return addBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + private int addBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + int after = before + amount; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status)" + + " VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, amount); + return after; + } + + // ======================== 扣除债券 ======================== + + + + private boolean deductBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + if (before < amount) return false; + int after = before - amount; + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET amount=? " + + "WHERE player_name=?"); + ps.setInt(1, after); + ps.setString(2, player); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, -amount); + return true; + } + + // ======================== 流水写入 ======================== + + private void logTransaction(String player, + String type, + int amount, + String targetPlayer, + String operator, + String reason, + int balanceBefore, + int balanceAfter) { + try { + // 合并模式:shop_buy 30秒内同商品合并 + if ("shop_buy".equals(type) + && reason != null + && !reason.isEmpty()) { + long mergeWindow = + System.currentTimeMillis() - 30000L; + PreparedStatement chk = + db.prepareStatement( + "SELECT id, amount " + + "FROM bond_transaction " + + "WHERE player_name=? " + + "AND type=? " + + "AND reason=? " + + "AND time > ? " + + "ORDER BY time DESC " + + "LIMIT 1"); + chk.setString(1, player); + chk.setString(2, type); + chk.setString(3, reason); + chk.setLong(4, mergeWindow); + ResultSet rs = chk.executeQuery(); + if (rs.next()) { + int existId = rs.getInt("id"); + int existAmt = rs.getInt("amount"); + rs.close(); + chk.close(); + PreparedStatement upd = + db.prepareStatement( + "UPDATE bond_transaction " + + "SET amount=?, " + + "balance_after=?, " + + "time=? " + + "WHERE id=?"); + upd.setInt(1, existAmt + amount); + upd.setInt(2, balanceAfter); + upd.setLong(3, + System.currentTimeMillis()); + upd.setInt(4, existId); + upd.executeUpdate(); + upd.close(); + return; + } + rs.close(); + chk.close(); + } + + // 原始插入 + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_transaction " + + "(player_name, type, amount," + + " target_player, operator," + + " reason, balance_before," + + " balance_after, time)" + + " VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, type); + ps.setInt(3, amount); + ps.setString(4, + targetPlayer != null ? targetPlayer : ""); + ps.setString(5, + operator != null ? operator : ""); + ps.setString(6, + reason != null ? reason : ""); + ps.setInt(7, balanceBefore); + ps.setInt(8, balanceAfter); + ps.setLong(9, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + // ======================== 扣除债券 ======================== + + /** 完整版:含冻结检查 + 流水记录 */ + public boolean deductBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return false; + return deductBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + public boolean checkoutCart(String player, + String[] itemNames, + int[] subtotals) { + int total = 0; + for (int s : subtotals) total += s; + if (isFrozen(player)) return false; + + int before = getBonds(player); + if (before < total) return false; + int after = before - total; + + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?,'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + + int running = before; + for (int i = 0; i < itemNames.length; i++) { + int next = running - subtotals[i]; + logTransaction(player, "shop_buy", subtotals[i], + "", "商店系统", + "购物车购买-" + itemNames[i], + running, next); + running = next; + } + + logLegacy(player, -total); + return true; + } + + public boolean setBonds(String player, int amount) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setInt(3, amount); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return true; + } + + // ======================== 转账 ======================== + + public TransferResult transfer(String from, + String to, + int amount) { + if (from.equalsIgnoreCase(to)) + return new TransferResult(false, + "不能给自己转账"); + if (isFrozen(from)) + return new TransferResult(false, + "你的账户已被冻结"); + if (isFrozen(to)) + return new TransferResult(false, + to + " 的账户已被冻结"); + if (isSameIP(from, to)) + return new TransferResult(false, + "不能与同IP玩家转账"); + + if (!deductBondsInternal(from, amount, + TX_TRANSFER_OUT, to, from, + "转账给" + to)) + return new TransferResult(false, "债券不足"); + + addBondsInternal(to, amount, + TX_TRANSFER_IN, from, from, + "收到来自" + from + "的转账"); + + return new TransferResult(true, "转账成功"); + } + + // ======================== 同IP检测 ======================== + + public boolean isSameIP(String p1, String p2) { + String ip1 = getPlayerIPAny(p1); + String ip2 = getPlayerIPAny(p2); + if (ip1 == null || ip2 == null) return false; + return ip1.equals(ip2); + } + + /** 获取同IP下的所有其他在线玩家 */ + public List getSameIPPlayers(String player) { + String ip = getPlayerIPAny(player); + if (ip == null) return Collections.emptyList(); + List list = new ArrayList<>(); + for (Player p : + plugin.getServer().getOnlinePlayers()) { + String pIp = plugin.getPlayerIP(p); + if (pIp != null && pIp.equals(ip) + && !p.getName() + .equalsIgnoreCase(player)) { + list.add(p.getName()); + } + } + return list; + } + + private String getPlayerIPAny(String player) { + // 在线:直接取 + Player p = plugin.getServer() + .getPlayerExact(player); + if (p != null) { + String ip = plugin.getPlayerIP(p); + if (ip != null) return ip; + } + // 离线:查主库 + try { + Object v = plugin.getDb() + .getField(player, "last_login_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + try { + Object v = plugin.getDb() + .getField(player, "register_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + return null; + } + + // ======================== 流水查询 ======================== + + public List> getTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } +// 放在 getTransactions 方法后面: + + /** + * 获取该玩家全部流水(不限时间,供打印使用) + */ + public List> + getAllTransactions(String player) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "ORDER BY time ASC"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("player_name", + rs.getString("player_name")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } +// 放在 getAllTransactions 方法后面: + + /** + * 获取所有有流水记录的玩家名(去重) + */ + public List getAllPlayerNames() { + List list = new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT DISTINCT player_name " + + "FROM bond_transaction " + + "ORDER BY player_name ASC"); + while (rs.next()) { + list.add(rs.getString("player_name")); + } + rs.close(); st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTransferTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "AND (type=? OR type=?) " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ps.setString(3, TX_TRANSFER_OUT); + ps.setString(4, TX_TRANSFER_IN); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } + + + private void logLegacy(String player, int amount) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_log " + + "(player_name,amount,time) " + + "VALUES (?,?,?)"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + public Map getLog( + String player, int limit) { + Map info = new LinkedHashMap<>(); + info.put("balance", getBonds(player)); + return info; + } + + // ======================== 转账结果 ======================== + + public static class TransferResult { + public final boolean success; + public final String message; + public TransferResult(boolean success, + String message) { + this.success = success; + this.message = message; + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/BondPrinter.java b/backup/20260605_152529/src/main/java/Sdf1_login/BondPrinter.java new file mode 100644 index 0000000..674ec3c --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/BondPrinter.java @@ -0,0 +1,378 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +public class BondPrinter { + + private final Main plugin; + private final File outputDir; + + private static final SimpleDateFormat FILE_SDF = + new SimpleDateFormat( + "yyyy-MM-dd_HH-mm-ss"); + private static final SimpleDateFormat CELL_SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private static final LinkedHashMap + TYPE_NAMES = new LinkedHashMap<>(); + + static { + TYPE_NAMES.put("admin_give", "管理员发放"); + TYPE_NAMES.put("admin_deduct", "管理员扣除"); + TYPE_NAMES.put("transfer_out", "转出"); + TYPE_NAMES.put("transfer_in", "转入"); + TYPE_NAMES.put("redeem", "CDK兑换"); + TYPE_NAMES.put("shop_buy", "商城购买"); + TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上 + TYPE_NAMES.put("daily_sign", "签到奖励"); + TYPE_NAMES.put("freeze", "冻结"); + TYPE_NAMES.put("unfreeze", "解冻"); + + } + + public BondPrinter(Main plugin) { + this.plugin = plugin; + this.outputDir = new File( + plugin.getDataFolder(), "printer"); + if (!this.outputDir.exists()) { + this.outputDir.mkdirs(); + } + } + + // ============ 打印单个玩家 ============ + + public void printPlayer(String player, + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintPlayer( + player, sender)); + } + + private void doPrintPlayer(String player, + CommandSender sender) { + try { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + + String fileName = player + "_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + deleteOld(player); + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, + escXml(player)); + writeXmlRow(pw, + new String[]{ + "时间", "类型", + "金额", "对象", + "操作者", "理由", + "操作前余额", + "操作后余额"}); + + for (Map tx : txs) { + writeXmlRow(pw, new String[]{ + txTime(tx), + txTypeName(tx), + String.valueOf( + txInt(tx, + "amount")), + txStr(tx, + "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")) + }); + } + + writeXmlFooter(pw); + pw.flush(); + } finally { + pw.close(); + } + + if (sender != null) { + sender.sendMessage("§a[打印] §f" + + player + " §a流水已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + txs.size() + + " §7条,§724小时后删除"); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 打印全服 ============ + + public void printAll(CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintAll(sender)); + } + + private void doPrintAll(CommandSender sender) { + try { + List players = + plugin.getBonds() + .getAllPlayerNames(); + if (players.isEmpty()) { + if (sender != null) { + sender.sendMessage("§c没有流水数据"); + } + return; + } + + String fileName = "全服流水_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + // ===== 收集所有流水 ===== + // key = 时间戳+玩家+金额,用于去重 + // value = 一行数据 + List allRows = new ArrayList<>(); + Set seenKeys = new HashSet<>(); + + for (String player : players) { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + for (Map tx : txs) { + int txId = ((Number) tx.get("id")) + .intValue(); + String dedupKey = + String.valueOf(txId); + if (!seenKeys.add(dedupKey)) { + continue; + } + + String timeStr = txTime(tx); + long timeMs = 0; + Object t = tx.get("time"); + if (t != null) { + timeMs = + ((Number) t).longValue(); + } + + allRows.add(new String[]{ + player, + timeStr, + txTypeName(tx), + String.valueOf( + txInt(tx, "amount")), + txStr(tx, "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")), + // 额外存时间戳用于排序 + String.valueOf(timeMs) + }); + } + } + + // ===== 按时间戳排序 ===== + allRows.sort((a, b) -> { + long ta = Long.parseLong( + a[a.length - 1]); + long tb = Long.parseLong( + b[b.length - 1]); + return Long.compare(ta, tb); + }); + + // ===== 写入文件 ===== + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, "全服流水"); + writeXmlRow(pw, new String[]{ + "玩家", "时间", "类型", + "金额", "对象", "操作者", + "理由", "操作前余额", + "操作后余额" + }); + + for (String[] row : allRows) { + // 去掉最后的时间戳列 + String[] cells = + new String[row.length - 1]; + System.arraycopy(row, 0, + cells, 0, cells.length); + writeXmlRow(pw, cells); + } + + writeXmlFooter(pw); + pw.flush(); + + if (sender != null) { + sender.sendMessage( + "§a[打印] §f全服流水 §a已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + players.size() + + " 名玩家,§f" + + allRows.size() + " §7条"); + sender.sendMessage( + "§7按时间从早到晚排序"); + sender.sendMessage( + "§724小时后自动删除"); + } + } finally { + pw.close(); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 清理 ============ + + public void cleanupOldFiles() { + long now = System.currentTimeMillis(); + long maxAge = 24L * 60 * 60 * 1000; + File[] files = outputDir.listFiles( + (d, n) -> n.endsWith(".xls")); + if (files == null) { + return; + } + int count = 0; + for (File f : files) { + if (now - f.lastModified() > maxAge) { + if (f.delete()) { + count++; + } + } + } + if (count > 0) { + plugin.getLogger().info( + "[Printer] 清理 " + + count + " 个过期文件"); + } + } + + private void deleteOld(String player) { + File[] old = outputDir.listFiles( + (d, n) -> n.startsWith(player + "_") + && n.endsWith(".xls")); + if (old != null) { + for (File f : old) { + f.delete(); + } + } + } + + // ============ XML工具 ============ + + private void writeXmlHeader(PrintWriter pw, + String sheetName) { + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(" "); + pw.println(" "); + } + + private void writeXmlRow(PrintWriter pw, + String[] cells) { + pw.println(" "); + for (String cell : cells) { + pw.println(" " + + "" + + escXml(cell != null ? cell : "") + + ""); + } + pw.println(" "); + } + + private void writeXmlFooter(PrintWriter pw) { + pw.println("
"); + pw.println("
"); + pw.println("
"); + } + + private String escXml(String val) { + if (val == null) { + return ""; + } + return val.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private String txTime(Map tx) { + Object t = tx.get("time"); + if (t == null) return ""; + long ms = ((Number) t).longValue(); + return CELL_SDF.format(new Date(ms)); + } + + private String txTypeName( + Map tx) { + String type = (String) tx.get("type"); + return TYPE_NAMES.getOrDefault(type, type); + } + + private int txInt(Map tx, + String key) { + Object v = tx.get(key); + if (v == null) return 0; + return ((Number) v).intValue(); + } + + private String txStr(Map tx, + String key) { + Object v = tx.get(key); + return v != null ? v.toString() : ""; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/CDKManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/CDKManager.java new file mode 100644 index 0000000..e07569f --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/CDKManager.java @@ -0,0 +1,293 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.nio.charset.StandardCharsets; + +public class CDKManager { + + private final Main plugin; + private Connection db; + private final Map pendingInputs + = new ConcurrentHashMap<>(); + + public CDKManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS cdk (" + + "code TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "type TEXT DEFAULT 'bond'," + + "used INTEGER DEFAULT 0," + + "used_by TEXT DEFAULT ''," + + "created_time INTEGER DEFAULT 0," + + "used_time INTEGER DEFAULT 0)"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[CDK] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== CDK兑换(供SDF1调用) ===== + + /** + * 兑换CDK,返回结果字符串 + * @return "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeem(String code, String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount,type,used FROM cdk " + + "WHERE code=?"); + ps.setString(1, code); + ResultSet rs = ps.executeQuery(); + if (!rs.next()) { + rs.close(); ps.close(); + return "fail:not_found"; + } + int amount = rs.getInt("amount"); + String type = rs.getString("type"); + int used = rs.getInt("used"); + rs.close(); ps.close(); + + if (used == 1) + return "fail:already_used"; + + if (plugin.getBonds() + .isFrozen(player)) + return "fail:frozen"; + + PreparedStatement up = db.prepareStatement( + "UPDATE cdk SET used=1," + + "used_by=?,used_time=? " + + "WHERE code=?"); + up.setString(1, player); + up.setLong(2, + System.currentTimeMillis()); + up.setString(3, code); + up.executeUpdate(); up.close(); + + if ("bond".equals(type)) { + int bef = plugin.getBonds() + .getBonds(player); + plugin.getBonds().addBonds(player, + amount, + BondManager.TX_REDEEM, + "", "CDK系统", + "CDK兑换: " + code); + int aft = plugin.getBonds() + .getBonds(player); + return "success:" + amount + + ":" + bef + ":" + aft; + } + return "fail:unknown_type"; + } catch (SQLException e) { + return "fail:" + e.getMessage(); + } + } + + // ===== 聊天输入系统 ===== + + public void requestInput(Player p, + String type, + String context) { + CDKInput input = new CDKInput(); + input.type = type; + input.context = context; + input.time = System.currentTimeMillis(); + pendingInputs.put(p.getName(), input); + } + + public boolean isListening(String playerName) { + return pendingInputs + .containsKey(playerName); + } + + public void onChat(Player p, String msg) { + CDKInput input = pendingInputs + .remove(p.getName()); + if (input == null) return; + + if (System.currentTimeMillis() + - input.time > 60000) { + p.sendMessage("§c输入超时"); + return; + } + + if ("cdk".equals(input.type)) { + String result = redeem( + msg.trim(), p.getName()); + if (result != null + && result.startsWith("success:")) { + String[] parts = result.split(":"); + int amt = Integer.parseInt(parts[1]); + int bef = Integer.parseInt(parts[2]); + int aft = Integer.parseInt(parts[3]); + p.sendMessage("§e§l[CDK] §f兑换成功!" + + "获得 §c" + amt + "§f债券"); + p.sendMessage("§7余额: §e" + bef + + " §7-> §a" + aft); + } else { + p.sendMessage("§c§l[CDK] §f" + + (result != null + ? result.replace("fail:", "") + : "无效的兑换码")); + } + return; + } + + if ("econ".equals(input.type)) { + handleEconInput(p, msg.trim(), + input.context); + return; + } + + if ("bond".equals(input.type)) { + handleBondInput(p, msg.trim(), + input.context); + } + } + + private void handleEconInput(Player admin, + String msg, + String target) { + net.milkbowl.vault.economy.Economy econ + = null; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy + .Economy.class); + if (reg != null) + econ = reg.getProvider(); + } catch (Exception ignored) {} + if (econ == null) { + admin.sendMessage("§c经济系统不可用"); + return; + } + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String numStr = + msg.substring(1).trim(); + double amount = + Double.parseDouble(numStr); + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + if (msg.startsWith("-")) { + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp == null) { + admin.sendMessage("§c" + target + + " 不在线"); + return; + } + if (!econ.has(tp, amount)) { + admin.sendMessage("§c余额不足"); + return; + } + econ.withdrawPlayer(tp, amount); + admin.sendMessage("§a扣款成功"); + } else { + econ.depositPlayer(target, amount); + admin.sendMessage("§a到账成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + private void handleBondInput(Player admin, + String msg, + String target) { + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String sign = msg.substring(0, 1); + String rest = + msg.substring(1).trim(); + String[] parts = + rest.split("\\s+", 2); + int amount = Integer.parseInt(parts[0]); + String reason = parts.length >= 2 + ? parts[1].trim() + : "管理员手动调整"; + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + String opName = admin.getName(); + String txType = "-".equals(sign) + ? BondManager.TX_ADMIN_DEDUCT + : BondManager.TX_ADMIN_GIVE; + + if ("-".equals(sign)) { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + if (plugin.getBonds().deductBonds( + target, amount, txType, + "", opName, reason)) { + admin.sendMessage("§a扣除成功"); + } else { + admin.sendMessage("§c债券不足"); + } + } else { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + plugin.getBonds().addBonds(target, + amount, txType, + "", opName, reason); + admin.sendMessage("§a发放成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + public void cancelInput(String player) { + pendingInputs.remove(player); + } + + static class CDKInput { + String type; + String context; + long time; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ChatFilterManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/ChatFilterManager.java new file mode 100644 index 0000000..59cb5de --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ChatFilterManager.java @@ -0,0 +1,612 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatFilterManager { + + private final JavaPlugin plugin; + private final List whitelistUrls = + new ArrayList<>(); + private final Set whitelistPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map + punishmentRules = + new LinkedHashMap<>(); + private final Map + punishmentDurations = + new LinkedHashMap<>(); + private final Map + violationCount = + new ConcurrentHashMap<>(); + private final Map + mutedPlayers = + new ConcurrentHashMap<>(); + private final Map messages = + new LinkedHashMap<>(); + private boolean notifyAdmin = true; + private boolean notifyAll = false; + private int muteDuration = 300; + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean v) { + enabled = v; + } + + + private static final Pattern URL_PATTERN = + Pattern.compile( + "(?i)(?:https?://\\S+)" + + "|(?:(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9\\-]*" + + "[a-zA-Z0-9])?\\.)+" + + "[a-zA-Z]{2,})" + + "(?:/\\S*)?"); + private static final Pattern DOT_VARIANTS = + Pattern.compile( + "[\uff0e\u3002\u2025\u2026\u00b7]"); + private static final Pattern COLOR_CODES = + Pattern.compile( + "\u00a7[0-9a-fk-orA-FK-OR]"); + + private static final Set FILE_EXT; + static { + Set s = new HashSet<>( + Arrays.asList( + "txt", "yml", "yaml", + "json", "xml", "java", + "class", "jar", "png", + "jpg", "jpeg", "gif", + "bmp", "svg", "ico", + "webp", "mp3", "mp4", + "avi", "mkv", "wav", + "flac", "ogg", "zip", + "rar", "7z", "tar", + "gz", "cfg", "conf", + "ini", "log", "db", + "sql", "sh", "bat", + "html", "htm", "css", + "js", "md", "pdf", + "dll", "so", "exe", + "bin", "c", "cpp", + "py", "rb", "go", + "rs", "kt", "lua", + "php", "asp", "jsp")); + FILE_EXT = Collections.unmodifiableSet(s); + } + + public ChatFilterManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + // ========== 配置加载 ========== + + public void loadConfig() { + whitelistUrls.clear(); + whitelistPlayers.clear(); + punishmentRules.clear(); + punishmentDurations.clear(); + messages.clear(); + + File f = new File( + plugin.getDataFolder(), "chat.txt"); + if (!f.exists()) writeDefaultFile(f); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("白名单") + || t.equals("白名单:")) { + section = "whitelist"; + continue; + } + if (t.equals("处罚规则") + || t.equals("处罚规则:")) { + section = "punishment"; + continue; + } + if (t.equals("白名单玩家") + || t.equals("白名单玩家:")) { + section = "players"; + continue; + } + if (t.contains(":")) { + String[] kv = t.split(":", 2); + String k = kv[0].trim(); + String v = kv[1].trim(); + if (equals("启用过滤") + || equals("enabled")) { + enabled = parseBool(v); + continue; + } + + if (k.equals("通知管理员")) { + notifyAdmin = parseBool(v); + continue; + } + if (k.equals("全服通报")) { + notifyAll = parseBool(v); + continue; + } + if (k.equals("禁言时长")) { + try { + muteDuration = + Integer.parseInt(v); + } catch (Exception ignored) { + } + continue; + } + } + switch (section) { + case "whitelist": + whitelistUrls.add( + t.toLowerCase()); + break; + case "punishment": + String[] parts = t.split(":"); + if (parts.length >= 2) { + try { + int cnt = + Integer.parseInt( + parts[0] + .trim()); + String type = + parts[1].trim() + .toLowerCase(); + int dur = + parts.length >= 3 + ? Integer.parseInt( + parts[2] + .trim()) + : muteDuration; + punishmentRules.put( + cnt, type); + punishmentDurations.put( + cnt, dur); + } catch (Exception ignored) { + } + } + break; + case "players": + whitelistPlayers.add(t); + break; + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_chat] 配置加载失败: " + + e.getMessage()); + } + + // 从消息.txt读取chat_开头的消息 + loadMessages(); + } + + private void loadMessages() { + File msgFile = new File( + plugin.getDataFolder(), + "消息.txt"); + if (!msgFile.exists()) return; + try (BufferedReader mr = + new BufferedReader( + new InputStreamReader( + new FileInputStream( + msgFile), + StandardCharsets.UTF_8))) { + String mline; + while ((mline = mr.readLine()) != null) { + mline = mline.trim(); + if (mline.isEmpty() + || mline.startsWith("#")) + continue; + // 支持 = 和 : 两种分隔符 + int eqIdx = mline.indexOf('='); + int coIdx = mline.indexOf(':'); + int splitIdx = -1; + if (eqIdx >= 0 && coIdx >= 0) { + splitIdx = Math.min(eqIdx, coIdx); + } else if (eqIdx >= 0) { + splitIdx = eqIdx; + } else if (coIdx >= 0) { + splitIdx = coIdx; + } + if (splitIdx < 0) continue; + String k = mline.substring(0, + splitIdx).trim(); + String v = mline.substring( + splitIdx + 1).trim(); + if (k.startsWith("chat_")) { + messages.put(k, v); + } + } + } catch (Exception ignored) { + } + } + + + private void writeDefaultFile(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("启用过滤: " + enabled); + pw.println("通知管理员: true"); + pw.println("全服通报: false"); + pw.println("禁言时长: 300"); + pw.println(); + pw.println("白名单:"); + pw.println("*.minecraft.net"); + pw.println("baidu.com"); + pw.println("github.com"); + pw.println(); + pw.println("处罚规则:"); + pw.println("1:warn:0"); + pw.println("2:warn:0"); + pw.println("3:mute:300"); + pw.println("5:mute:600"); + pw.println("8:kick:0"); + pw.println("10:ban:0"); + pw.println(); + pw.println("白名单玩家:"); + } catch (IOException ignored) { + } + } + + public void saveConfig() { + File f = new File( + plugin.getDataFolder(), "chat.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: " + notifyAdmin); + pw.println("全服通报: " + notifyAll); + pw.println("禁言时长: " + muteDuration); + pw.println(); + pw.println("白名单:"); + for (String u : whitelistUrls) + pw.println(u); + pw.println(); + pw.println("处罚规则:"); + for (Map.Entry entry + : punishmentRules.entrySet()) { + int dur = punishmentDurations + .getOrDefault(entry.getKey(), + muteDuration); + pw.println(entry.getKey() + ":" + + entry.getValue() + + ":" + dur); + } + pw.println(); + pw.println("白名单玩家:"); + for (String p : whitelistPlayers) + pw.println(p); + } catch (IOException e) { + plugin.getLogger().severe( + "[Sdf1_chat] 保存失败: " + + e.getMessage()); + } + } + + // ========== 消息 ========== + + public String msg(String key, + String... args) { + String template = messages.getOrDefault( + key, key); + for (int i = 0; i < args.length - 1; + i += 2) { + template = template.replace( + "{" + args[i] + "}", + args[i + 1]); + } + return template; + } + + // ========== URL检测 ========== + + private String normalizeMessage(String msg) { + msg = COLOR_CODES.matcher(msg) + .replaceAll(""); + msg = DOT_VARIANTS.matcher(msg) + .replaceAll("."); + return msg; + } + + public List extractUrls( + String message) { + String normalized = + normalizeMessage(message); + List urls = new ArrayList<>(); + Matcher m = URL_PATTERN + .matcher(normalized); + while (m.find()) { + String url = m.group().toLowerCase(); + url = url.replaceAll( + "[\\s.,;!?,。;!?、)》」』】" + + "\\]\\[\\((]+$", + ""); + if (!url.isEmpty()) urls.add(url); + } + return urls; + } + + public boolean isWhitelisted(String url) { + String clean = url + .replaceAll("^https?://", "") + .toLowerCase(); + int colon = clean.indexOf(':'); + int slash = clean.indexOf('/'); + if (colon > 0 && (slash < 0 + || colon < slash)) { + clean = clean.substring(0, colon); + } + slash = clean.indexOf('/'); + if (slash > 0) { + clean = clean.substring(0, slash); + } + for (String entry : whitelistUrls) { + String e = entry.toLowerCase().trim(); + if (e.startsWith("*.")) { + String suffix = e.substring(1); + if (clean.endsWith(suffix) + || clean.equals( + suffix.substring(1))) + return true; + } else { + if (clean.equals(e) + || clean.endsWith( + "." + e)) + return true; + } + } + return false; + } + + public boolean isLikelyDomain(String url) { + int lastDot = url.lastIndexOf('.'); + if (lastDot < 0 + || lastDot >= url.length() - 1) + return false; + String ext = url.substring(lastDot + 1) + .toLowerCase(); + return !FILE_EXT.contains(ext); + } + + // ========== 玩家操作 ========== + + public boolean isMuted(String name) { + Long expiry = mutedPlayers.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() + >= expiry) { + mutedPlayers.remove(name); + return false; + } + return true; + } + + public String fmtDuration(int seconds) { + if (seconds <= 0) return "0秒"; + int day = seconds / 86400; + int hr = (seconds % 86400) / 3600; + int min = (seconds % 3600) / 60; + int sec = seconds % 60; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hr > 0) + sb.append(hr).append("小时"); + if (min > 0) + sb.append(min).append("分"); + if (sec > 0 || sb.length() == 0) + sb.append(sec).append("秒"); + return sb.toString(); + } + + public void applyPunishment(Player player, + int violation) { + String type = "warn"; + int duration = muteDuration; + int maxRule = 0; + for (Map.Entry entry + : punishmentRules.entrySet()) { + if (violation >= entry.getKey() + && entry.getKey() > maxRule) { + maxRule = entry.getKey(); + type = entry.getValue(); + duration = punishmentDurations + .getOrDefault( + entry.getKey(), + muteDuration); + } + } + if (type.equals("mute")) { + if (isMuted(player.getName())) + return; + mutedPlayers.put(player.getName(), + System.currentTimeMillis() + + (long) duration + * 1000L); + } + final String fType = type; + final int fDur = duration; + final int fV = violation; + final String fName = player.getName(); + Bukkit.getScheduler().runTask(plugin, + () -> { + Player p = Bukkit.getPlayer(fName); + if (p == null) return; + switch (fType) { + case "warn": + // 警告不处罚,只提示 + break; + case "mute": + p.sendMessage( + msg("chat_muted")); + break; + case "kick": + p.kickPlayer( + msg("chat_muted")); + break; + case "ban": + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban " + fName + + " §c多次发送违规链接"); + break; + case "banip": + InetSocketAddress addr = + p.getAddress(); + if (addr != null) { + String ip = addr + .getAddress() + .getHostAddress(); + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban-ip " + ip + + " §c发送违规链接"); + } + break; + } + }); + } + + public void unmutePlayer(String name) { + mutedPlayers.remove(name); + } + + public void resetPlayer(String name) { + mutedPlayers.remove(name); + violationCount.remove(name); + } + + public boolean isPlayerWhitelisted( + String name) { + return whitelistPlayers.contains(name); + } + + public boolean isNotifyAdmin() { + return notifyAdmin; + } + + public boolean isNotifyAll() { + return notifyAll; + } + + public int getViolationCount(String name) { + return violationCount + .getOrDefault(name, 0); + } + + public void incrementViolation( + String name) { + violationCount.put(name, + getViolationCount(name) + 1); + } + + public List getWhitelistUrls() { + return whitelistUrls; + } + + public Set getWhitelistPlayers() { + return whitelistPlayers; + } + + public Map + getPunishmentRules() { + return punishmentRules; + } + + public Map + getPunishmentDurations() { + return punishmentDurations; + } + + public Map + getMutedPlayers() { + return mutedPlayers; + } + + public void setNotifyAdmin(boolean v) { + notifyAdmin = v; + } + + public void setNotifyAll(boolean v) { + notifyAll = v; + } + + public void setMuteDuration(int v) { + muteDuration = v; + } + + public void addUrl(String url) { + whitelistUrls.add( + url.toLowerCase().trim()); + saveConfig(); + } + + public boolean removeUrl(String url) { + boolean r = whitelistUrls.remove( + url.toLowerCase().trim()); + saveConfig(); + return r; + } + + public void addWhitelistPlayer( + String name) { + whitelistPlayers.add(name); + saveConfig(); + } + + public boolean removeWhitelistPlayer( + String name) { + boolean r = + whitelistPlayers.remove(name); + saveConfig(); + return r; + } + + public void cleanExpired() { + long now = System.currentTimeMillis(); + mutedPlayers.entrySet().removeIf( + e -> e.getValue() <= now); + } + + private boolean parseBool(String v) { + String s = v.toLowerCase().trim(); + return s.equals("true") + || s.equals("on") + || s.equals("yes") + || s.equals("1") + || s.contains("开启") + || s.contains("启用") + || s.contains("是"); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ChatInputManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/ChatInputManager.java new file mode 100644 index 0000000..49f53fd --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ChatInputManager.java @@ -0,0 +1,842 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class ChatInputManager { + + public enum InputType { + NONE, + CHANGE_PWD_STEP1, + CHANGE_PWD_STEP2, + SET_EMAIL, + SET_EMAIL_VERIFY, + ADMIN_AUTH, + ADMIN_SET_POINTS, + ADMIN_SET_PWD, + ADMIN_DELETE_CONFIRM, + ADMIN_SET_AFK_TIME, + SMTP_CONFIG, + SMTP_VERIFY, + BACK_CHECKIN, + INVITE_INPUT_CODE, + TICKET_TITLE, + TICKET_DESC, + TICKET_SCORE, + TICKET_REJECT, + TICKET_ASSIGN, + TICKET_INVITE_PROVIDER, + TICKET_REMOVE_PROVIDER, + TICKET_COMPLETE, + TICKET_REPORT_ISSUE, + TICKET_REPLY, + TICKET_ADMIN_TYPE, + TICKET_ADMIN_TITLE, + TICKET_ADMIN_DESC, + TICKET_ADMIN_PROVIDER, + BACK_CHECK_ECONOMY_CONFIRM, + USER_MGMT_SEARCH, + + } + + public static class PlayerState { + public InputType type = InputType.NONE; + public String targetPlayer = ""; + public String ticketType = ""; + public String ticketTitle = ""; + public String ticketDesc = ""; + public int ticketId = 0; + public int smtpStep = 0; + public int scoreValue = 0; + + } + + private final Map states = + new HashMap<>(); + private Main plugin; + + public PlayerState getState(Player p) { + return states.computeIfAbsent( + p.getName(), + k -> new PlayerState()); + } + + public void reset(Player p) { + states.remove(p.getName()); + } + public boolean isInFlow(Player p) { + PlayerState state = states.get(p.getName()); + return state != null + && state.type != InputType.NONE; + } + + public void handleInput(Player p, String msg, + Main mainPlugin) { + if (this.plugin == null) + this.plugin = mainPlugin; + PlayerState state = getState(p); + ConfigManager config = + mainPlugin.getConfig2(); + DatabaseManager db = mainPlugin.getDb(); + + switch (state.type) { + + case CHANGE_PWD_STEP1: { + String name = p.getName(); + String salt = (String) db.getField( + name, "password_salt"); + String hash = + PasswordUtils.hash(msg, salt); + boolean mainOk = + db.checkPassword(name, hash); + if (mainOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "main"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + boolean tempOk = + db.checkPasswordOrTemp( + name, hash); + if (tempOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "temp"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + p.sendMessage(config.msg( + "password_wrong")); + return; + } + + case CHANGE_PWD_STEP2: { + String name = p.getName(); + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String oldHash = + (String) db.getField(name, + "password_hash"); + String oldSalt = + (String) db.getField(name, + "password_salt"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + msg, newSalt); + if (newHash.equals(oldHash)) { + p.sendMessage(config.msg( + "password_same")); + reset(p); + return; + } + mainPlugin.recordPasswordChange( + name, oldHash, oldSalt); + db.setField(name, + "password_hash", newHash); + db.setField(name, + "password_salt", newSalt); + mainPlugin + .getNeedsPasswordChange() + .remove(name); + p.sendMessage(config.msg( + "password_changed")); + reset(p); + return; + } + + case SET_EMAIL: { + if (!msg.contains("@") + || !msg.contains(".")) { + p.sendMessage(config.msg( + "email_format_error")); + reset(p); + return; + } + String oldEmail = (String) + db.getField(p.getName(), + "email"); + String emailAddr = msg.trim(); + // 已绑定邮箱:验证登录密码 + if (oldEmail != null + && !oldEmail.isEmpty()) { + state.targetPlayer = emailAddr; + state.type = + InputType.SET_EMAIL_VERIFY; + state.ticketTitle = + "need_pwd"; + p.sendMessage( + "§e您已绑定邮箱: " + + oldEmail); + p.sendMessage( + "§e请验证登录密码以确认修改:"); + return; + } + // 未绑定邮箱:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + + 100000)); + state.targetPlayer = emailAddr; + state.ticketTitle = code; + state.type = + InputType.SET_EMAIL_VERIFY; + final String to = emailAddr; + final String c = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + boolean sent = mainPlugin + .getEmail() + .sendVerifyCode( + to, c); + Bukkit.getScheduler() + .runTask(mainPlugin, + () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + reset(fp); + } + }); + }); + return; + } + + + case SET_EMAIL_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§c邮箱设置已取消"); + reset(p); + return; + } + String expected = + state.ticketTitle; + if (msg.trim().equals(expected)) { + db.setField(p.getName(), + "email", + state.targetPlayer); + p.sendMessage( + "§a邮箱已设置: " + + state.targetPlayer); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case ADMIN_AUTH: { + if (msg.equals( + config.adminPassword)) { + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openAdmin(p)); + } else { + p.sendMessage( + "§c管理密码错误"); + } + reset(p); + return; + } + + case ADMIN_SET_POINTS: { + try { + int pts = Integer.parseInt( + msg.trim()); + db.setField( + state.targetPlayer, + "points", pts); + p.sendMessage("§a已将 " + + state.targetPlayer + + " 积分设为 " + pts); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + String tgt = + state.targetPlayer; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openUserDetail( + p, tgt)); + reset(p); + return; + } + + case ADMIN_SET_PWD: { + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String tgt = state.targetPlayer; + String salt = + PasswordUtils + .generateSalt(); + String hash = + PasswordUtils.hash( + msg, salt); + db.setField(tgt, + "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + db.setField(tgt, + "temp_pw_used", 0); + p.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码," + + "请使用临时密码登录"); + // 邮件通知 + String emailAddr = (String) + db.getField(tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fBody = body; + final String fName = tgt; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> + mainPlugin.getEmail() + .sendTempPassword( + to, + fName, + fBody)); + } + reset(p); + return; + } + + case ADMIN_DELETE_CONFIRM: { + if (msg.equals( + state.targetPlayer)) { + db.deleteUser( + state.targetPlayer); + p.sendMessage("§a已删除 " + + state.targetPlayer); + Player tgt = + Bukkit.getPlayer( + state.targetPlayer); + if (tgt != null + && tgt.isOnline()) + tgt.kickPlayer( + "§c账号已被删除"); + } else { + p.sendMessage( + "§c输入不匹配,已取消"); + } + reset(p); + return; + } + + case ADMIN_SET_AFK_TIME: { + try { + int minutes = + Integer.parseInt( + msg.trim()); + config.afkTimeout = + minutes * 60; + config.saveSettings(); + p.sendMessage( + "§a挂机时长已设为 " + + minutes + + " 分钟"); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + reset(p); + return; + } + + case SMTP_CONFIG: { + String m = msg.trim(); + if ("000".equals(m) + || "取消取消取消" + .equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if ("取消".equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + int step = state.smtpStep; + if (step <= 1) { + if (!"0".equals(m)) + config.setSmtp( + "smtp地址", m); + state.smtpStep = 2; + p.sendMessage("§e当前端口: " + + config.getSmtp( + "smtp端口")); + p.sendMessage( + "§e输入端口(0跳过):"); + } else if (step == 2) { + if (!"0".equals(m)) + config.setSmtp( + "smtp端口", m); + state.smtpStep = 3; + p.sendMessage( + "§e输入发件邮箱:"); + } else if (step == 3) { + if (!"0".equals(m)) + config.setSmtp( + "smtp账号", m); + state.smtpStep = 4; + p.sendMessage( + "§e输入SMTP密码(0跳过):"); + } else if (step == 4) { + if (!"0".equals(m)) + config.setSmtp( + "smtp密码", m); + state.smtpStep = 5; + p.sendMessage( + "§e输入接收邮箱(留空发到发件邮箱):"); + } else if (step == 5) { + String recv = m; + if (recv.isEmpty() + || "0".equals(recv)) + recv = config.getSmtp( + "smtp账号"); + config.setSmtp( + "验证码接收邮箱", recv); + if (!config + .canSendSmtpVerify()) { + p.sendMessage( + "§c发送间隔不足1分钟"); + reset(p); + return; + } + String code = config + .generateSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String acct = config.getSmtp( + "smtp账号"); + if (acct.isEmpty() + || addr.isEmpty()) { + p.sendMessage( + "§c地址或账号为空"); + reset(p); + return; + } + config.recordSmtpSendTime(); + final String fRecv = recv; + final String fCode = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> fp.sendMessage( + "§e正在发送验证码...")); + boolean sent = + mainPlugin.getEmail() + .sendVerifyCode( + fRecv, + fCode); + final boolean fSent = sent; + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> { + if (fSent) { + state.type = + InputType + .SMTP_VERIFY; + state.smtpStep = 0; + fp.sendMessage( + "§a验证码已发送到 " + + fRecv); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败,查看控制台"); + reset(fp); + } + }); + }); + } + return; + } + + case SMTP_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if (config.checkSmtpVerifyCode( + msg.trim())) { + config.saveSmtp(); + config.clearSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String port = config.getSmtp( + "smtp端口"); + String acct = config.getSmtp( + "smtp账号"); + String recv = config.getSmtp( + "验证码接收邮箱"); + boolean ssl = config.smtpSsl; + p.sendMessage("§a§l=== SMTP配置完成 ==="); + p.sendMessage("§7地址: §f" + addr); + p.sendMessage("§7端口: §f" + port); + p.sendMessage("§7账号: §f" + acct); + p.sendMessage("§7接收: §f" + recv); + p.sendMessage("§7加密: §f" + ssl); + p.sendMessage("§a§l===================="); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case BACK_CHECKIN: { + String result = + mainPlugin.getCheckIn() + .backCheckIn(p, msg); + if (result == null) { + // 等待选择(NEED_CONFIRM) + return; + } + if (result.startsWith( + "NEED_CONFIRM:")) { + String[] parts = + result.split(":"); + // 存储到Main的Map + mainPlugin.pendingBackCheck.put( + p.getName(), + parts[1] + ":" + parts[2]); + return; + } + if (result.startsWith("§c")) { + p.sendMessage(result); + } else { + p.sendMessage(result); + } + reset(p); + return; + } + + + case INVITE_INPUT_CODE: { + String code = msg.trim(); + String myName = p.getName(); + String myCode = + (String) db.getField( + myName, + "invite_code"); + if (code.isEmpty()) { + p.sendMessage( + "§c邀请码不能为空"); + reset(p); + return; + } + if (code.equals(myCode)) { + p.sendMessage( + "§c不能用自己邀请码"); + reset(p); + return; + } + boolean found = false; + for (Map u : + db.getAllUsers()) { + String uc = (String) + u.getOrDefault( + "invite_code", ""); + if (code.equals(uc)) { + String inv = (String) + u.get("player_name"); + db.setField(myName, + "invited_by", inv); + db.addPoints(inv, 50); + p.sendMessage( + "§a邀请成功!邀请人: " + + inv); + Player ip = + Bukkit.getPlayer(inv); + if (ip != null + && ip.isOnline()) + ip.sendMessage("§a" + + myName + + " 使用了您的邀请码"); + found = true; + break; + } + } + if (!found) + p.sendMessage( + "§c邀请码不存在"); + reset(p); + return; + } + + case TICKET_TITLE: { + state.ticketTitle = msg; + state.type = + InputType.TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return; + } + + case TICKET_DESC: { + String tt = state.ticketType; + String tl = state.ticketTitle; + reset(p); + mainPlugin.getTicket() + .submitTicket( + p, tt, tl, msg); + return; + } + + case TICKET_SCORE: { + int tid = state.ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + reset(p); + mainPlugin.getTicket() + .confirmAndScore( + p, tid, sc); + } catch ( + NumberFormatException e) { + reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return; + } + + case TICKET_REJECT: { + int tid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .rejectTicket( + p, tid, msg); + return; + } + + case TICKET_ASSIGN: { + int tid = state.ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + reset(p); + mainPlugin.getTicket() + .assignToProvider( + p, tid, prov, + price); + final int fTid = tid; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getTicket() + .openDetail( + p, fTid)); + return; + } + + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return; + } + db.addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return; + } + + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.isServiceProvider(tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return; + } + db.removeServiceProvider(tgt); + p.sendMessage("§a已移除 " + tgt); + return; + } + + case TICKET_COMPLETE: { + try { + int tid = Integer.parseInt( + msg.trim()); + mainPlugin.getTicket() + .completeAsProvider( + p, tid); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效工单号"); + } + reset(p); + return; + } + + case TICKET_REPORT_ISSUE: { + String report = msg.trim(); + reset(p); + p.sendMessage("§e异常已上报"); + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§c§l[工单异常] §f" + + p.getName() + + ": " + report); + } + } + return; + } + + case TICKET_REPLY: { + int replyTid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .replyTicket( + p, replyTid, msg); + return; + } + + case TICKET_ADMIN_TYPE: { + state.ticketType = msg.trim(); + state.type = + InputType + .TICKET_ADMIN_TITLE; + p.sendMessage( + "§e请输入工单标题:"); + return; + } + + case TICKET_ADMIN_TITLE: { + state.ticketTitle = msg; + state.type = + InputType + .TICKET_ADMIN_DESC; + p.sendMessage( + "§e请输入工单描述:"); + return; + } + + case TICKET_ADMIN_DESC: { + state.targetPlayer = msg; + state.type = + InputType + .TICKET_ADMIN_PROVIDER; + p.sendMessage( + "§e输入服务商名和价格:"); + p.sendMessage( + "§7例: playerA 20"); + return; + } + + case TICKET_ADMIN_PROVIDER: { + String[] parts2 = msg.trim() + .split("\\s+"); + String prov2 = parts2[0]; + int price2 = 0; + if (parts2.length >= 2) { + try { + price2 = Integer.parseInt( + parts2[1]); + } catch ( + NumberFormatException e) { + } + } + String t2 = state.ticketType; + String ti2 = state.ticketTitle; + String d2 = state.targetPlayer; + reset(p); + mainPlugin.getTicket() + .adminCreateAndAssign( + p, t2, ti2, + d2, prov2, price2); + return; + } + + default: + reset(p); + return; + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/CheckInManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/CheckInManager.java new file mode 100644 index 0000000..b594b0f --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/CheckInManager.java @@ -0,0 +1,302 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class CheckInManager { + + private final Main plugin; + + public CheckInManager(Main plugin) { + this.plugin = plugin; + } + + // ===== 今日是否已签到 ===== + public boolean isCheckedInToday( + String name) { + String last = (String) plugin.getDb() + .getField(name, + "last_checkin_date"); + String today = new SimpleDateFormat( + "yyyy-MM-dd") + .format(new Date()); + return today.equals(last); + } + + // ===== 签到 ===== + // ===== 签到 ===== + public String checkIn(Player p) { + String name = p.getName(); + if (isCheckedInToday(name)) { + return plugin.getConfig2() + .msg("checkin_already"); + } + + Object streakObj = plugin.getDb() + .getField(name, "checkin_streak"); + int streak = streakObj != null + ? ((Number) streakObj).intValue() + : 0; + + String lastDate = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + + if (lastDate != null && !lastDate.isEmpty()) { + try { + java.util.Date lastD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(lastDate); + java.util.Date todayD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(today); + long diff = todayD.getTime() + - lastD.getTime(); + if (diff > 86400000L * 2) { + streak = 0; + } else if (diff > 86400000L) { + streak++; + } + } catch (Exception ignored) { + } + } + + if (streak == 0) streak = 1; + else streak++; + + double multi = 1.0; + if (streak >= 21) multi = 1.5; + else if (streak >= 14) multi = 1.3; + else if (streak >= 7) multi = 1.1; + + int basePoints = + plugin.getConfig2().checkinPoints; + int points = (int) (basePoints * multi); + + plugin.getDb().addPoints(name, points); + plugin.getDb().setField(name, + "checkin_streak", streak); + plugin.getDb().setField(name, + "last_checkin_date", today); + + Object totalObj = plugin.getDb() + .getField(name, "total_checkin_days"); + int total = totalObj != null + ? ((Number) totalObj).intValue() + : 0; + plugin.getDb().setField(name, + "total_checkin_days", total + 1); + + // ★ 债券盲盒奖励(6参数版,写入流水)★ + int bondMin = + plugin.getConfig2().checkinBondMin; + int bondMax = + plugin.getConfig2().checkinBondMax; + if (bondMin <= 0) bondMin = 1; + if (bondMax < bondMin) bondMax = bondMin; + int bondReward = bondMin + + new java.util.Random() + .nextInt(bondMax - bondMin + 1); + plugin.getBonds().addBonds(name, bondReward, + "daily_sign", "", "签到系统", + "每日签到连续" + streak + "天"); + + return plugin.getConfig2() + .msg("checkin_success") + .replace("{points}", + String.valueOf(points)) + .replace("{streak}", + String.valueOf(streak)) + .replace("{multi}", + String.valueOf(multi)) + + " §e+" + bondReward + "债券"; + } + + // ===== 获取连续天数 ===== + public int getStreak(String name) { + Object val = plugin.getDb() + .getField(name, + "checkin_streak"); + if (val instanceof Number) { + return ((Number) val) + .intValue(); + } + return 0; + } + // 标准化日期 + private String normalizeDate(String input) { + String s = input.trim().replace("/", "-"); + if (s.matches("\\d{8}")) { + return s.substring(0, 4) + "-" + + s.substring(4, 6) + "-" + + s.substring(6, 8); + } + return s; + } + // ===== 补签入口 ===== + public String backCheckIn( + Player p, String dateStr) { + String name = p.getName(); + String normalized = + normalizeDate(dateStr); + + // 严格日期验证 + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd"); + sdf.setLenient(false); + java.util.Date target; + try { + target = sdf.parse(normalized); + } catch (Exception ex) { + return "§c无效日期: " + dateStr; + } + String reformat = sdf.format(target); + if (!reformat.equals(normalized)) { + return "§c无效日期: " + dateStr; + } + + String today = sdf.format(new Date()); + if (normalized.compareTo(today) >= 0) { + return "§c今天不需要补签"; + } + + String threeDaysAgo = getPastDate(3); + if (normalized.compareTo( + threeDaysAgo) < 0) { + return "§c只能补签3天内" + + "(最多到 " + threeDaysAgo + ")"; + } + + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + boolean alreadyBacked = false; + if (!existing.isEmpty()) { + for (String d : existing.split(",")) { + if (normalizeDate(d.trim()) + .equals(normalized)) { + alreadyBacked = true; + break; + } + } + } + if (alreadyBacked) { + return "§c该日期已补签: " + normalized; + } + + Object normalCheck = + plugin.getDb().getField( + name, "last_checkin_date"); + String lastCheckin = normalCheck != null + ? normalCheck.toString() : ""; + if (lastCheckin.equals(normalized)) { + return "§c该日期已签到: " + normalized; + } + + // ★ 补签消耗:3债券 + 10积分 + // 积分不够时:用 9债券(3倍)代替 ★ + int bondCost = 3; + int pointCost = 10; + int bondAltCost = 9; // 3倍债券替代 + + int myBonds = plugin.getBonds() + .getBonds(name); + Object ptsObj = plugin.getDb() + .getField(name, "points"); + int myPoints = ptsObj != null + ? ((Number) ptsObj).intValue() + : 0; + + + // 方案A:3债券 + 10积分 + boolean canA = myBonds >= bondCost + && myPoints >= pointCost; + // 方案B:9债券(积分不够时) + boolean canB = myBonds >= bondAltCost + && myPoints < pointCost; + + if (!canA && !canB) { + if (myPoints < pointCost + && myBonds < bondAltCost) { + return "§c补签费用不足" + + "(需3债券+10积分," + + "或9债券代替)" + + " 当前: " + + myBonds + "债券 " + + myPoints + "积分"; + } + if (myBonds < bondCost) { + return "§c债券不足(需" + + bondCost + "枚)"; + } + return "§c积分不足(需" + + pointCost + "分)"; + } + + // 执行扣款 + if (canA) { + // 方案A:3债券 + 10积分 + plugin.getBonds().deductBonds( + name, bondCost, + "admin_deduct", "", + "补签系统", "补签扣款"); + plugin.getDb().deductPoints( + name, pointCost); + } else { + // 方案B:9债券代替 + plugin.getBonds().deductBonds( + name, bondAltCost, + "admin_deduct", "", + "补签系统", "补签扣款(债券替代)"); + } + + doBackCheckIn(p, normalized); + + String costMsg = canA + ? bondCost + "债券+" + pointCost + "积分" + : bondAltCost + "债券(替代)"; + return "§a补签成功!日期: " + + normalized + + " §7消耗: " + costMsg; + } + + + + // ===== 执行补签 ===== + // ===== 执行补签 ===== + public void doBackCheckIn( + Player p, String dateStr) { + String name = p.getName(); + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + String updated = existing.isEmpty() + ? dateStr + : existing + "," + dateStr; + plugin.getDb().setField(name, + "backcheck_dates", updated); + int streak = getStreak(name); + plugin.getDb().setField(name, + "checkin_streak", streak + 1); + // ★ 补签不发放奖励,只记录 + } + + // ===== 获取N天前日期 ===== + private String getPastDate( + int daysAgo) { + Calendar cal = + Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, + -daysAgo); + return new SimpleDateFormat( + "yyyy-MM-dd") + .format(cal.getTime()); + } +} \ No newline at end of file diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/CommissionManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/CommissionManager.java new file mode 100644 index 0000000..cb24687 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/CommissionManager.java @@ -0,0 +1,227 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Logger; + +public class CommissionManager { + + private final Main main; + private final Logger logger; + private final List rules = + new ArrayList<>(); + + public CommissionManager(Main main) { + this.main = main; + this.logger = main.getLogger(); + loadRules(); + } + + public void loadRules() { + rules.clear(); + File f = new File( + main.getDataFolder(), + "提成规则.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + // 格式: 邀请人数:提成百分比 + // 如 "5:15" = 邀请5人提成15% + String[] parts = + line.split(":"); + if (parts.length >= 2) { + try { + int count = Integer.parseInt( + parts[0].trim()); + double rate = Double + .parseDouble( + parts[1] + .trim()) / 100.0; + rules.add(new double[]{ + count, rate}); + } catch (Exception ignored) { + } + } + } + // 按人数排序(从大到小) + rules.sort((a, b) -> + Double.compare(b[0], a[0])); + logger.info( + "[Commission] 加载 " + + rules.size() + " 条提成规则"); + } catch (Exception e) { + logger.warning( + "[Commission] 加载失败: " + + e.getMessage()); + } + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 邀请提成规则"); + pw.println("# 格式: 邀请人数:提成百分比"); + pw.println("# 邀请10人提成20%"); + pw.println("10:20"); + pw.println("# 邀请5人提成15%"); + pw.println("5:15"); + pw.println("# 邀请1人提成10%"); + pw.println("1:10"); + } catch (IOException ignored) { + } + } + + /** + * 获取邀请人的提成比例 + * 根据已邀请人数匹配最高规则 + */ + public double getCommissionRate( + String inviter) { + int inviteCount = getInviteCount( + inviter); + for (double[] rule : rules) { + if (inviteCount >= (int) rule[0]) { + return rule[1]; + } + } + return 0; + } + + /** + * 获取邀请人数 + */ + public int getInviteCount( + String inviter) { + try { + Object val = + main.getDb().getField( + inviter, + "monthly_invite_count"); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + Object total = + main.getDb().getField( + inviter, + "total_invite_count"); + if (total instanceof Number) { + return ((Number) total).intValue(); + } + } catch (Exception ignored) { + } + return 0; + } + + /** + * 发放提成 + * 被邀请人获得奖励时,邀请人获得提成 + */ + public void payCommission( + String invitee, + double originalAmount) { + // 查找邀请人 + String inviter = getInviter(invitee); + if (inviter == null + || inviter.isEmpty()) return; + + double rate = getCommissionRate( + inviter); + if (rate <= 0) return; + + double commission = + originalAmount * rate; + if (commission <= 0) return; + + // 通过 Vault 发放 + var rsp = main.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy.Economy.class); + if (rsp == null) return; + net.milkbowl.vault.economy.Economy econ = + rsp.getProvider(); + if (econ == null) return; + + econ.depositPlayer(inviter, commission); + + // 通知邀请人 + Player inviterP = + main.getServer() + .getPlayer(inviter); + if (inviterP != null + && inviterP.isOnline()) { + inviterP.sendMessage( + "§a§l[邀请提成] §f" + + invitee + " 完成任务," + + "你获得 §e" + + String.format("%.2f", + commission) + + " §f游戏币提成 (" + + (int)(rate * 100) + "%)"); + } + + logger.info( + "[Commission] 提成: " + + inviter + " ← " + + invitee + " | " + + String.format("%.2f", + originalAmount) + + " × " + + (int)(rate * 100) + "% = " + + String.format("%.2f", + commission)); + } + + /** + * 获取邀请人 + */ + private String getInviter(String player) { + try { + Object val = + main.getDb().getField( + player, + "invited_by"); + if (val != null + && !val.toString() + .isEmpty()) { + return val.toString(); + } + } catch (Exception ignored) { + } + return null; + } + + /** + * 判断金额是否包含经济内容 + */ + public boolean hasEconomyContent( + String text) { + String t = text.toLowerCase(); + return t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额"); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ConfigManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/ConfigManager.java new file mode 100644 index 0000000..a15ada0 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ConfigManager.java @@ -0,0 +1,372 @@ +package Sdf1_login; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class ConfigManager { + + private final File dataFolder; + private final Map messages + = new LinkedHashMap<>(); + private final Map smtpSettings + = new LinkedHashMap<>(); + + // ===== 公共字段 ===== + public String adminTag = "admin"; + public String adminPassword = "qweasd"; + public int maxAccountsPerIP = 3; + public String approvalMode = "manual"; + public int autoApproveDelayMinutes = 30; + public int loginTimeout = 60; + public boolean afkEnabled = false; + public int afkTimeout = 300; + public boolean smtpSsl = true; + public String smtpVerifyCode = ""; + public long smtpVerifyExpire = 0; + public long smtpLastSendTime = 0; + public String smtpConfiguringPlayer = ""; + public String broadcastServerUrl = ""; + public int httpPort = 8080; + public String garbageTag = "admin"; + + public double checkinRewardFixed = 0; + public double checkinRewardMin = 0; + public double checkinRewardMax = 0; + public boolean checkinGivePoints = true; + public int checkinPoints = 10; + public String checkinRewardType = "none"; + public int backCheckPointMultiplier = 5; + public int checkinBondMin = 1; + public int checkinBondMax = 3; + + public double providerPointsPerScore = 2.0; + public int baseEconomyReward = 100; + public double providerEconomyPerScore = 50.0; + public int requesterPointsReward = 10; + + public boolean garbageEnabled = true; + public int garbageInterval = 300; + public int garbageMaxRounds = 1; + + // ★ 构造函数接收 File,和 Main.java 调用一致 ★ + public ConfigManager(File dataFolder) { + this.dataFolder = dataFolder; + } + public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济 + + + // ==================== 消息 ==================== + + public void loadMessages() { + messages.clear(); + File file = new File(dataFolder, "消息.txt"); + if (!file.exists()) createDefaultMessages(file); + loadMap(file, messages); + } + + public String msg(String key) { + String val = messages.getOrDefault(key, key); + val = val.replace("", "&l"); + val = val.replace("", "&r"); + val = val.replace("
", "\n"); + val = val.replace("\\n", "\n"); + val = val.replaceAll( + "&([0-9a-fk-orA-FK-OR])", "§$1"); + return val; + } + + private void createDefaultMessages(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 消息配置 ====="); + L.add("login_timeout=§c登录超时"); + L.add("not_registered=§e您尚未注册"); + L.add("not_logged_in=§e您尚未登录"); + L.add("already_registered=§c您已注册过"); + L.add("already_logged_in=§c您已登录"); + L.add("reg_success=§a注册成功!"); + L.add("login_success=§a登录成功!"); + L.add("login_failed=§c密码错误"); + L.add("password_wrong=§c密码错误"); + L.add("password_changed=§a密码修改成功!"); + L.add("password_format_error=§c密码需6位以上含大小写和数字"); + L.add("password_same=§c新密码不能与旧密码相同"); + L.add("checkin_already=§c您今日已签到"); + L.add("checkin_success=§a签到成功!获得 {points} 积分"); + L.add("chat_muted=§c你已被禁言"); + L.add("未知参数=§c未知参数或权限不足"); + writeLines(f, L); + } + + // ==================== SMTP ==================== + + public void loadSmtp() { + smtpSettings.clear(); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); + loadMap(file, smtpSettings); + } + + public String getSmtp(String key) { + return smtpSettings.getOrDefault(key, ""); + } + + public void setSmtp(String key, String value) { + smtpSettings.put(key, value); + } + + public void saveSmtp() { + File file = new File(dataFolder, "SMTP设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + for (Map.Entry e + : smtpSettings.entrySet()) { + L.add(e.getKey() + "=" + e.getValue()); + } + writeLines(file, L); + } + + private void createDefaultSmtp(File f) { + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + L.add("smtp地址=smtp.example.com"); + L.add("smtp端口=465"); + L.add("smtp账号="); + L.add("smtp密码="); + L.add("发件人名称=Sdf1_login"); + L.add("smtp加密=true"); + writeLines(f, L); + } + + // ==================== 插件设置 ==================== + + public void loadSettings() { + File file = new File(dataFolder, "插件设置.txt"); + if (!file.exists()) createDefaultSettings(file); + Map m = new LinkedHashMap<>(); + loadMap(file, m); + + adminTag = m.getOrDefault("管理标签", adminTag); + adminPassword = m.getOrDefault("管理密码", adminPassword); + maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3); + approvalMode = m.getOrDefault("审批模式", approvalMode); + autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30); + loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60); + afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false")); + afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300); + smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true")); + httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080); + broadcastServerUrl = m.getOrDefault("resource-pack-url", ""); + + providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0); + baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100); + providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0); + requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10); + + garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true")); + garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300); + garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1); + + checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0); + checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0); + checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0); + checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true")); + checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10); + backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5); + checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1); + checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3); + if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin; + + if (checkinRewardFixed > 0) checkinRewardType = "fixed"; + else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range"; + else checkinRewardType = "none"; + rewardChannel = m.getOrDefault("奖励发放方式", "债券"); + rewardChannel = normalizeRewardChannel(rewardChannel); + +// 验证合法值 + if (!"economy".equalsIgnoreCase(rewardChannel) + && !"bonds".equalsIgnoreCase(rewardChannel)) { + rewardChannel = "bonds"; + } + + + + // ★ 自动补充新配置项 ★ + boolean needAppend = false; + List newKeys = new ArrayList<>(); + if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); } + if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); } + if (!m.containsKey("奖励发放方式")) { + needAppend = true; + newKeys.add("奖励发放方式=债券"); + } + if (needAppend) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + for (String key : newKeys) { bw.write(key); bw.newLine(); } + bw.flush(); bw.close(); + System.out.println("[Config] 已补充签到债券配置"); + + } catch (IOException ignored) {} + if (!m.containsKey("奖励发放方式")) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + bw.write("奖励发放方式=bonds"); + bw.newLine(); + bw.flush(); bw.close(); + } catch (IOException ignored) {} + } + + } + } + /** + * 奖励方式标准化:中英文全兼容 + * "债券" "bonds" "Bond" "BONDS" → "bonds" + * "经济" "economy" "Economy" "ECONOMY" → "economy" + * 其他任何值 → "bonds"(默认债券) + */ + private String normalizeRewardChannel(String raw) { + if (raw == null) return "bonds"; + String s = raw.trim().toLowerCase(); + // 中文 + if (s.contains("债券")) return "bonds"; + if (s.contains("经济")) return "economy"; + // 英文 + if (s.contains("bond")) return "bonds"; + if (s.contains("econ")) return "economy"; + if (s.contains("Vault")) return "economy"; + // 兜底 + return "bonds"; + } + + public void saveSettings() { + File file = new File(dataFolder, "插件设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=" + adminTag); + L.add("管理密码=" + adminPassword); + L.add("每IP最大账号数=" + maxAccountsPerIP); + L.add("审批模式=" + approvalMode); + L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); + L.add("登录超时秒数=" + loginTimeout); + L.add("挂机踢出=" + afkEnabled); + L.add("挂机超时秒数=" + afkTimeout); + L.add("resource-pack-url=" + broadcastServerUrl); + L.add("http-port=" + httpPort); + L.add("垃圾站_启用=" + garbageEnabled); + L.add("垃圾站_清理间隔秒=" + garbageInterval); + L.add("垃圾站_保留轮数=" + garbageMaxRounds); + L.add("签到固定金额=" + checkinRewardFixed); + L.add("签到最小金额=" + checkinRewardMin); + L.add("签到最大金额=" + checkinRewardMax); + L.add("签到给积分=" + checkinGivePoints); + L.add("签到积分数量=" + checkinPoints); + L.add("签到债券最小=" + checkinBondMin); + L.add("签到债券最大=" + checkinBondMax); + L.add("补签积分倍率=" + backCheckPointMultiplier); + L.add("奖励发放方式=债券"); + writeLines(file, L); + } + + private void createDefaultSettings(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=admin"); + L.add("管理密码=qweasd"); + L.add("每IP最大账号数=3"); + L.add("审批模式=manual"); + L.add("自动审批延迟分钟=30"); + L.add("登录超时秒数=60"); + L.add("挂机踢出=false"); + L.add("挂机超时秒数=300"); + L.add("resource-pack-url="); + L.add("http-port=8080"); + L.add("垃圾站_启用=true"); + L.add("垃圾站_清理间隔秒=300"); + L.add("垃圾站_保留轮数=1"); + L.add("签到固定金额=0"); + L.add("签到最小金额=50"); + L.add("签到最大金额=500"); + L.add("签到给积分=true"); + L.add("签到积分数量=10"); + L.add("签到债券最小=1"); + L.add("签到债券最大=3"); + L.add("补签积分倍率=5"); + L.add("奖励发放方式=债券"); + writeLines(f, L); + } + public List getAfkWhitelist() { + List list = new ArrayList<>(); + File file = new File(dataFolder, "挂机白名单.txt"); + if (!file.exists()) { + try { + file.createNewFile(); + FileWriter fw = new FileWriter(file); + fw.write("# 每行一个玩家名\n"); + fw.flush(); + fw.close(); + } catch (IOException ignored) {} + return list; + } + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) + list.add(line); + } + } catch (IOException ignored) {} + return list; + } + + // ==================== 其他 ==================== + + public String generateSmtpVerifyCode() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length()))); + smtpVerifyCode = sb.toString(); + smtpVerifyExpire = System.currentTimeMillis() + 300000L; + return smtpVerifyCode; + } + + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; } + return smtpVerifyCode.equalsIgnoreCase(code); + } + + public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; } + public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; } + public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); } + + // ==================== 文件工具 ==================== + + private void loadMap(File file, Map map) { + try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); + } + } catch (IOException ignored) {} + } + + private void writeLines(File file, List lines) { + try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + for (String l : lines) { w.write(l); w.newLine(); } + } catch (IOException ignored) {} + } + + private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } } + private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/CypayCommand.java b/backup/20260605_152529/src/main/java/Sdf1_login/CypayCommand.java new file mode 100644 index 0000000..7fd9067 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/CypayCommand.java @@ -0,0 +1,577 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +public class CypayCommand + implements CommandExecutor, TabCompleter { + + private final Main plugin; + private static final SimpleDateFormat SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + public CypayCommand(Main plugin) { + this.plugin = plugin; + } + + private boolean hasAdminPerm(CommandSender s) { + if (s.isOp()) return true; + if (!(s instanceof Player)) return true; + Player p = (Player) s; + String tag = + plugin.getConfig2().adminTag; + return !tag.isEmpty() + && p.getScoreboardTags().contains(tag); + } + + private int parseAmount(String s) { + return Integer.parseInt( + s.trim().replaceAll("[+]", "")); + } + + private List filter( + List list, String prefix) { + String low = prefix.toLowerCase(); + return list.stream() + .filter(x -> x.toLowerCase() + .startsWith(low)) + .collect(Collectors.toList()); + } + + @Override + public boolean onCommand(CommandSender sender, + Command cmd, + String label, + String[] args) { + + // ===== /cypay — 查自己 ===== + if (args.length == 0) { + String name = sender instanceof Player + ? sender.getName() : "Console"; + int bonds = + plugin.getBonds().getBonds(name); + String status = plugin.getBonds() + .getAccountStatus(name); + sender.sendMessage( + "§e§l[债券] §f" + name + + " 余额: §a" + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals(status) + ? "§c冻结中" + : "§a正常")); + return true; + } + + // ===== /cypay import ===== + /* if (args.length == 2 + && "import".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String file = args[1]; + if (!file.endsWith(".txt")) + file += ".txt"; + int result = plugin.getCDK() + .importFromFile(file); + if (result == -1) + sender.sendMessage("§c文件不存在: " + + file); + else + sender.sendMessage("§a导入完成: " + + result + " 条"); + return true; + }*/ + + // ===== /cypay freeze ===== + if (args.length == 2 + && "freeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (plugin.getBonds().isFrozen(target)) { + sender.sendMessage("§c" + target + + " 已处于冻结状态"); + return true; + } + plugin.getBonds().freezeAccount(target); + sender.sendMessage("§a已冻结 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§c§l[系统] 你的债券账户" + + "已被管理员冻结"); + return true; + } + + // ===== /cypay unfreeze ===== + if (args.length == 2 + && "unfreeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (!plugin.getBonds() + .isFrozen(target)) { + sender.sendMessage("§c" + target + + " 未被冻结"); + return true; + } + plugin.getBonds().unfreezeAccount(target); + sender.sendMessage("§a已解冻 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a§l[系统] 你的债券账户" + + "已被解冻"); + return true; + } + + // ===== /cypay water [days] ===== + if (args.length >= 2 + && "water".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + int days = 7; + if (args.length >= 3) { + try { + days = Integer.parseInt(args[2]); + } catch (NumberFormatException ignored) { + } + } + days = Math.min(days, + BondManager.ADMIN_MAX_WATER_DAYS); + + if (days <= 0) days = 1; + + List> txs = + plugin.getBonds() + .getTransactions( + target, days); + + sender.sendMessage( + "§e§l===== " + target + + " 流水记录 (近" + + days + "天) ====="); + sender.sendMessage("§f当前余额: §a" + + plugin.getBonds() + .getBonds(target) + + " §f状态: " + + (plugin.getBonds() + .isFrozen(target) + ? "§c冻结" : "§a正常")); + + if (txs.isEmpty()) { + sender.sendMessage("§7 (暂无记录)"); + } else { + for (Map tx : txs) { + sender.sendMessage( + formatTxLine(tx)); + } + } + sender.sendMessage( + "§e§l==========================" + + "=========="); + return true; + } + // ===== /cypay printer [player] ===== + if ("printer".equalsIgnoreCase(args[0])) { + if (!hasAdminPerm(sender) + && !(sender instanceof Player) + == false) { + // 控制台也可以用 + if (!(sender instanceof Player)) + ; // 放行 + else if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + } + if (args.length == 1) { + // 打印全服 + sender.sendMessage( + "§e正在导出全服流水..."); + plugin.getBonds(); // 确保实例就绪 + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printAll(sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + if (args.length == 2) { + // 打印指定玩家 + String target = args[1]; + sender.sendMessage("§e正在导出 " + + target + " 的流水..."); + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printPlayer(target, sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + } + + // ===== /cypay give/remove ===== + if (args.length == 3) { + String action = args[0].toLowerCase(); + if ("give".equals(action) + || "remove".equals(action)) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + try { + int amount = + parseAmount(args[2]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + String txType = + "give".equals(action) + ? BondManager + .TX_ADMIN_GIVE + : BondManager + .TX_ADMIN_DEDUCT; + String opName = + sender instanceof Player + ? sender.getName() + : "Console"; + + if ("give".equals(action)) { + plugin.getBonds().addBonds( + target, amount, + txType, "", + opName, "管理员发放"); + sender.sendMessage( + "§a已给 " + target + + " " + amount + + " 债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a管理员给了你 " + + amount + + " 债券"); + } else { + if (plugin.getBonds() + .deductBonds(target, + amount, + txType, "", + opName, + "管理员扣除")) { + sender.sendMessage( + "§a已扣除 " + target + + " " + amount + + " 债券"); + Player tp = plugin + .getServer() + .getPlayerExact( + target); + if (tp != null) + tp.sendMessage( + "§c管理员扣除了" + + "你的 " + + amount + + " 债券"); + } else { + sender.sendMessage( + "§c" + target + + " 债券不足" + + "或账户冻结"); + } + } + } catch (NumberFormatException e) { + sender.sendMessage( + "§c金额格式错误"); + } + return true; + } + } + + // ===== /cypay — 仅玩家转账 ===== + if (args.length == 2) { + try { + int amount = parseAmount(args[1]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + // ★ 控制台禁止转账 + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c控制台不能转账,请使用 " + + "/cypay give <玩家> <金额>"); + return true; + } + Player p = (Player) sender; + String target = args[0]; + BondManager.TransferResult r = + plugin.getBonds().transfer( + p.getName(), + target, amount); + if (r.success) { + p.sendMessage("§e§l[债券转账] §f向 §a" + + target + " §f转出 §c" + + amount + " §f枚债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage("§e§l[债券转账] §f收到 §a" + + p.getName() + + " §f转入 §a" + + amount + " §f枚债券"); + } else { + p.sendMessage("§c" + r.message + + ",当前: " + + plugin.getBonds() + .getBonds(p.getName())); + } + return true; + } catch (NumberFormatException ignored) { + } + } + + // ===== /cypay — 查指定玩家 ===== + if (args.length == 1) { + String target = args[0]; + // ★ 验证玩家在 login.db 中存在 ★ + if (!plugin.getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + + // String target = args[0]; + if (hasAdminPerm(sender)) { + int bonds = plugin.getBonds() + .getBonds(target); + String status = plugin.getBonds() + .getAccountStatus(target); + sender.sendMessage( + "§e§l[债券] §f" + target + + " 余额: §a" + + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals( + status) + ? "§c冻结中" + : "§a正常")); + } else { + int bonds = plugin.getBonds() + .getBonds( + sender.getName()); + sender.sendMessage( + "§e§l[债券] §f你的余额: §a" + + bonds); + } + return true; + } + + showHelp(sender); + return true; + } + + // ===== 流水行格式化 ===== + + private String formatTxLine( + Map tx) { + String type = (String) tx.get("type"); + int amount = ((Number) tx.get("amount")) + .intValue(); + String tgt = (String) tx.get("target_player"); + String reason = (String) tx.get("reason"); + int bal = ((Number) tx.get("balance_after")) + .intValue(); + long time = ((Number) tx.get("time")) + .longValue(); + + boolean pos = amount > 0; + String sign = pos ? "+" : ""; + String color = pos ? "§a" : "§c"; + + StringBuilder sb = new StringBuilder(); + sb.append("§7[") + .append(SDF.format(new Date(time))) + .append("] ") + .append(formatTxType(type)) + .append(" ") + .append(color).append(sign) + .append(amount).append("§7"); + if (tgt != null && !tgt.isEmpty()) + sb.append(" §e↔ ").append(tgt); + if (reason != null && !reason.isEmpty()) + sb.append(" §7(").append(reason) + .append(")"); + sb.append(" §7余额:§f").append(bal); + return sb.toString(); + } + + private String formatTxType(String type) { + if (type == null) return "§7未知"; + switch (type) { + case BondManager.TX_ADMIN_GIVE: + return "§a管理员发放"; + case BondManager.TX_ADMIN_DEDUCT: + return "§c管理员扣除"; + case BondManager.TX_TRANSFER_OUT: + return "§c转出"; + case BondManager.TX_TRANSFER_IN: + return "§a转入"; + case BondManager.TX_REDEEM: + return "§aCDK兑换"; + case BondManager.TX_SHOP_BUY: + return "§e商城购买"; + case BondManager.TX_DAILY_SIGN: + return "§a签到奖励"; + case BondManager.TX_FREEZE: + return "§c冻结"; + case BondManager.TX_UNFREEZE: + return "§a解冻"; + default: + return "§7" + type; + } + } + + private void showHelp(CommandSender sender) { + sender.sendMessage("§e§l债券命令帮助:"); + sender.sendMessage( + "§e/cypay §7- 查询自己余额"); + sender.sendMessage( + "§e/cypay <玩家> §7- 查余额(管理员)"); + sender.sendMessage( + "§e/cypay <玩家> <金额> §7- 转账"); + if (hasAdminPerm(sender)) { + sender.sendMessage( + "§e/cypay give <玩家> <金额> §7- 给债券"); + sender.sendMessage( + "§e/cypay remove <玩家> <金额> §7- 扣债券"); + sender.sendMessage( + "§e/cypay import <文件> §7- 导入CDK"); + sender.sendMessage( + "§e/cypay freeze <玩家> §7- 冻结"); + sender.sendMessage( + "§e/cypay unfreeze <玩家> §7- 解冻"); + sender.sendMessage( + "§e/cypay water <玩家> [天数] §7- 流水"); + sender.sendMessage( + "§e/cypay printer §7- 导出全服流水xlsx"); + sender.sendMessage( + "§e/cypay printer <玩家> §7- 导出指定玩家xlsx"); + + } + } + + @Override + public List onTabComplete( + CommandSender sender, Command command, + String label, String[] args) { + List list = new ArrayList<>(); + + if (args.length == 1) { + String last = args[0].toLowerCase(); + if (hasAdminPerm(sender)) { + list.add("give"); + list.add("remove"); + list.add("import"); + list.add("freeze"); + list.add("unfreeze"); + list.add("water"); + list.add("printer"); + + File cdkDir = new File( + plugin.getDataFolder(), + "cdk"); + if (cdkDir.exists()) { + File[] files = cdkDir.listFiles( + (d, n) -> n + .endsWith(".txt")); + if (files != null) + for (File f : files) + list.add(f.getName()); + } + } + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, last); + } + + if (args.length == 2) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first) + || "freeze".equals(first) + || "unfreeze".equals(first) + || "water".equals(first)) { + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, + args[1].toLowerCase()); + } + list.add("10"); + list.add("50"); + list.add("100"); + return filter(list, + args[1].toLowerCase()); + } + + if (args.length == 3) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first)) { + list.add("10"); + list.add("50"); + list.add("100"); + list.add("999"); + return filter(list, + args[2].toLowerCase()); + } + if ("water".equals(first)) { + list.add("1"); + list.add("3"); + list.add("7"); + list.add("14"); + return filter(list, + args[2].toLowerCase()); + } + } + + return list; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/DatabaseManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/DatabaseManager.java new file mode 100644 index 0000000..cb2b8f7 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/DatabaseManager.java @@ -0,0 +1,1758 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + + ")"); + + + // 安全添加 users 缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 inventory_data 列判断) + boolean hasOldTable = false; + try { + ResultSet rs = st.executeQuery( + "SELECT inventory_data " + + "FROM inventory_backups " + + "LIMIT 1"); + hasOldTable = true; + rs.close(); + } catch (SQLException e) { + hasOldTable = false; + } + + if (hasOldTable) { + // 旧表存在,迁移数据到新表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups_new (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 迁移旧数据 + try { + st.execute("INSERT INTO " + + "inventory_backups_new " + + "(player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience) " + + "SELECT player_name, " + + "inventory_data, " + + "armor_data, " + + "extra_data, " + + "player_level, " + + "player_exp " + + "FROM inventory_backups"); + } catch (SQLException ignored) { + } + st.execute("DROP TABLE " + + "inventory_backups"); + st.execute("ALTER TABLE " + + "inventory_backups_new " + + "RENAME TO " + + "inventory_backups"); + } else { + // 无旧表或已迁移,直接创建 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + } + + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + // PVP区域表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER DEFAULT 0" + + ")"); + +// PVP击杀总榜表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT NOT NULL," + + "region_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, region_name)" + + ")"); + + +// 菜单图标表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "menu_icons (" + + "player_name TEXT PRIMARY KEY," + + "icon_b64 TEXT DEFAULT ''," + + "item_name TEXT DEFAULT ''," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 垃圾回收站物品表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "item_data TEXT DEFAULT ''," + + "amount INTEGER DEFAULT 1," + + "create_time INTEGER DEFAULT 0" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +// ==================== 菜单图标 ==================== + + public void saveMenuIcon(String name, + String iconB64, String itemName) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO menu_icons " + + "(player_name, icon_b64, " + + "item_name, save_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, iconB64); + ps.setString(3, itemName); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT icon_b64 FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String b64 = null; + if (rs.next()) { + b64 = rs.getString("icon_b64"); + } + rs.close(); + ps.close(); + return b64; + } catch (SQLException e) { + return null; + } + } + + public void deleteMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM menu_icons " + + "WHERE player_name=? " + + "AND icon_b64 IS NOT NULL " + + "AND icon_b64 != '' " + + "LIMIT 1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public void createUser(String name, String hash, + String salt) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + public boolean checkPasswordOrTemp( + String name, String hash) { + // 先检查主密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean checkPassword(String name, + String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +// ==================== PVP区域 ==================== + + public void savePVPRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions " + + "(name, world, " + + "x1, y1, z1, " + + "x2, y2, z2, " + + "create_time) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map + getPVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getAllPVPRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + db.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deletePVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + +// ==================== PVP击杀 ==================== + + public int getPVPKills( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int kills = 0; + if (rs.next()) + kills = rs.getInt("kills"); + rs.close(); + ps.close(); + return kills; + } catch (SQLException e) { + return 0; + } + } + + public int getPVPDeaths( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int deaths = 0; + if (rs.next()) + deaths = rs.getInt("deaths"); + rs.close(); + ps.close(); + return deaths; + } catch (SQLException e) { + return 0; + } + } + + public void addPVPKill( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,1,0) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "kills=kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void addPVPDeath( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,0,1) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "deaths=deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getPVPKillTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT player_name, " + + "kills, deaths " + + "FROM pvp_kills " + + "WHERE " + + "region_name=? " + + "AND kills>0 " + + "ORDER BY kills " + + "DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Connection getDb() { + return db; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/EmailManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/EmailManager.java new file mode 100644 index 0000000..b6906e5 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/EmailManager.java @@ -0,0 +1,284 @@ +package Sdf1_login; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Base64; +import java.util.logging.Logger; + +public class EmailManager { + + private final ConfigManager config; + private final Logger logger; + + public EmailManager(ConfigManager config) { + this.config = config; + this.logger = Logger.getLogger("Sdf1_login"); + } + + public boolean isConfigured() { + String host = config.getSmtp("smtp地址"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + return host != null && !host.isEmpty() + && user != null && !user.isEmpty() + && pass != null && !pass.isEmpty(); + } + + public boolean sendVerifyCode(String to, + String code) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置," + + "无法发送验证码"); + return false; + } + logger.info("[Sdf1_login] 发送验证码到 " + to); + return sendEmailSync(to, + "[Sdf1_login] 验证码", + "您的验证码: " + code + + "\n5分钟内有效。\n" + + "如非本人操作请忽略。"); + } + + public boolean sendTempPassword(String to, + String player, String tempPwd) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, + "[Sdf1_login] 临时密码", + "玩家 " + player + + " 的临时密码: " + tempPwd + + "\n请尽快登录并修改密码!"); + } + + public boolean sendBody(String to, String subject, String body) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, subject, body); + } + + + private boolean sendEmailSync(String to, + String subject, String body) { + final boolean[] result = {false}; + Thread t = new Thread(() -> { + try { + result[0] = sendEmail( + to, subject, body); + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件线程异常: " + + e.getMessage()); + } + }, "Sdf1_login-SMTP"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch (InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + logger.severe( + "[Sdf1_login] SMTP超时(15秒)"); + return false; + } + return result[0]; + } + + private boolean sendEmail(String to, + String subject, String body) { + String host = config.getSmtp("smtp地址"); + String portStr = config.getSmtp("smtp端口"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + String fromName = config.getSmtp( + "发件人名称"); + + int port; + try { + port = Integer.parseInt(portStr); + } catch (Exception e) { + port = 465; + } + + try { + String encSubject = "=?UTF-8?B?" + + Base64.getEncoder() + .encodeToString( + subject.getBytes("UTF-8")) + + "?="; + String encBody = Base64.getEncoder() + .encodeToString( + body.getBytes("UTF-8")); + + if (port == 465) { + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket socket = (SSLSocket) + factory.createSocket( + host, port); + socket.setSoTimeout(10000); + doSmtp(socket, host, user, pass, + to, encSubject, encBody, + fromName); + socket.close(); + } else { + Socket plainSocket = + new Socket(host, port); + plainSocket.setSoTimeout(10000); + BufferedReader preReader = + new BufferedReader( + new InputStreamReader( + plainSocket + .getInputStream(), + "UTF-8")); + PrintWriter preWriter = + new PrintWriter( + plainSocket + .getOutputStream(), + true); + checkResponse(preReader, "220"); + sendLine(preWriter, + "EHLO " + host); + checkResponse(preReader, "250"); + sendLine(preWriter, "STARTTLS"); + checkResponse(preReader, "220"); + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket sslSocket = (SSLSocket) + factory.createSocket( + plainSocket, host, + port, true); + sslSocket.startHandshake(); + doSmtp(sslSocket, host, user, pass, + to, encSubject, encBody, + fromName); + sslSocket.close(); + } + logger.info( + "[Sdf1_login] 邮件发送成功 -> " + + to); + return true; + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件异常: " + + e.getClass() + .getSimpleName() + + ": " + e.getMessage()); + if (e.getCause() != null) { + logger.severe( + "[Sdf1_login] 根因: " + + e.getCause() + .getMessage()); + } + return false; + } + } + + private void doSmtp(SSLSocket socket, + String host, String user, String pass, + String to, String subject, String body, + String fromName) throws Exception { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + socket.getInputStream(), + "UTF-8")); + OutputStream rawOut = + socket.getOutputStream(); + PrintWriter writer = + new PrintWriter( + new java.io.BufferedOutputStream( + rawOut), true); + + checkResponse(reader, "220"); + sendLine(writer, "EHLO " + host); + checkResponse(reader, "250"); + sendLine(writer, "AUTH LOGIN"); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + user.getBytes("UTF-8"))); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + pass.getBytes("UTF-8"))); + checkResponse(reader, "235"); + sendLine(writer, + "MAIL FROM:<" + user + ">"); + checkResponse(reader, "250"); + sendLine(writer, + "RCPT TO:<" + to + ">"); + checkResponse(reader, "250"); + sendLine(writer, "DATA"); + checkResponse(reader, "354"); + sendLine(writer, + "From: " + fromName + + " <" + user + ">"); + sendLine(writer, + "To: <" + to + ">"); + sendLine(writer, + "Reply-To: <" + user + ">"); + sendLine(writer, + "Subject: " + subject); + sendLine(writer, "MIME-Version: 1.0"); + sendLine(writer, + "Content-Type: text/plain; " + + "charset=UTF-8"); + sendLine(writer, + "Content-Transfer-Encoding: " + + "base64"); + sendLine(writer, ""); + sendLine(writer, body); + sendLine(writer, "."); + checkResponse(reader, "250"); + sendLine(writer, "QUIT"); + } + + private void sendLine(PrintWriter writer, + String line) { + writer.print(line + "\r\n"); + writer.flush(); + } + + private void checkResponse( + BufferedReader reader, + String expected) throws Exception { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + if (line.length() >= 4 + && Character.isDigit( + line.charAt(0)) + && line.charAt(3) == ' ') + break; + } + String resp = sb.toString().trim(); + if (!resp.startsWith(expected)) { + if (resp.startsWith("535")) + logger.severe( + "[Sdf1_login] SMTP 535 " + + "认证失败"); + throw new Exception( + "SMTP响应异常: 期望 " + + expected + + ",实际: " + resp); + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/GUIManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/GUIManager.java new file mode 100644 index 0000000..cbc7102 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/GUIManager.java @@ -0,0 +1,2681 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + + + + +import java.util.*; + +public class GUIManager implements Listener { + + private final Main plugin; + + public static final String T_MAIN = + "§6§l服务器菜单"; + public static final String T_MORE = + "§6§l更多功能"; + public static final String T_EDITOR = + "§6§l编辑菜单项"; + public static final String T_BIOME = + "§b§l目标群系传送"; + public static final String T_MY_INFO = + "§6§l我的信息"; + public static final String T_GIFT_STAGES = + "§6§l礼包阶段"; + public static final String T_ADMIN = + "§6§l管理员面板"; + public static final String T_INVITE = + "§6§l邀请数据"; + public static final String T_USER_MGMT = + "§6§l用户管理"; + public static final String T_TASK_CENTER = + "§6§l任务中心"; + private final Map userMgmtPages = new HashMap<>(); + private static final int UMGMT_PAGE_SIZE = 45; + + + + public GUIManager(Main plugin) { + this.plugin = plugin; + } + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_MAIN); +// ===== 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + + // 已占用的槽位 + Set occupied = new HashSet<>(); + occupied.add(10); + occupied.add(12); + occupied.add(14); + occupied.add(16); + occupied.add(19); + occupied.add(21); + occupied.add(23); + occupied.add(25); + occupied.add(29); + occupied.add(31); + occupied.add(32); + occupied.add(34); + + // 自定义区域:第1排(0-8) + 第5排(36-44) + 第6排(45-53) + int[] customArea = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53 + }; + + int autoIdx = 0; + for (int ci = 0; + ci < customs.size() && ci < 27; ci++) { + MenuManager.MenuItem mi = + customs.get(ci); + + // 权限过滤 + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + + int targetSlot = -1; + + // 玩家指定了格子? + if (mi.slot != null) { + targetSlot = findBestSlot( + mi.slot - 1, occupied); + } + + // 未指定或冲突 → 自动排队 + if (targetSlot < 0) { + while (autoIdx < customArea.length) { + int s = customArea[autoIdx++]; + if (!occupied.contains(s)) { + targetSlot = s; + break; + } + } + } + + if (targetSlot < 0 + || targetSlot >= 54) continue; + occupied.add(targetSlot); + + // 放置物品 + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(java.util.Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(targetSlot, icon); + } + } + +// 背景填充 + fillBg(g); + p.openInventory(g); + + // ===== Row1(9-17): 核心功能 ===== + g.setItem(10, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(12, mkItem(Material.PAPER, + "§b§l邀请数据")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + g.setItem(16, mkItem(Material.COMPASS, + "§b§l群系传送", + "§7传送到指定群系")); + + // ===== Row2(18-26): 日常功能 ===== + g.setItem(19, mkItem(Material.BOOK, + "§6工单系统")); + g.setItem(21, mkItem(Material.EMERALD_BLOCK, + "§a§l每日签到", + "§7签到获取积分和惊喜奖励")); + g.setItem(23, mkItem(Material.ENDER_CHEST, + "§6§l任务中心")); + if (p.isOp() || isAdmin(p)) { + g.setItem(25, mkItem(Material.HOPPER, + "§6§l垃圾回收站", + "§7查看和取回清理的物品")); + + + } + // ===== Row3(27-35): 管理功能 ===== + // ★ 商店 - 所有人都能看 ★ + g.setItem(27, mkItem(Material.CHEST, + "§6§l商店", + "§7浏览、购买、出售物品", + "§7货币: 债券")); + + if (isAdmin(p)) { + g.setItem(29, mkItem( + Material.REDSTONE_BLOCK, + "§c§l管理员面板")); + } + if (p.isOp()) { + g.setItem(31, mkItem(Material.ECHO_SHARD, + "§c§l管理菜单", + "§7添加、编辑、删除菜单项")); + } + // 在 openMain 方法中,Row3 区域加: + + // ★ 余额查询 - 所有人都能看 ★ + double myBal = 0; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault.economy + .Economy.class); + if (reg != null) + myBal = reg.getProvider() + .getBalance(p); + } catch (Exception ignored) {} + int myBonds = plugin.getBonds().getBonds(p.getName()); + g.setItem(32, mkItem(Material.EMERALD, + "§b§l我的钱包", + "§7查看债券余额与流水记录")); + +// ★ 余额操作 - 仅OP和tag玩家 ★ + if (isAdmin(p)) { + g.setItem(34, mkItem(Material.GOLD_INGOT, + "§6§l余额操作", + "§7给钱、扣钱")); + } + + + + // ===== Row0(0-8): 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + for (int i = 0; + i < customs.size() && i < 9; + i++) { + MenuManager.MenuItem mi = + customs.get(i); + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(i, icon); + } + } + + // 背景最后填充(不覆盖物品) + fillBg(g); + p.openInventory(g); + } + private int findBestSlot( + int requested, + Set occupied) { + // 目标位空闲直接用 + if (requested >= 0 && requested < 54 + && !occupied.contains(requested)) { + return requested; + } + // BFS上下左右找最近空位 + int row = requested / 9; + int col = requested % 9; + boolean[][] visited = + new boolean[6][9]; + java.util.Queue queue = + new java.util.LinkedList<>(); + queue.add(new int[]{row, col}); + visited[row][col] = true; + int[][] dirs = { + {-1, 0}, {1, 0}, + {0, -1}, {0, 1}}; + while (!queue.isEmpty()) { + int[] cur = queue.poll(); + for (int[] d : dirs) { + int nr = cur[0] + d[0]; + int nc = cur[1] + d[1]; + if (nr >= 0 && nr < 6 + && nc >= 0 && nc < 9 + && !visited[nr][nc]) { + visited[nr][nc] = true; + int s = nr * 9 + nc; + if (!occupied.contains(s)) { + return s; + } + queue.add(new int[]{nr, nc}); + } + } + } + return -1; + } + + public void openMenuManager(Player p) { + List list = + plugin.getMenu().getItems(); + Inventory g = Bukkit.createInventory( + null, 27, + "§c§l菜单管理"); + fillBg(g); + + for (int i = 0; + i < list.size() && i < 9; i++) { + MenuManager.MenuItem mi = list.get(i); + g.setItem(i, mkItem( + mi.icon.getType(), + "§e" + mi.title, + "§7指令: " + mi.command, + "§7权限: " + mi.permType, + "", + "§a左键编辑", + "§7右键切换权限")); + } + + g.setItem(22, mkItem(Material.LIME_WOOL, + "§a§l添加菜单项")); + + g.setItem(26, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + + public void openBalanceOps(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作"); + fillBg(g); + + net.milkbowl.vault.economy.Economy eco = null; + try { + var reg = plugin.getServer().getServicesManager() + .getRegistration(net.milkbowl.vault.economy.Economy.class); + if (reg != null) eco = reg.getProvider(); + } catch (Exception ignored) {} + + int slot = 0; + for (Player op : Bukkit.getOnlinePlayers()) { + if (slot >= 45) break; + double bal = 0; + if (eco != null) bal = eco.getBalance(op); + boolean isSelf = op.getName().equals(p.getName()); + int bondBal = plugin.getBonds().getBonds(op.getName()); + g.setItem(slot, mkItem( + isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD, + "§e" + op.getName(), + "§7金币: §a$" + String.format("%.2f", bal), + "§7债券: §e" + bondBal + " §6枚", + "", + "§a左键经济(±) §6右键债券(±)")); + slot++; + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openShop(Player p) { + p.closeInventory(); + plugin.getShopManager().openShopMain(p); + } + + public void openTicket(Player p) { + p.closeInventory(); + p.performCommand("sdf1_login ticket"); + } + @EventHandler + public void onInvClose( + + InventoryCloseEvent e) { + if (!(e.getPlayer() instanceof Player)) + return; + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + Player p3 = (Player) e.getPlayer(); + String t = e.getView().getTitle(); + if (!(e.getPlayer() instanceof Player)) return; + Player p2 = (Player) e.getPlayer(); + + if (!t.equals(T_EDITOR)) return; + + // ★ 编辑器关闭时清理空项 ★ + List list = + plugin.getMenu().getItems(); + if (!list.isEmpty()) { + MenuManager.MenuItem last = + list.get(list.size() - 1); + if ((last.command == null + || last.command.trim().isEmpty() + || last.command.trim() + .equals("/")) + && "新菜单项" + .equals(last.title)) { + list.remove(list.size() - 1); + } + } + } + + public void openSubMenu( + Player p, String fileName) { + String menuTitle = + "§6§l" + fileName + .replace(".txt", ""); + Inventory g = Bukkit.createInventory( + null, 54, menuTitle); + fillBg(g); + + java.io.File f = new java.io.File( + plugin.getDataFolder(), fileName); + if (!f.exists()) { + p.sendMessage("§c文件不存在: " + + fileName); + return; + } + try { + List lines = + java.nio.file.Files + .readAllLines( + f.toPath(), + java.nio.charset + .StandardCharsets + .UTF_8); + List items = + new ArrayList<>(); + StringBuilder buf = + new StringBuilder(); + boolean inItem = false; + + for (String raw : lines) { + String l = raw.trim(); + if (l.startsWith("#") + || l.startsWith("//")) + continue; + if (l.contains("")) { + inHtml = false; + l = l.substring( + l.indexOf("-->") + 3) + .trim(); + } else { + plugin.getLogger().info( + "[Menu] L" + lineNum + + ": [HTML注释中]"); + continue; + } + } + + // ===== 第3优先:整行注释(最高裁决) ===== + // 以 # 或 // 开头的行,直接跳过 + // 不管后面包含什么 /* "); + pw.println(); + pw.println("{"); + pw.println(" 标题:主城传送"); + pw.println(" 指令:/spawn"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:COMPASS"); + pw.println("}"); + pw.println(); + pw.println("{"); + pw.println(" 标题:随机传送"); + pw.println(" 指令:/rtp"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:ENDER_PEARL"); + pw.println("}"); + } catch (Exception ignored) { + } + } + + // ===== 保存 ===== + public void saveMenu() { + File f = new File( + plugin.getDataFolder(), + "菜单.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 服务器菜单"); + pw.println(); + for (MenuItem mi : items) { + pw.println("{"); + pw.println(" 标题:" + + mi.title); + pw.println(" 指令:" + + mi.command); + pw.println(" 权限类型:" + + mi.permType); + // ★ 保存实际材质名 ★ + pw.println(" 图标:" + + mi.icon.getType().name()); + pw.println("}"); + pw.println(); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Menu] 保存失败: " + + e.getMessage()); + } + } + + // ===== 聊天编辑 ===== + public boolean isEditing(String name) { + return chatWait.contains(name); + } + + public void editTitle( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "title"); + p.closeInventory(); + p.sendMessage("§e请输入菜单标题:"); + } + + public void editCommand( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "command"); + p.closeInventory(); + p.sendMessage("§e请输入指令" + + "(自动补全/):"); + } + + public void onChat(Player p, String input) { + String name = p.getName(); + if (!chatWait.remove(name)) return; + + String field = chatField.remove(name); + Integer idx = chatIdx.get(name); + if (idx == null || idx >= items.size()) return; + + MenuItem mi = items.get(idx); + + if ("title".equals(field)) { + mi.title = input; + p.sendMessage("§a标题: " + input); + } else if ("command".equals(field)) { + if (!input.startsWith("/")) + input = "/" + input; + mi.command = input; + p.sendMessage("§a指令: " + input); + } + + Bukkit.getScheduler().runTask(plugin, () -> + plugin.gui.openEditor(p, idx)); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/OrderManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/OrderManager.java new file mode 100644 index 0000000..9dcb625 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/OrderManager.java @@ -0,0 +1,1345 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; + +public class OrderManager implements Listener { + private final Main plugin; + private final List orders = + Collections.synchronizedList(new ArrayList<>()); + private final Map userViewPage = new HashMap<>(); + private final Map adminViewPage = new HashMap<>(); + private final Map adminViewTarget = new HashMap<>(); + private final Set partialRefundListening = new HashSet<>(); + private final Map partialRefundTarget = new HashMap<>(); + private final Map receiptBuilderSection = new HashMap<>(); + private String tplHeader = ""; + private String tplItemLine = "{品名} x{数量} {原价}→{执行价} 小计{小计}枚"; + private String tplDiscountLine = "优惠: -{优惠额}枚 ({优惠类型})"; + private String tplPackLine = "打包: +{打包费}枚 ({打包颜色})"; + private String tplSummary = "原价{原价合计} → 实付{实付}"; + private String tplFooter = "感谢惠顾,欢迎再来!"; + private static final long RETENTION_30D = 30L * 24 * 60 * 60 * 1000; + private static final long SELF_REFUND_WINDOW = 5 * 60 * 1000; + private static final int PAGE_SIZE = 21; + private static final int[] SLOTS = { + 10,11,12,13,14,15,16,19,20,21,22,23,24,25,28,29,30,31,32,33,34}; + // ===== 字段 ===== + private final Set shulkerUsedOrders = + Collections.synchronizedSet(new HashSet<>()); + // ===== 待审批的退货退款 ===== + private final Map pendingReturns = + new HashMap<>(); + // ===== 退货申请确认 ===== + private final Map returnConfirmWaiting = + new HashMap<>(); // uuid -> orderId + private static final int RETURN_CONFIRM_TIMEOUT = 600; // 30秒 + + + private static class PendingReturn { + long orderId; + UUID playerUUID; + String playerName; + int totalPaid; + long createTime; + String packColor; + String packType; + List items; + + PendingReturn(OrderRecord r) { + this.orderId = r.orderId; + this.playerUUID = r.uuid; + this.playerName = r.player; + this.totalPaid = r.totalPaid; + this.createTime = System.currentTimeMillis(); + this.packColor = r.packColor; + this.packType = r.packType; + this.items = new ArrayList<>(r.items); + } + } + + + public static class OrderItem { + public String name; public String mat; + public int originalPrice; public int finalPrice; public int qty; + public OrderItem(String n,String m,int op,int fp,int q){ + name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;} + public int subtotal(){return finalPrice*qty;} + public int originalSubtotal(){return originalPrice*qty;} + } + public static class OrderRecord { + public long orderId; public UUID uuid; public String player; + public List items; public int totalOriginal; public int totalPaid; + public int discount; public String discountType; public String couponCode; + public int packFee; public String packType; public String packColor; + public long timestamp; public long printTime; public int status; + public int refundedAmount; public String refundType; + } + + public OrderManager(Main plugin) { + this.plugin = plugin; + loadReceiptConfig(); + loadOrders(); + startCleanupTask(); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + public List getOrders() { return orders; } + + // ===== 小票模板 ===== + private void loadReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + if (!f.exists()) createDefaultReceiptConfig(f); + Map m = loadMap(f); + tplHeader = m.getOrDefault("头部", tplHeader); + tplItemLine = m.getOrDefault("商品行", tplItemLine); + tplDiscountLine = m.getOrDefault("优惠行", tplDiscountLine); + tplPackLine = m.getOrDefault("打包行", tplPackLine); + tplSummary = m.getOrDefault("汇总", tplSummary); + tplFooter = m.getOrDefault("页脚", tplFooter); + } + private void saveReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=" + tplHeader); + L.add("商品行=" + tplItemLine); + L.add("优惠行=" + tplDiscountLine); + L.add("打包行=" + tplPackLine); + L.add("汇总=" + tplSummary); + L.add("页脚=" + tplFooter); + writeLines(f, L); + } + private void createDefaultReceiptConfig(File f) { + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}"); + L.add("商品行=§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚"); + L.add("优惠行=优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})"); + L.add("打包行=打包: §c+{打包费}枚§r ({打包颜色})"); + L.add("汇总=原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚"); + L.add("页脚=感谢惠顾,欢迎再来!"); + writeLines(f, L); + } + + // ===== 订单记录 ===== + public void recordOrder(Player p, List items, + int totalOriginal, int totalPaid, int discount, + String discountType, String couponCode, int packFee, + String packType, String packColor) { + OrderRecord r = new OrderRecord(); + r.orderId = System.currentTimeMillis(); + r.uuid = p.getUniqueId(); r.player = p.getName(); + r.items = items; r.totalOriginal = totalOriginal; + r.totalPaid = totalPaid; r.discount = discount; + r.discountType = discountType; r.couponCode = couponCode; + r.packFee = packFee; r.packType = packType; + r.packColor = packColor; r.timestamp = r.orderId; + r.printTime = 0; r.status = 0; r.refundedAmount = 0; + r.refundType = null; + orders.add(r); saveOrders(); + } + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + // ===== 小票聊天栏 ===== + /** + * 聊天栏打印小票(给定参数 Player + OrderRecord) + * 不要改动方法签名 + */ + public void sendReceiptChat(Player p, OrderRecord r) { + if (p == null || r == null) return; + + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + p.sendMessage("§7§m "); + p.sendMessage("§6§l======== 商店购物小票 ========"); + + // 退款标记 + String statusTag = getStatusTag(r); + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§7订单号: §f#" + r.orderId); + p.sendMessage("§7下单时间: §f" + + df.format(new Date(r.timestamp))); + p.sendMessage("§7玩家: §f" + r.player); + p.sendMessage("§7------------------------------"); + + for (OrderItem it : r.items) { + String price; + if (it.originalPrice != it.finalPrice) { + price = "§f" + it.originalPrice + + "§7→§6" + it.finalPrice; + } else { + price = "§6" + it.finalPrice; + } + p.sendMessage("§7 §e" + it.name + + " §7x" + it.qty + + " " + price + "枚" + + " §6小计" + it.subtotal() + "枚"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7原价: §f" + r.totalOriginal + "枚"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "§7(优惠券 -" + pct + "%)" + : "eco".equals(r.discountType) + ? "§7(环保 -" + pct + "%)" + : "§7(-" + pct + "%)"; + p.sendMessage("§7优惠: §a-" + r.discount + + "枚 " + dt); + } + + if (r.packFee > 0) { + String cn = r.packColor != null + ? r.packColor + .replace("_SHULKER_BOX", "") + .replace("SHULKER_BOX", "原色") + : "默认"; + p.sendMessage("§7打包: §c+" + r.packFee + + "枚 §7(" + cn + "潜影盒)"); + } else { + p.sendMessage("§7包装: 直接放入背包"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7实付: §6" + r.totalPaid + "枚"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + p.sendMessage("§7余额: §a" + bal + "枚"); + + // 退款标记 + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§6§l=============================="); + r.printTime = System.currentTimeMillis(); + } + + /** + * 生成书本小票内容 + */ + private String buildReceiptText(OrderRecord r) { + String statusTag = getStatusTag(r); + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + StringBuilder sb = new StringBuilder(); + + sb.append("§6§l===== 商店小票 =====\n"); + + // 退款标记 + String tag = getStatusTag(r); + if (!tag.isEmpty()) sb.append(tag).append("\n"); + + sb.append("订单号: #").append(r.orderId).append("\n"); + sb.append("时间: ").append( + df.format(new Date(r.timestamp))).append("\n"); + sb.append("玩家: ").append(r.player).append("\n"); + sb.append("------------------------------\n"); + + for (OrderItem it : r.items) { + String pr = (it.originalPrice != it.finalPrice) + ? it.originalPrice + "->" + it.finalPrice + : String.valueOf(it.finalPrice); + sb.append(it.name).append(" x").append(it.qty) + .append(" ").append(pr).append("枚") + .append(" 小计").append(it.subtotal()) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("原价: ").append(r.totalOriginal) + .append("枚\n"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "优惠券(-" + pct + "%)" + : "环保(-" + pct + "%)"; + sb.append("优惠: -").append(r.discount) + .append("枚 ").append(dt).append("\n"); + } + + if (r.packFee > 0) { + sb.append("打包: +").append(r.packFee) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("实付: ").append(r.totalPaid).append("枚\n"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + sb.append("余额: ").append(bal).append("枚\n"); + + // 退款标记 + if (!statusTag.isEmpty()) sb.append("\n").append(tag).append("\n"); + + return sb.toString(); + } + + /** + * 退款状态标记文本 + */ + private String getStatusTag(OrderRecord r) { + switch (r.status) { + case 2: + return "§c§l<<<<<<< 已全额退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 3: + return "§c§l<<<<<<< 已部分退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 4: + return "§c§l<<<<<<< 退款申请已拒绝 >>>>>>>"; + default: + return ""; + } + } + + /** + * 创建小票书 + */ + public ItemStack createReceiptBook(OrderRecord r) { + String text = buildReceiptText(r); + String[] lines = text.split("\n"); + + ItemStack book = new ItemStack(Material.WRITTEN_BOOK); + BookMeta bm = (BookMeta) book.getItemMeta(); + bm.setTitle("购物小票 #" + r.orderId); + bm.setAuthor("商店"); + + StringBuilder page = new StringBuilder(); + int lineCount = 0; + for (String line : lines) { + // ★ 保留颜色代码 + page.append(line).append("\n"); + lineCount++; + if (lineCount >= 13) { + bm.addPage(page.toString().trim()); + page = new StringBuilder(); + lineCount = 0; + } + } + if (page.length() > 0) { + bm.addPage(page.toString().trim()); + } + + book.setItemMeta(bm); + return book; + } + + // ===== 退款工具 ===== + private boolean withinSelfRefundWindow(OrderRecord r) { return System.currentTimeMillis() - r.timestamp < SELF_REFUND_WINDOW; } + private int countDirectItems(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private int countShulkers(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private void removeItems(Player p, String matName, int amt) { + Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt)); + } + private boolean removeOneShulker(Player p, + String materialName) { + // 先尝试精确匹配 + Material mat = Material.matchMaterial(materialName); + if (mat != null) { + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + return true; + } + } + } + // 匹配不到,移除任意一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .endsWith("_SHULKER_BOX")) { + p.getInventory().clear(i); + return true; + } + } + return false; + } + public String validateRefund(OrderRecord r, Player p) { + if (r.status != 0) + return "订单状态: " + getStatusName(r.status); + + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // ★ 检查:潜影盒是否被放置过 + if (shulkerUsedOrders.contains(r.orderId)) { + return "§c潜影盒已被使用过,无法自助退款"; + } + + // ★ 检查:背包里是否有同一订单的潜影盒 + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im == null || im.getLore() == null) + continue; + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + return null; // 找到了,可退 + } + } + } + return "§c对应的潜影盒已不在背包中"; + } else { + // ★ 散装:检测物品数量 + for (OrderItem it : r.items) { + int have = countDirectItems(p, it.mat); + if (have < it.qty) + return "§c" + it.name + "不足(有" + + have + "/" + it.qty + ")"; + } + } + return null; + } + + + + private void notifyPlayer(UUID uuid, String msg) { Player p = Bukkit.getPlayer(uuid); if (p != null && p.isOnline()) p.sendMessage(msg); } + private void notifyAdmins(Player p, OrderRecord r) { + String m = "§e[订单] " + p.getName() + " 申请退款 #" + r.orderId + " §6" + r.totalPaid + "枚"; + for (Player a : Bukkit.getOnlinePlayers()) if (isAdmin(a) && !a.equals(p)) a.sendMessage(m); + } + private String getStatusName(int s) { switch(s){case 0:return "正常";case 1:return "申请中";case 2:return "已退";case 3:return "部分退";case 4:return "已拒绝";default:return "未知";} } + private boolean isAdmin(Player p) { + if (p.isOp()) return true; + try { ConfigManager cm = plugin.getConfigMgr(); if (cm == null) return false; return p.hasPermission("sdf1.admin") || p.hasPermission("sdf1." + cm.adminTag); } catch (Exception e) { return p.hasPermission("sdf1.admin"); } + } + + // ===== 订单中心 ===== + public void openOrderCenter(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l订单中心"); + g.setItem(13, mkItem(Material.BOOK, "§e§l购买记录", "§7查看历史订单", "", "§a点击进入")); + if (isAdmin(p)) { g.setItem(31, mkItem(Material.NETHER_STAR, "§c§l管理员面板", "§7在线玩家购物记录", "§7支持退款操作", "", "§a点击进入")); + g.setItem(33, mkItem(Material.CRAFTING_TABLE, "§e§l小票配置", "§7配置小票模板样式", "", "§a点击进入")); } + g.setItem(49, mkItem(Material.ARROW, "§7返回商店")); + p.openInventory(g); + } + + // ===== 购买记录 ===== + public void openMyOrders(Player p, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l购买记录"); + List list = getPlayerAllOrders(p.getUniqueId()); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + userViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon; String st; String act; + if (r.status != 0) { icon = r.status == 1 ? Material.PAPER : Material.BARRIER; st = "§7状态: " + getStatusName(r.status); act = "§b左键: 补打小票"; } + else if (withinSelfRefundWindow(r)) { icon = Material.EMERALD; st = "§7状态: §a可退款(5分钟内)"; act = "§b左键: 补打小票\n§a右键: 退款退货\n§eShift+右键: 仅退款"; } + else { icon = Material.BOOK; st = "§7状态: §7已过自助退款期"; act = "§b左键: 补打小票\n§e右键: 申请退款退货\n§6Shift+右键: 申请仅退款"; } + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7原价: §f" + r.totalOriginal + "枚"); + if (r.discount > 0) lore.add("§7优惠: §a-" + r.discount + "枚"); + lore.add("§7实付: §6" + r.totalPaid + "枚"); lore.add(st); lore.add(""); + for (String line : act.split("\n")) lore.add(line); + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7暂无订单记录")); + addPageButtons(g, page, total); + p.openInventory(g); + } + + // ===== 管理员面板 ===== + public void openAdminPanel(Player p, int page) { + if (!isAdmin(p)) { p.sendMessage("§c权限不足"); return; } + Inventory g = Bukkit.createInventory(null, 54, "§c§l管理员订单面板"); + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int total = Math.max(1, (int) Math.ceil((double) online.length / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); adminViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, online.length); + // ★ 替换 openAdminPanel 中的物品创建部分 + for (int i = start; i < end; i++) { + int si = i - start; + if (si >= SLOTS.length) break; + Player t = online[i]; + List tl = getPlayerOrdersWithin( + t.getUniqueId(), RETENTION_30D); + int ts = 0; + for (OrderRecord r : tl) ts += r.totalPaid; + + ItemStack is = new ItemStack(Material.PLAYER_HEAD); + org.bukkit.inventory.meta.SkullMeta sm = + (org.bukkit.inventory.meta.SkullMeta) + is.getItemMeta(); + if (sm != null) { + sm.setOwner(t.getName()); // ★ 设置头颅皮肤 + sm.setDisplayName("§e" + t.getName()); + sm.setLore(Arrays.asList( + "§7订单: §f" + tl.size() + "笔", + "§7消费: §6" + ts + "枚", + "", "§a点击进入")); + is.setItemMeta(sm); // ★ 这行之前缺失了 + } + g.setItem(SLOTS[si], is); + } + + if (online.length == 0) g.setItem(31, mkItem(Material.BARRIER, "§7无在线玩家")); + addPageButtons(g, page, total); p.openInventory(g); + } + + // ===== 管理员订单 ===== + public void openAdminOrders(Player p, UUID targetUUID, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§c§l订单管理"); + String name = Bukkit.getOfflinePlayer(targetUUID).getName(); + List list = getPlayerOrdersWithin(targetUUID, RETENTION_30D); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + adminViewPage.put(p.getUniqueId(), page); adminViewTarget.put(p.getUniqueId(), targetUUID); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon = r.status == 2 || r.status == 4 ? Material.BARRIER : r.status == 1 ? Material.PAPER : Material.BOOK; + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7实付: §6" + r.totalPaid + "枚"); + if (r.status == 1) { lore.add("§7状态: §e申请中(" + nn(r.refundType) + ")"); lore.add(""); lore.add("§a右键: 批准"); lore.add("§c左键: 拒绝"); } + else if (r.status == 2) lore.add("§7已退: §a" + r.refundedAmount + "枚"); + else if (r.status == 3) lore.add("§7部分退: §a" + r.refundedAmount + "枚"); + else if (r.status == 4) lore.add("§7已拒绝"); + else { lore.add(""); lore.add("§a右键: 整单退款"); lore.add("§e左键: 部分退款(输入%)"); } + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7该玩家暂无订单")); + addPageButtons(g, page, total); + g.setItem(49, mkItem(org.bukkit.Material.PAPER, name + " §e" + (page + 1) + "/" + total)); + p.openInventory(g); + } + private void addPageButtons(Inventory g, int page, int total) { + g.setItem(48, page > 0 ? mkItem(Material.ARROW, "§7上一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + if (g.getItem(49) == null) g.setItem(49, mkItem(Material.PAPER, "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 ? mkItem(Material.ARROW, "§7下一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + } + + // ===== 小票配置 ===== + public void openReceiptConfig(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l小票配置"); + g.setItem(10, mkItem(Material.PAPER, "§e§l头部", "§7" + brief(tplHeader), "", "§a点击编辑")); + g.setItem(11, mkItem(Material.PAPER, "§e§l商品行", "§7" + brief(tplItemLine), "", "§a点击编辑")); + g.setItem(12, mkItem(Material.PAPER, "§e§l优惠行", "§7" + brief(tplDiscountLine), "", "§a点击编辑")); + g.setItem(14, mkItem(Material.PAPER, "§e§l打包行", "§7" + brief(tplPackLine), "", "§a点击编辑")); + g.setItem(15, mkItem(Material.PAPER, "§e§l汇总", "§7" + brief(tplSummary), "", "§a点击编辑")); + g.setItem(16, mkItem(Material.PAPER, "§e§l页脚", "§7" + brief(tplFooter), "", "§a点击编辑")); + g.setItem(22, mkItem(Material.BOOK, "§6§l预览(随机价格)", "§7预览小票效果")); + g.setItem(48, mkItem(Material.LIME_WOOL, "§a§l保存")); g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(50, mkItem(Material.RED_WOOL, "§c§l重置")); + p.openInventory(g); + } + public void openReceiptBuilder(Player p, String section) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l编辑: " + sectionName(section)); + g.setItem(10, mkItem(Material.EMERALD, "§a{品名}")); g.setItem(11, mkItem(Material.EMERALD, "§a{原价}")); + g.setItem(12, mkItem(Material.EMERALD, "§a{执行价}")); g.setItem(13, mkItem(Material.EMERALD, "§a{数量}")); + g.setItem(14, mkItem(Material.EMERALD, "§a{小计}")); g.setItem(15, mkItem(Material.EMERALD, "§a{商店名}")); + g.setItem(16, mkItem(Material.EMERALD, "§a{玩家}")); g.setItem(19, mkItem(Material.DIAMOND, "§b{订单号}")); + g.setItem(20, mkItem(Material.DIAMOND, "§b{订单时间}")); g.setItem(21, mkItem(Material.DIAMOND, "§b{打印时间}")); + g.setItem(22, mkItem(Material.DIAMOND, "§b{日期}")); g.setItem(23, mkItem(Material.DIAMOND, "§b{时间}")); + g.setItem(24, mkItem(Material.DIAMOND, "§b{原价合计}")); g.setItem(25, mkItem(Material.DIAMOND, "§b{实付}")); + g.setItem(28, mkItem(Material.GOLD_INGOT, "§6{优惠额}")); g.setItem(29, mkItem(Material.GOLD_INGOT, "§6{优惠类型}")); + g.setItem(30, mkItem(Material.GOLD_INGOT, "§6{优惠券码}")); g.setItem(31, mkItem(Material.GOLD_INGOT, "§6{打包费}")); + g.setItem(32, mkItem(Material.GOLD_INGOT, "§6{打包颜色}")); g.setItem(33, mkItem(Material.GOLD_INGOT, "§6{余额}")); + g.setItem(37, mkItem(Material.STONE, "§f═══")); g.setItem(38, mkItem(Material.STONE, "§f───")); + g.setItem(39, mkItem(Material.STONE, "§f═════════════")); g.setItem(40, mkItem(Material.PAPER, "§e换行(\\n)")); + g.setItem(47, mkItem(Material.RED_STAINED_GLASS_PANE, "§c删末尾")); g.setItem(48, mkItem(Material.RED_WOOL, "§c清空")); + g.setItem(49, mkItem(Material.ARROW, "§7返回")); g.setItem(50, mkItem(Material.LIME_WOOL, "§a保存")); + g.setItem(51, mkItem(Material.BLUE_WOOL, "§9重置")); + p.openInventory(g); + } + // ===== 点击处理 ===== + public boolean handleCenterClick(Player p, int raw) { + if (raw == 13) { openMyOrders(p, 0); return true; } + if (raw == 31 && isAdmin(p)) { openAdminPanel(p, 0); return true; } + if (raw == 33 && isAdmin(p)) { openReceiptConfig(p); return true; } + if (raw == 49) { plugin.getShopManager().openShopMain(p); return true; } + return true; + } + public boolean handleMyOrdersClick(Player p, int raw, boolean left, boolean shift) { + List list = getPlayerAllOrders(p.getUniqueId()); + int page = userViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openMyOrders(p, page - 1); return true; } + if (raw == 50) { openMyOrders(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (left) { + try { ItemStack book = createReceiptBook(r); + HashMap ov = p.getInventory().addItem(book); + for (ItemStack d : ov.values()) p.getWorld().dropItemNaturally(p.getLocation(), d); + r.printTime = System.currentTimeMillis(); p.sendMessage("§a已补打小票书: #" + r.orderId); + } catch (Exception e) { p.sendMessage("§c补打失败"); } + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // ★ Shift+右键:仅退款申请(需管理员审批) + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请,等待管理员审批"); + notifyAdmins(p, r); + } else { + // ★ 普通右键:5分钟内直接退(自动通过) + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason + + ",Shift+右键可申请仅退款"); + } else { + // 扣货 + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + if (isShulker) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory() + .getItem(i); + if (is != null && is.getType() + .name().contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + // 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + } + + + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // 仅退款申请 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } else { + // 5分钟内退货退款 + String reason = validateRefund(r, p); + if (reason != null) { + // ★ 不满足条件:询问是否申请 + p.sendMessage("§c" + reason); + askReturnConfirm(p, r); + } else { + // 满足条件:自动退 + doAutoReturn(p, r); + } + } + } else { + if (shift) { + // 申请仅退款 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请"); + notifyAdmins(p, r); + } else { + // ★ 申请退货退款:先检查条件 + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason); + } + // ★ 无论是否满足条件都询问 + askReturnConfirm(p, r); + } + } + } + } + if (left) { + // 已退款订单仍然允许补打小票,但小票内会显示退款标记 + try { + ItemStack book = createReceiptBook(r); + HashMap overflow = + p.getInventory().addItem(book); + for (ItemStack drop : overflow.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + r.printTime = System.currentTimeMillis(); + if (r.status != 0) { + p.sendMessage("§e已补打小票(含退款标记): #" + + r.orderId); + } else { + p.sendMessage("§a已补打小票书: #" + + r.orderId); + } + } catch (Exception e) { + p.sendMessage("§c补打失败"); + } + } + + final int fp = page; Bukkit.getScheduler().runTask(plugin, () -> openMyOrders(p, fp)); + return true; + } + public boolean handleAdminClick(Player p, int raw) { + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openAdminPanel(p, page - 1); return true; } + if (raw == 50) { openAdminPanel(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= online.length) return true; + openAdminOrders(p, online[actual].getUniqueId(), 0); return true; + } + public boolean handleAdminOrdersClick(Player p, int raw, boolean left, boolean shift) { + UUID tu = adminViewTarget.get(p.getUniqueId()); + if (tu == null) return false; + List list = getPlayerOrdersWithin(tu, RETENTION_30D); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { + openAdminOrders(p, tu, page - 1); + return true; + } + if (raw == 50) { + openAdminOrders(p, tu, page + 1); + return true; + } + if (raw == 53) { + openAdminPanel(p, 0); + return true; + } + int si = slotIndex(raw); + if (si < 0) return true; + int actual = si + page * PAGE_SIZE; + if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (r.status == 2 || r.status == 3 || r.status == 4) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + return true; + } + if (left) { + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款被拒绝"); + openAdminOrders(p, tu, page); + } else if (r.status == 0) { + partialRefundListening.add(p.getUniqueId()); + partialRefundTarget.put(p.getUniqueId(), r); + p.closeInventory(); + p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚"); + } + } else { + if (r.status == 0 || r.status == 1) { + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + } + } + // ★ 替换 handleAdminOrdersClick 中的审批逻辑 + if (left) { + // 左键 = 拒绝 + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款申请已被拒绝"); + openAdminOrders(p, tu, page); + } + } else { + // 右键 = 批准 + if (r.status == 1) { + String rt = r.refundType != null + ? r.refundType : ""; + + // ★ 退款退货申请 → 检查潜影盒 + if (rt.equals("refund_return_apply")) { + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 检查玩家背包里有没有带订单标签的盒子 + Player target = Bukkit.getPlayer(r.uuid); + if (target == null || !target.isOnline()) { + p.sendMessage("§c玩家不在线,无法执行退货退款"); + openAdminOrders(p, tu, page); + return true; + } + + boolean foundTagged = false; + boolean foundAny = false; + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is == null) continue; + if (!is.getType().name() + .contains("SHULKER")) continue; + foundAny = true; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + foundTagged = true; + // 收走盒子 + target.getInventory().clear(i); + break; + } + } + } + if (foundTagged) break; + } + + if (foundTagged) { + // ★ 有带标记的盒子 → 收走 + 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + + r.player + " §6" + + r.totalPaid + "枚(盒子已收走)"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + return true; + } else if (foundAny) { + // ★ 有盒子但没标记 → 等管理员决定 + pendingReturns.put(r.orderId, + new PendingReturn(r)); + p.closeInventory(); + p.sendMessage("§e§l[退货审批] 订单 #" + + r.orderId); + p.sendMessage("§7玩家背包有潜影盒但" + + "无订单标记,可能已被替换"); + p.sendMessage("§a输入 §e1 §a= 收走盒子并退款"); + p.sendMessage("§e输入 §e2 §e= 仅退款(不收盒子)"); + p.sendMessage("§c输入 §c3 §c= 拒绝退款"); + p.sendMessage("§72分钟内未操作自动拒绝"); + // 启动2分钟超时 + startReturnTimeout(r.orderId); + return true; + } else { + // ★ 背包里没有任何盒子 + p.sendMessage("§c玩家背包里没有潜影盒"); + p.sendMessage("§7可拒绝并通知玩家" + + "申请仅退款或把货拿回来"); + openAdminOrders(p, tu, page); + return true; + } + } + // ★ 散装退货退款:扣货+退钱 + Player target = Bukkit.getPlayer(r.uuid); + if (target != null && target.isOnline()) { + for (OrderItem it : r.items) + removeItems(target, it.mat, it.qty); + } + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + return true; + } + + // ★ 仅退款申请 / 5分钟内自动退款 + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已批准退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退款已批准,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + } + } + return true; + } + + /** + * 询问用户是否提交退货退款申请 + */ + private void askReturnConfirm(Player p, OrderRecord r) { + returnConfirmWaiting.put(p.getUniqueId(), r.orderId); + p.sendMessage(""); + p.sendMessage("§e§l[退货退款申请]"); + p.sendMessage("§7订单 #" + r.orderId + + " 实付" + r.totalPaid + "枚"); + p.sendMessage("§a输入 §e1 §a= 提交退货退款申请"); + p.sendMessage("§c输入 §e2 §c= 取消"); + p.sendMessage("§730秒内未操作自动取消"); + p.sendMessage(""); + + // 30秒超时 + final UUID uuid = p.getUniqueId(); + final long oid = r.orderId; + new BukkitRunnable() { + @Override + public void run() { + Long waiting = returnConfirmWaiting.get(uuid); + if (waiting != null && waiting == oid) { + returnConfirmWaiting.remove(uuid); + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§7退货退款申请已超时取消"); + } + } + } + }.runTaskLater(plugin, RETURN_CONFIRM_TIMEOUT); + } + + /** + * 执行5分钟内自动退货退款 + */ + private void doAutoReturn(Player p, OrderRecord r) { + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 找并收走带标签的盒子 + boolean found = false; + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + p.getInventory().clear(i); + found = true; + break; + } + } + } + if (found) break; + } + // 没找到标签盒子,收任意潜影盒 + if (!found) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + + + // ===== 2分钟超时 ===== + private void startReturnTimeout(long orderId) { + new BukkitRunnable() { + @Override + public void run() { + PendingReturn pr = pendingReturns + .remove(orderId); + if (pr == null) return; // 已处理 + Player admin = Bukkit.getPlayer(pr.playerUUID); + if (admin != null && admin.isOnline()) { + admin.sendMessage("§c[退货审批] 订单 #" + + orderId + " 超时,自动拒绝"); + } + // 通知玩家 + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款申请已超时拒绝"); + // 恢复订单状态 + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 0; + r.refundType = null; + saveOrders(); + } + } + }.runTaskLater(plugin, 2400L); // 2分钟 + } + + // ===== 查找订单 ===== + private OrderRecord findOrder(long orderId) { + synchronized (orders) { + for (OrderRecord r : orders) { + if (r.orderId == orderId) return r; + } + } + return null; + } + + + public boolean handleReceiptConfigClick(Player p, int raw) { + if (raw == 49) { openOrderCenter(p); return true; } + if (raw == 48) { saveReceiptConfig(); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 50) { createDefaultReceiptConfig(new File(plugin.getDataFolder(), "小票设置.txt")); loadReceiptConfig(); p.sendMessage("§e已重置"); openReceiptConfig(p); return true; } + if (raw == 22) { sendRandomPreview(p); return true; } + String sec = null; + if (raw == 10) sec = "header"; else if (raw == 11) sec = "itemLine"; else if (raw == 12) sec = "discountLine"; + else if (raw == 14) sec = "packLine"; else if (raw == 15) sec = "summary"; else if (raw == 16) sec = "footer"; + if (sec != null) { receiptBuilderSection.put(p.getUniqueId(), sec); openReceiptBuilder(p, sec); } + return true; + } + public boolean handleReceiptBuilderClick(Player p, int raw) { + String sec = receiptBuilderSection.get(p.getUniqueId()); if (sec == null) return false; + String token = getTokenBySlot(raw); + if (token != null) { String cur = getSection(sec); setSection(sec, cur.isEmpty() ? token : cur + token); openReceiptBuilder(p, sec); return true; } + if (raw == 47) { String c = getSection(sec); int lb = c.lastIndexOf('}'); if (lb >= 0) { int ls = c.lastIndexOf('{', lb); if (ls >= 0) setSection(sec, c.substring(0, ls)); } else setSection(sec, c.substring(0, Math.max(0, c.length() - 8))); openReceiptBuilder(p, sec); return true; } + if (raw == 48) { setSection(sec, ""); openReceiptBuilder(p, sec); return true; } + if (raw == 49) { receiptBuilderSection.remove(p.getUniqueId()); openReceiptConfig(p); return true; } + if (raw == 50) { saveReceiptConfig(); receiptBuilderSection.remove(p.getUniqueId()); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 51) { setSection(sec, getDefaultVal(sec)); openReceiptBuilder(p, sec); return true; } + return true; + } + + // ===== 聊天事件 ===== + @EventHandler + public void onAdminChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); UUID uuid = p.getUniqueId(); + // ★ 在 onAdminChat 方法开头添加 +// ===== 退货审批输入 ===== + for (Map.Entry entry : + new HashMap<>(pendingReturns).entrySet()) { + Long pendingOrderId = entry.getKey(); + PendingReturn pr = entry.getValue(); + if (pr.playerUUID.equals(p.getUniqueId()) + || isAdmin(p)) { + try { + int choice = Integer.parseInt( + e.getMessage().trim()); + pendingReturns.remove(pendingOrderId); + OrderRecord r = findOrder(pendingOrderId); + if (r == null) { + p.sendMessage("§c订单不存在"); + return; + } + + if (choice == 1) { + // ★ 收走盒子并退款 + Player target = Bukkit.getPlayer( + pr.playerUUID); + if (target != null && target.isOnline()) { + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + pendingOrderId)) { + target.getInventory().clear(i); + break; + } + } + } + break; + } + } + } + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(盒子已收走)"); + notifyPlayer(pr.playerUUID, + "§a[订单] 退货退款成功,§6" + + pr.totalPaid + "枚已退还"); + + } else if (choice == 2) { + // ★ 仅退款(不收盒子) + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§e已仅退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(未收盒子)"); + notifyPlayer(pr.playerUUID, + "§e[订单] 仅退款成功,§6" + + pr.totalPaid + "枚已退还" + + ",盒子无需退回"); + + } else if (choice == 3) { + // ★ 拒绝 + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款 #" + pendingOrderId); + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款已被拒绝" + + ",您可以申请仅退款" + + "或把货拿回来后重新申请"); + } else { + p.sendMessage("§c请输入 1、2 或 3"); + pendingReturns.put(pendingOrderId, pr); + } + return; + + } catch (NumberFormatException ignored) { + // 不是数字,跳过 + } + } + } +// ★ 在 onAdminChat 方法开头,处理用户退货确认 + if (returnConfirmWaiting.containsKey(uuid)) { + String msg = e.getMessage().trim(); + if (msg.equals("1")) { + // ★ 确认提交 + Long orderId = returnConfirmWaiting.remove(uuid); + if (orderId != null) { + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 1; + r.refundType = "refund_return_apply"; + saveOrders(); + p.sendMessage("§e已提交退货退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } + } + e.setCancelled(true); + return; + } else if (msg.equals("2")) { + // ★ 取消 + returnConfirmWaiting.remove(uuid); + p.sendMessage("§7已取消退货退款申请"); + e.setCancelled(true); + return; + } else { + // 输入其他内容:取消等待 + returnConfirmWaiting.remove(uuid); + // 不 return,让消息正常发送 + } + } + + if (partialRefundListening.contains(uuid)) { + e.setCancelled(true); partialRefundListening.remove(uuid); + OrderRecord t = partialRefundTarget.remove(uuid); if (t == null) return; + try { int pct = Integer.parseInt(e.getMessage().trim()); if (pct < 1 || pct > 100) { p.sendMessage("§c1-100"); return; } + int ref = t.totalPaid * pct / 100; if (ref <= 0) ref = 1; + plugin.getBonds().addBonds(t.player, ref); t.status = 3; t.refundedAmount = ref; saveOrders(); + p.sendMessage("§a部分退款 §e" + t.player + " §6" + ref + "枚(" + pct + "%)"); + notifyPlayer(t.uuid, "§a退款 §6" + ref + "枚"); + final UUID tu = adminViewTarget.get(uuid); final int pg = adminViewPage.getOrDefault(uuid, 0); + Bukkit.getScheduler().runTask(plugin, () -> openAdminOrders(p, tu, pg)); + } catch (NumberFormatException ex) { p.sendMessage("§c请输入数字"); } catch (Exception ex) { plugin.getLogger().log(Level.WARNING, "[Order]退款异常", ex); } + return; + } + if (receiptBuilderSection.containsKey(uuid)) { + e.setCancelled(true); String sec = receiptBuilderSection.remove(uuid); if (sec == null) return; + String msg = e.getMessage().trim(); + if ("cancel".equalsIgnoreCase(msg)) { Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); return; } + setSection(sec, msg.replace("\\n", "\n")); p.sendMessage("§a已更新 §e" + sec); + Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); + } + } + + // ===== 数据查询 ===== + private List getPlayerAllOrders(UUID uuid) { + List r = new ArrayList<>(); + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && System.currentTimeMillis() - o.timestamp < RETENTION_30D) r.add(o); } + Collections.reverse(r); return r; + } + private List getPlayerOrdersWithin(UUID uuid, long ms) { + List r = new ArrayList<>(); long cut = System.currentTimeMillis() - ms; + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && o.timestamp >= cut) r.add(o); } + Collections.reverse(r); return r; + } + + // ===== 持久化 ===== + private void loadOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); if (!f.exists()) return; + try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)); + OrderRecord cur = null; String line; + while ((line = br.readLine()) != null) { line = line.trim(); + if (line.equals("==")) { cur = new OrderRecord(); cur.items = new ArrayList<>(); } + else if (cur != null && line.contains("=")) { int eq = line.indexOf('='); String k = line.substring(0, eq); String v = line.substring(eq + 1); + switch (k) { case "id": cur.orderId = Long.parseLong(v); cur.timestamp = cur.orderId; break; case "uuid": cur.uuid = UUID.fromString(v); break; + case "player": cur.player = v; break; case "totalOriginal": cur.totalOriginal = Integer.parseInt(v); break; case "totalPaid": cur.totalPaid = Integer.parseInt(v); break; + case "discount": cur.discount = Integer.parseInt(v); break; case "discountType": cur.discountType = v; break; case "couponCode": cur.couponCode = v; break; + case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break; + case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break; + case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break; + case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; } + } else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; } + } if (cur != null && cur.uuid != null) orders.add(cur); br.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } + } + private void saveOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); + try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); + synchronized (orders) { for (OrderRecord r : orders) { pw.println("=="); + pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player); + pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); + pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); + pw.println("couponCode=" + nn(r.couponCode)); pw.println("packFee=" + r.packFee); + pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor)); + pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount); + pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType)); + for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty); + } } pw.flush(); pw.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); } + } + private void startCleanupTask() { new BukkitRunnable() { public void run() { synchronized (orders) { if (orders.removeIf(OrderManager.this::isExpired)) saveOrders(); } } }.runTaskTimer(plugin, 36000L, 36000L); } + /** + * 将小票书塞入潜影盒 + */ + public void addBookToShulker(ItemStack shulker, ItemStack book) { + try { + org.bukkit.inventory.meta.BlockStateMeta bm = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (bm == null) return; + org.bukkit.block.BlockState st = bm.getBlockState(); + if (st instanceof org.bukkit.block.Container) { + ((org.bukkit.block.Container) st) + .getInventory().addItem(book); + bm.setBlockState(st); + shulker.setItemMeta(bm); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Order] 塞书失败: " + e.getMessage()); + } + } + + // ===== 工具 ===== + private String nn(String s) { return s != null ? s : ""; } + private boolean isExpired(OrderRecord r) { return System.currentTimeMillis() - r.timestamp > RETENTION_30D; } + private String fmtDate(long ts) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts)); } + private int slotIndex(int raw) { for (int i = 0; i < SLOTS.length; i++) if (SLOTS[i] == raw) return i; return -1; } + private String brief(String s) { if (s == null || s.isEmpty()) return "(空)"; String c = sc(s); return c.length() > 30 ? c.substring(0, 30) + "..." : c; } + private String sectionName(String s) { switch(s){case "header":return "头部";case "itemLine":return "商品行";case "discountLine":return "优惠行";case "packLine":return "打包行";case "summary":return "汇总";case "footer":return "页脚";default:return s;} } + private String sc(String s) { + if (s == null) return ""; + return s.replaceAll("§[0-9a-fk-orA-FK-OR]", ""); + } + private String getSection(String s) { switch(s){case "header":return tplHeader;case "itemLine":return tplItemLine;case "discountLine":return tplDiscountLine;case "packLine":return tplPackLine;case "summary":return tplSummary;case "footer":return tplFooter;default:return "";} } + private void setSection(String s, String v) { switch(s){case "header":tplHeader=v;break;case "itemLine":tplItemLine=v;break;case "discountLine":tplDiscountLine=v;break;case "packLine":tplPackLine=v;break;case "summary":tplSummary=v;break;case "footer":tplFooter=v;break;} } + private String getDefaultVal(String s) { switch(s){case "header":return "§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}";case "itemLine":return "§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚";case "discountLine":return "优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})";case "packLine":return "打包: §c+{打包费}枚§r ({打包颜色})";case "summary":return "原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚";case "footer":return "感谢惠顾,欢迎再来!";default:return "";} } + private String getTokenBySlot(int raw) { switch(raw){case 10:return "{品名}";case 11:return "{原价}";case 12:return "{执行价}";case 13:return "{数量}";case 14:return "{小计}";case 15:return "{商店名}";case 16:return "{玩家}";case 19:return "{订单号}";case 20:return "{订单时间}";case 21:return "{打印时间}";case 22:return "{日期}";case 23:return "{时间}";case 24:return "{原价合计}";case 25:return "{实付}";case 28:return "{优惠额}";case 29:return "{优惠类型}";case 30:return "{优惠券码}";case 31:return "{打包费}";case 32:return "{打包颜色}";case 33:return "{余额}";case 37:return "═══";case 38:return "───";case 39:return "═════════════";case 40:return "\\n";default:return null;} } + private ItemStack mkItem(Material mat, String name, String... lore) { ItemStack it = new ItemStack(mat); ItemMeta im = it.getItemMeta(); if (im != null) { im.setDisplayName(name); if (lore.length > 0) im.setLore(Arrays.asList(lore)); it.setItemMeta(im); } return it; } + private Map loadMap(File f) { Map m = new LinkedHashMap<>(); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), 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) m.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); } br.close(); } catch (Exception ignored) {} return m; } + private void writeLines(File f, List lines) { try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); for (String l : lines) pw.println(l); pw.flush(); pw.close(); } catch (Exception ignored) {} } + private void sendRandomPreview(Player p) { String[] n = {"钻石剑","钻石镐","铁剑","金苹果"}; String[] m = {"DIAMOND_SWORD","DIAMOND_PICKAXE","IRON_SWORD","GOLDEN_APPLE"}; Random r = new Random(); List items = new ArrayList<>(); for (int i = 0; i < 2 + r.nextInt(3); i++) { int idx = r.nextInt(n.length); items.add(new OrderItem(n[idx], m[idx], 100 + r.nextInt(5000), 100 + r.nextInt(5000), 1 + r.nextInt(10))); } OrderRecord o = new OrderRecord(); o.orderId = System.currentTimeMillis(); o.uuid = p.getUniqueId(); o.player = p.getName(); o.items = items; o.totalOriginal = 0; for (OrderItem it : items) o.totalOriginal += it.originalSubtotal(); o.totalPaid = o.totalOriginal; o.timestamp = o.orderId; o.status = 0; p.sendMessage("§6§l===== 小票预览(随机) ====="); sendReceiptChat(p, o); p.sendMessage("§6§l===== 预览结束 ====="); } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/PVPManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/PVPManager.java new file mode 100644 index 0000000..326c0fb --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/PVPManager.java @@ -0,0 +1,1380 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scoreboard.*; + + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PVPManager implements Listener { + + private final Main plugin; + private Connection pvpDb; + + private final Map + regions = new ConcurrentHashMap<>(); + private final Map + inRegion = new ConcurrentHashMap<>(); + private final Map + sessionKills = + new ConcurrentHashMap<>(); + private final Map + lastKillTime = + new ConcurrentHashMap<>(); + private final Map + selectState = + new ConcurrentHashMap<>(); + private final Map + killSessions = + new ConcurrentHashMap<>(); + private final Set tripleAnnounced = + ConcurrentHashMap.newKeySet(); + private final Map lastScoreboardRefresh = new ConcurrentHashMap<>(); + + + // 边框显示 + private final Set + permanentBorders = + ConcurrentHashMap.newKeySet(); + private final Set + tempBorders = + ConcurrentHashMap.newKeySet(); + + + public static class KillSession { + public int count = 0; + public long firstKill = 0; + public long lastKill = 0; + public boolean tripleAnnounced = + false; + public String region = ""; + } + + public static class PVPRegion { + public String name; + public String world; + public int x1, y1, z1; + public int x2, y2, z2; + public int minX, minY, minZ; + public int maxX, maxY, maxZ; + + public void calcBounds() { + minX = Math.min(x1, x2); + minY = Math.min(y1, y2); + minZ = Math.min(z1, z2); + maxX = Math.max(x1, x2); + maxY = Math.max(y1, y2); + maxZ = Math.max(z1, z2); + } + + public boolean contains( + Location loc) { + if (loc == null) return false; + if (!loc.getWorld().getName() + .equals(world)) + return false; + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + } + + public PVPManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadRegions(); + } + + // ===== 独立数据库 ===== + + private void initDB() { + try { + Class.forName( + "org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), + "pvp.db"); + pvpDb = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = + pvpDb.createStatement(); + st.execute( + "PRAGMA journal_mode=WAL"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "name TEXT NOT NULL " + + "UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER " + + "DEFAULT 0)"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT " + + "NOT NULL," + + "region_name TEXT " + + "NOT NULL," + + "kills INTEGER " + + "DEFAULT 0," + + "deaths INTEGER " + + "DEFAULT 0," + + "PRIMARY KEY" + + "(player_name, " + + "region_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[PVP] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== DB 操作 ===== + + private void saveRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions" + + " (name, world," + + " x1,y1,z1," + + " x2,y2,z2," + + " create_time)" + + " VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private Map + getRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + private List> + getAllRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + pvpDb.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + private void deleteRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private int getKills(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("kills"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private int getDeaths(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("deaths"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private void addKill(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,1,0) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET kills=" + + "kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private void addDeath(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,0,1) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET deaths=" + + "deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private List> + getTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT player_name," + + " kills,deaths" + + " FROM " + + "pvp_kills " + + "WHERE " + + "region_name=?" + + " AND kills>0" + + " ORDER BY kills" + + " DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = + ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString( + "player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Set getRegionNames() { + return regions.keySet(); + } + + // ===== 加载 ===== + + private void loadRegions() { + regions.clear(); + List> list = + getAllRegions(); + for (Map row : list) { + PVPRegion r = new PVPRegion(); + r.name = (String) row + .get("name"); + r.world = (String) row + .get("world"); + r.x1 = ((Number) row + .get("x1")).intValue(); + r.y1 = ((Number) row + .get("y1")).intValue(); + r.z1 = ((Number) row + .get("z1")).intValue(); + r.x2 = ((Number) row + .get("x2")).intValue(); + r.y2 = ((Number) row + .get("y2")).intValue(); + r.z2 = ((Number) row + .get("z2")).intValue(); + r.calcBounds(); + regions.put(r.name, r); + } + } + + // ===== 工具 ===== + + public ItemStack createTool() { + ItemStack tool = new ItemStack( + Material.BLAZE_ROD); + ItemMeta m = tool.getItemMeta(); + m.setDisplayName( + "§6§l[PVP] 区域选择工具"); + m.setLore(Arrays.asList( + "§7右键:设置第一个角", + "§7左键:设置第二个角")); + m.getPersistentDataContainer() + .set(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING, + "tool"); + tool.setItemMeta(m); + return tool; + } + + private boolean isPVPTool( + ItemStack item) { + if (item == null) return false; + ItemMeta m = item.getItemMeta(); + if (m == null) return false; + return m.getPersistentDataContainer() + .has(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING); + } + + public void giveTool(Player p) { + boolean has = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (isPVPTool(it)) { + has = true; + break; + } + } + if (!has) { + p.getInventory() + .addItem(createTool()); + } + } + + // ===== 边框粒子 ===== + + private void drawBorder(Player target, + PVPRegion r) { + if (r == null) return; + World w = Bukkit.getWorld(r.world); + if (w == null) return; + int step = 2; + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.maxX, r.minY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.minY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.minX, r.minY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.minY, r.minZ, step); + drawLine(target, w, + r.minX, r.maxY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.minZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.maxY, r.maxZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + } + + private void drawLine(Player target, + World w, + int x1, int y1, int z1, + int x2, int y2, int z2, + int step) { + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double dist = Math.sqrt( + dx * dx + dy * dy + dz * dz); + int steps = (int) (dist / step); + if (steps < 1) steps = 1; + for (int i = 0; i <= steps; i++) { + double t = (double) i / steps; + Location loc = new Location(w, + x1 + dx * t, + y1 + dy * t + 0.1, + z1 + dz * t); + w.spawnParticle( + Particle.FLAME, loc, 1, + 0.0, 0.0, 0.0, 0.0); + } + } + + // ===== 每秒 tick ===== + + public void tickRegions() { + long now = + System.currentTimeMillis(); + + // 玩家区域检测 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pn = p.getName(); + Location loc = p.getLocation(); + String inside = null; + for (PVPRegion r : + regions.values()) { + if (r.contains(loc)) { + inside = r.name; + break; + } + } + String was = inRegion.get(pn); + + if (inside != null && !inside.equals(was)) { + inRegion.put(pn, inside); + if (was == null) { + String reg = inside; + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> + updateScoreboard( + pp, reg), + 1L); + } + + } else if (inside == null + && was != null) { + inRegion.remove(pn); + // 不删 sessionKills,等倒计时结束再清 + removeScoreboard(p); + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> plugin + .giveMenuSnowball(pp), + 5L); + } + + } + + // 5分钟超时重置连杀session + for (String pname : + new ArrayList<>( + killSessions + .keySet())) { + KillSession ks = + killSessions.get(pname); + if (ks == null) continue; + if (now - ks.lastKill + > 5 * 60 * 1000) { + killSessions.remove(pname); + } + } + + // 5分钟无击杀 → 计数榜切总榜 + // 5分钟无击杀 → 归档并清零 + for (String rName : + new ArrayList<>( + lastKillTime + .keySet())) { + long last = lastKillTime + .getOrDefault(rName, 0L); + if (now - last > 5 * 60 * 1000) { + // 清零该区域所有sessionKills + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + rName + ":")); + // 切换到总榜 + refreshScoreboard(rName); + lastKillTime.remove(rName); + lastScoreboardRefresh.remove(rName); + + } + } + + + // 边框渲染 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (pr == null) continue; + PVPRegion r = regions.get(pr); + if (r == null) continue; + if (permanentBorders + .contains(pr)) { + drawBorder(p, r); + } + if (tempBorders + .contains(pr)) { + drawBorder(p, r); + } + // 每秒刷新活跃区域的计分板倒计时 + for (String rName : new ArrayList<>(lastKillTime.keySet())) { + long last = lastScoreboardRefresh.getOrDefault(rName, 0L); + if (now - last >= 1000) { + lastScoreboardRefresh.put(rName, now); + for (Player tp : Bukkit.getOnlinePlayers()) { + String inR = inRegion.get(tp.getName()); + if (rName.equals(inR)) { + showCounter(tp, rName); + } + } + } + } + + + + } + } + + private void broadcastWithSound(String msg, Sound sound) { + Bukkit.broadcastMessage(msg); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), sound, 1.0f, 1.0f); + } + } + + + private void initSession(String player, + String region) { + killSessions.remove(player); + tripleAnnounced.remove( + region + ":" + player); + KillSession s = new KillSession(); + s.region = region; + s.firstKill = + System.currentTimeMillis(); + s.lastKill = + System.currentTimeMillis(); + killSessions.put(player, s); + } + + // ===== 击杀事件 ===== + + @EventHandler + public void onDeath( + EntityDeathEvent event) { + if (!(event.getEntity() + instanceof Player)) return; + Player victim = + (Player) event.getEntity(); + Player killer = + victim.getKiller(); + if (killer == null) return; + if (killer.equals(victim)) return; + + String region = + inRegion.get( + victim.getName()); + if (region == null) return; + + String vName = victim.getName(); + String kName = killer.getName(); + + // 总榜 + addKill(kName, region); + addDeath(vName, region); + + // 计数榜 + String kKey = + region + ":" + kName; + sessionKills.merge(kKey, 1, + Integer::sum); + lastKillTime.put(region, + System.currentTimeMillis()); + + // 终结检测 + // 终结检测 + KillSession victimSession = + killSessions.get(vName); + if (victimSession != null + && victimSession.count >= 3) { + String endMsg = + "§b§l" + kName + + " §e终结了 " + + vName + " §e的 " + + victimSession.count + + "连杀!§a恭喜终结者 " + + kName + + "§e!§7快来挑战!"; + killSessions.remove(vName); + broadcastWithSound(endMsg, Sound.ENTITY_ENDER_DRAGON_GROWL); + + killSessions.remove(vName); + } + + + // 击杀者连杀 + KillSession ks = + killSessions.get(kName); + if (ks == null + || !ks.region.equals(region) + || System.currentTimeMillis() + - ks.lastKill > 5 * 60 * 1000) { + tripleAnnounced.remove( + region + ":" + kName); + KillSession ns = + new KillSession(); + ns.region = region; + ns.count = 1; + ns.firstKill = + System.currentTimeMillis(); + ns.lastKill = + System.currentTimeMillis(); + killSessions.put(kName, ns); + } else { + ks.count++; + ks.lastKill = + System.currentTimeMillis(); + } + + // 多杀通报 + KillSession killerSession = + killSessions.get(kName); + if (killerSession != null) { + plugin.getLogger().info( + "[PVP] checkMultiKill: " + + kName + " count=" + + killerSession.count + + " region=" + region); + checkMultiKill(killer, + region, killerSession); + } + + // 刷新计分板 + for (Player p : + Bukkit.getOnlinePlayers()) { + if (region.equals( + inRegion.get( + p.getName()))) { + updateScoreboard(p, region); + } + } + + victim.sendMessage("§c你在「" + + region + "」被 " + + kName + " 击杀"); + killer.sendMessage("§a你在「" + + region + "」击杀了 " + + vName); + } + + private void checkMultiKill( + Player killer, String region, + KillSession session) { + int count = session.count; + String kName = killer.getName(); + String key = region + ":" + kName; + String msg = null; + + if (count >= 5) { + msg = "§c§l" + kName + + " §e在「" + region + + "」达成 " + count + + "连杀!§7§l快来挑战!"; + } else if (count == 3) { + if (tripleAnnounced.add(key)) { + msg = "§6§l三杀!" + + kName + + " 势不可挡!" + + "§7快来挑战!"; + } + } + + if (msg != null) { + if (count >= 5) { + broadcastWithSound(msg, Sound.ENTITY_ENDER_DRAGON_GROWL); + } else { + broadcastWithSound(msg, Sound.BLOCK_END_PORTAL_SPAWN); + } + } + + } + + + + // ===== 计分板(英文ID) ===== + + private void ensureScoreboard( + Player p, String region) { + // 活跃击杀:每秒强制刷新(更新倒计时) + if (lastKillTime.containsKey(region)) { + showCounter(p, region); + return; + } + // 无活跃击杀:只在没有scoreboard时创建总榜 + Scoreboard sb = p.getScoreboard(); + if (sb == null + || sb.getObjective( + "sdf1pvp") == null) { + showTotal(p, region); + } + } + + + private void updateScoreboard( + Player p, String region) { + if (lastKillTime + .containsKey(region)) { + showCounter(p, region); + } else { + showTotal(p, region); + } + } + + private void showTotal(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = + mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§6§l击杀总榜 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List> top = + getTop(region, 10); + if (top.isEmpty()) { + obj.getScore("§7暂无记录") + .setScore(0); + } else { + int rank = 10; + for (Map row : + top) { + String name = (String) row + .get("player_name"); + int kills = ((Number) row + .get("kills")) + .intValue(); + int deaths = ((Number) row + .get("deaths")) + .intValue(); + String line = + "§e#" + (11 - rank) + + " §f" + name + + " §a" + kills + + "§7/§c" + deaths; + obj.getScore(line) + .setScore(rank); + rank--; + } + } + p.setScoreboard(sb); + } + private void showCounter(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§c§l实时战况 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List entries = + new ArrayList<>(); + for (Map.Entry e : + sessionKills.entrySet()) { + if (!e.getKey().startsWith( + region + ":")) + continue; + if (e.getValue() <= 0) continue; + String pName = e.getKey() + .substring( + region.length() + 1); + entries.add(new String[]{ + pName, + String.valueOf( + e.getValue())}); + } + + if (entries.isEmpty()) { + obj.getScore("§7等待击杀...") + .setScore(1); + } else { + entries.sort((a, b) -> + Integer.compare( + Integer.parseInt(b[1]), + Integer.parseInt(a[1]))); + int rank = 10; + for (String[] entry : entries) { + obj.getScore( + "§e" + entry[0] + + " §a" + entry[1]) + .setScore(rank); + rank--; + if (rank < 1) break; + } + } + + Long last = + lastKillTime.get(region); + if (last != null) { + long elapsed = + System.currentTimeMillis() + - last; + long remaining = + Math.max(0, + 5 * 60 * 1000 - elapsed); + long secs = remaining / 1000; + obj.getScore( + "§7冷却: " + secs + "s") + .setScore(-1); + } + + // 关键:强制赋值新scoreboard + p.setScoreboard(sb); + } + + private void removeScoreboard( + Player p) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + p.setScoreboard( + mgr.getNewScoreboard()); + } + + private void refreshScoreboard( + String regionName) { + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (regionName.equals(pr)) { + showTotal(p, regionName); + } + } + } + + // ===== 工具选择事件 ===== + + @EventHandler + public void onInteract( + PlayerInteractEvent event) { + Player p = event.getPlayer(); + ItemStack hand = + p.getInventory() + .getItemInMainHand(); + if (!isPVPTool(hand)) return; + + if (!event.getAction() + .isRightClick() + && !event.getAction() + .isLeftClick()) return; + + Location loc = p.getLocation(); + String[] state = + selectState.get(p.getName()); + if (state == null) { + state = new String[]{ + loc.getWorld() + .getName(), + "", ""}; + selectState.put( + p.getName(), state); + } + + String coord = + loc.getBlockX() + "," + + loc.getBlockY() + "," + + loc.getBlockZ(); + + if (event.getAction() + .isRightClick()) { + state[0] = loc.getWorld() + .getName(); + state[1] = coord; + p.sendMessage( + "§a第一个角: " + coord); + } else { + state[2] = coord; + p.sendMessage( + "§a第二个角: " + coord); + } + event.setCancelled(true); + } + + // ===== 退出清理 ===== + + @EventHandler + public void onQuit( + PlayerQuitEvent event) { + String name = + event.getPlayer().getName(); + inRegion.remove(name); + selectState.remove(name); + } + + // ===== 指令 ===== + + public boolean onCommand(Player p, + String[] args) { + if (args.length == 0) { + return showStats(p, + p.getName()); + } + switch (args[0].toLowerCase()) { + case "create": + return handleCreate( + p, args); + case "stats": + if (args.length >= 2) + return showStats(p, + args[1]); + return showStats(p, + p.getName()); + case "list": + return handleList(p); + case "delete": + if (args.length >= 2) + return handleDelete(p, + args[1]); + p.sendMessage( + "§c用法: " + + "/pvp delete <名字>"); + return true; + case "tool": + giveTool(p); + p.sendMessage( + "§a已发放" + + "PVP区域选择工具"); + return true; + case "on": + return handleBorderOn( + p, args); + case "off": + return handleBorderOff( + p, args); + case "tempban": + return handleTempBan( + p, args); + default: + return showStats(p, + p.getName()); + } + } + + private boolean handleBorderOn( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp on <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + permanentBorders.add(name); + p.sendMessage("§a已开启「" + + name + "」永久边框显示"); + return true; + } + + private boolean handleBorderOff( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp off <区域名>"); + return true; + } + permanentBorders.remove(name); + tempBorders.remove(name); + p.sendMessage("§a已关闭「" + + name + "」边框显示"); + return true; + } + + private boolean handleTempBan( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp tempban <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + tempBorders.add(name); + p.sendMessage("§a「" + name + + "」临时边框显示15秒"); + final String rName = name; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> + tempBorders + .remove(rName), + 300L); + return true; + } + + private boolean handleCreate( + Player p, String[] args) { + if (args.length >= 8) { + try { + int x1 = Integer.parseInt( + args[1]); + int y1 = Integer.parseInt( + args[2]); + int z1 = Integer.parseInt( + args[3]); + int x2 = Integer.parseInt( + args[4]); + int y2 = Integer.parseInt( + args[5]); + int z2 = Integer.parseInt( + args[6]); + String rName = args[7]; + return createRegion(p, + rName, + p.getWorld() + .getName(), + x1, y1, z1, + x2, y2, z2); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c坐标格式错误"); + return true; + } + } + + if (args.length >= 2) { + String[] state = + selectState + .get(p.getName()); + if (state == null + || state[1].isEmpty() + || state[2].isEmpty()) { + p.sendMessage( + "§c请先用工具选择两个角"); + return true; + } + String[] p1 = + state[1].split(","); + String[] p2 = + state[2].split(","); + return createRegion(p, + args[1], state[0], + Integer.parseInt(p1[0]), + Integer.parseInt(p1[1]), + Integer.parseInt(p1[2]), + Integer.parseInt(p2[0]), + Integer.parseInt(p2[1]), + Integer.parseInt(p2[2])); + } + + p.sendMessage( + "§c用法: /pvp create " + + " " + + " <名字>"); + p.sendMessage( + "§c或: /pvp create " + + "<名字> (先用工具选择)"); + return true; + } + + private boolean createRegion( + Player p, String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + if (getRegion(name) != null) { + p.sendMessage( + "§c区域已存在: " + name); + return true; + } + PVPRegion r = new PVPRegion(); + r.name = name; + r.world = world; + r.x1 = x1; + r.y1 = y1; + r.z1 = z1; + r.x2 = x2; + r.y2 = y2; + r.z2 = z2; + r.calcBounds(); + saveRegion(name, world, + x1, y1, z1, + x2, y2, z2); + regions.put(name, r); + p.sendMessage( + "§a§lPVP区域创建成功!"); + p.sendMessage( + "§7名称: §f" + name); + p.sendMessage("§7范围: §f" + + r.minX + "," + r.minY + + "," + r.minZ + " → " + + r.maxX + "," + r.maxY + + "," + r.maxZ); + selectState.remove(p.getName()); + return true; + } + + private boolean handleList( + Player p) { + List> all = + getAllRegions(); + if (all.isEmpty()) { + p.sendMessage( + "§7暂无PVP区域"); + return true; + } + p.sendMessage( + "§6§lPVP区域列表:"); + for (Map row : + all) { + p.sendMessage("§e- §f" + + row.get("name") + + " §7(" + row.get("world") + + ")"); + } + return true; + } + + private boolean handleDelete( + Player p, String name) { + if (getRegion(name) == null) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + deleteRegion(name); + regions.remove(name); + permanentBorders.remove(name); + tempBorders.remove(name); + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + name + ":")); + lastKillTime.remove(name); + lastScoreboardRefresh.remove(name); + p.sendMessage( + "§a已删除: " + name); + return true; + } + + private boolean showStats(Player p, + String target) { + String region = + inRegion.get(target); + if (region == null) { + p.sendMessage("§6§l" + target + + " 的PVP战绩:"); + List> all = + getAllRegions(); + boolean has = false; + for (Map row : + all) { + String rn = (String) row + .get("name"); + int kills = + getKills(target, rn); + int deaths = + getDeaths(target, rn); + if (kills > 0 + || deaths > 0) { + p.sendMessage( + "§e- §f" + rn + + " §a击杀:" + kills + + " §c死亡:" + deaths); + has = true; + } + } + if (!has) + p.sendMessage( + "§7暂无记录"); + } else { + int kills = + getKills(target, region); + int deaths = + getDeaths(target, region); + double kd = deaths > 0 + ? (double) kills / deaths + : kills; + p.sendMessage("§6§l" + target + + " @ " + region); + p.sendMessage( + "§a击杀: " + kills + + " §c死亡: " + deaths + + " §eKD: " + + String.format( + "%.2f", kd)); + } + return true; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/PasswordUtils.java b/backup/20260605_152529/src/main/java/Sdf1_login/PasswordUtils.java new file mode 100644 index 0000000..4029ed4 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/PasswordUtils.java @@ -0,0 +1,72 @@ +package Sdf1_login; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtils { + + private static final SecureRandom RANDOM = + new SecureRandom(); + + public static String generateSalt() { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hash(String password, String salt) { + try { + MessageDigest md = + MessageDigest.getInstance("SHA-256"); + md.update(Base64.getDecoder().decode(salt)); + byte[] hash = + md.digest(password.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean validate(String password) { + if (password == null) return false; + if (password.length() < 6 || password.length() > 20) + return false; + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + return hasUpper && hasLower && hasDigit; + } + + public static String generateTempPassword() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(8); + for (int i = 0; i < 8; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateInviteCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateVerifyCode() { + int code = 100000 + RANDOM.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/PlayerStorage.java b/backup/20260605_152529/src/main/java/Sdf1_login/PlayerStorage.java new file mode 100644 index 0000000..f93caa3 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/PlayerStorage.java @@ -0,0 +1,130 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.logging.Logger; + +public class PlayerStorage { + private Connection conn; + private final Logger logger; + + public PlayerStorage(File dataFolder, + Logger logger) { + this.logger = logger; + File dbFile = new File( + dataFolder, + "quest_data.db"); + try { + Class.forName( + "org.sqlite.JDBC"); + conn = DriverManager + .getConnection( + "jdbc:sqlite:" + + dbFile + .getAbsolutePath()); + conn.setAutoCommit(true); + Statement s = + conn.createStatement(); + s.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_kv (" + + "player TEXT NOT NULL," + + "key TEXT NOT NULL," + + "value TEXT," + + "PRIMARY KEY" + + "(player, key))"); + s.close(); + logger.info( + "[QuestStorage] 初始化: " + + dbFile.getAbsolutePath()); + } catch (Exception e) { + logger.severe( + "[QuestStorage] 失败: " + + e.getMessage()); + } + } + + public Object getCondition( + String player, String key) { + if (conn == null) return null; + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value " + + "FROM quest_kv " + + "WHERE player=? " + + "AND key=?"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ResultSet rs = + ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") + : null; + rs.close(); + ps.close(); + return parseValue(val); + } catch (SQLException e) { + return null; + } + } + + public void setCondition( + String player, String key) { + setConditionRaw( + player, key, "true"); + } + public void debugPlayer(String player) { + + } + public void setConditionRaw( + String player, String key, + String value) { + if (conn == null) return; + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO quest_kv" + + "(player,key,value)" + + " VALUES(?,?,?)"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ps.setString(3, value); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + logger.warning( + "[QuestStorage] 写入失败: " + + key + "=" + value + + " | " + e.getMessage()); + } + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) + conn.close(); + } catch (Exception e) {} + } + + private Object parseValue(String v) { + if (v == null) return null; + try { + return Long.parseLong(v); + } catch (Exception e1) { + try { + return Double.parseDouble(v); + } catch (Exception e2) { + if ("true".equalsIgnoreCase(v)) + return true; + if ("false".equalsIgnoreCase(v)) + return false; + return v; + } + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/PointsManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/PointsManager.java new file mode 100644 index 0000000..77fc195 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/PointsManager.java @@ -0,0 +1,374 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +import java.lang.reflect.Method; +import java.util.*; + +public class PointsManager { + + private final Main plugin; + private Object cyInstance = null; + private Method cyActivationMethod = null; + private Method cyGetShopItemsMethod = null; + + private final List> cyItems = + new ArrayList<>(); + + public PointsManager(Main plugin) { + this.plugin = plugin; + discoverCY(); + } + + private void discoverCY() { + try { + org.bukkit.plugin.Plugin p = Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (p != null && p.isEnabled()) { + cyInstance = p; + cyGetShopItemsMethod = p.getClass() + .getMethod("onSdf1GetShopItems"); + cyActivationMethod = p.getClass() + .getMethod("onSdf1Activation", + String.class, + int.class, int.class); + plugin.getLogger().info( + "[Sdf1_login] CY_beibao 联控已连接"); + } + } catch (Exception ignored) { + } + } + + private void refreshCYItems() { + cyItems.clear(); + if (cyInstance == null + || cyGetShopItemsMethod == null) return; + try { + Object result = + cyGetShopItemsMethod.invoke(cyInstance); + if (result == null) return; + String data = result.toString(); + if (data.isEmpty()) return; + String[] items = data.split(";"); + for (String itemStr : items) { + String[] p = itemStr.split("\\|"); + if (p.length < 8) continue; + Map item = + new LinkedHashMap<>(); + item.put("id", p[0]); + item.put("name", p[1]); + item.put("days", + Integer.parseInt(p[2])); + item.put("slots", + Integer.parseInt(p[3])); + item.put("price", + Integer.parseInt(p[4])); + item.put("stock", + Integer.parseInt(p[5])); + item.put("icon", p[6]); + item.put("lifetime", + "1".equals(p[7])); + cyItems.add(item); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 拉取CY商品失败: " + + e.getMessage()); + } + } + + private void activateCY(String name, + int slots, int days) { + try { + if (cyInstance == null + || cyActivationMethod == null) return; + cyActivationMethod.invoke( + cyInstance, name, slots, days); + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] CY激活失败: " + + e.getMessage()); + } + } + + public int getPoints(String name) { + return ((Number) plugin.getDb() + .getField(name, "points")).intValue(); + } + + public Inventory createShopGUI(Player p) { + int points = getPoints(p.getName()); + Inventory inv = Bukkit.createInventory( + null, 54, + "§d§l积分商城 | 当前积分: " + points); + fillBg(inv); + inv.setItem(10, mkItem(Material.EMERALD, + "§a游戏币充值", + "§7消耗积分兑换游戏币", + "§7100积分 = $1000", + "", + "§e点击兑换")); + inv.setItem(12, mkItem(Material.ENDER_CHEST, + "§b背包空间", + "§7消耗积分兑换背包格子", + "§750积分 = 9格永久空间", + "", + "§e点击兑换")); + inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, + "§e经验瓶", + "§7消耗积分兑换经验", + "§720积分 = 10级经验", + "", + "§e点击兑换")); + if (cyInstance != null) { + inv.setItem(31, mkItem(Material.CHEST, + "§6背包商城", + "§7来自CY_beibao的商品", + "", + "§e点击查看")); + } + inv.setItem(49, mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public Inventory createCYShopGUI(Player p) { + refreshCYItems(); + int points = getPoints(p.getName()); + int size = Math.max(54, + ((cyItems.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory inv = Bukkit.createInventory( + null, size, + "§d§lCY背包商城 | 当前积分: " + points); + fillBg(inv); + for (int i = 0; i < cyItems.size() + && i < 45; i++) { + Map item = cyItems.get(i); + String name = (String) item.get("name"); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + List lore = new ArrayList<>(); + lore.add("§7价格: §e" + price + " 积分"); + if (lifetime) { + lore.add("§7格子: §e终身 +" + slots); + } else { + lore.add("§7格子: §e+" + slots); + lore.add("§7时间: §e" + days + "天"); + } + if (stock == 0) { + lore.add("§7库存: §a无限"); + } else if (stock == -1) { + lore.add("§7库存: §c已下架"); + } else { + lore.add("§7库存: §e" + stock); + } + lore.add(""); + if (points >= price && stock != -1) { + lore.add("§e点击购买"); + } else if (stock == -1) { + lore.add("§c已下架"); + } else { + lore.add("§c积分不足"); + } + inv.setItem(i, mkItem(Material.CHEST, + "§a" + name, + lore.toArray(new String[0]))); + } + inv.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public void addItem(String name, + int price, String cmd2) { + // 追加到商品配置文件 + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.io.FileWriter fw = + new java.io.FileWriter(file, true); + fw.write(name + ":" + price + + ":" + cmd2 + "\n"); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void removeItem(int index) { + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.util.List lines = + new java.util.ArrayList<>(); + java.io.BufferedReader br = + new java.io.BufferedReader( + new java.io.FileReader( + file)); + String line; + int i = 0; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty() + || line.startsWith("#")) { + lines.add(line); + continue; + } + if (i != index) { + lines.add(line); + } + i++; + } + br.close(); + java.io.PrintWriter pw = + new java.io.PrintWriter(file); + for (String l : lines) pw.println(l); + pw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void handleClick(Player p, int slot) { + int points = getPoints(p.getName()); + switch (slot) { + case 10: + if (points >= 100) { + plugin.getDb().deductPoints( + p.getName(), 100); + if (plugin.getEconomy() != null) { + plugin.getEconomy() + .depositPlayer(p, 1000); + } + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "100")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 12: + if (points >= 50) { + plugin.getDb().deductPoints( + p.getName(), 50); + activateCY(p.getName(), 9, 0); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "50")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 14: + if (points >= 20) { + plugin.getDb().deductPoints( + p.getName(), 20); + p.setLevel(p.getLevel() + 10); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "20")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 31: + if (cyInstance != null) { + p.openInventory( + createCYShopGUI(p)); + } + break; + } + } + + public void handleCYClick(Player p, int slot) { + if (slot < 0 || slot >= cyItems.size() + || slot >= 45) return; + Map item = cyItems.get(slot); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + if (stock == -1) { + p.sendMessage("§c该商品已下架"); + return; + } + int points = getPoints(p.getName()); + if (points < price) { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + return; + } + + plugin.getDb().deductPoints(p.getName(), price); + int actDays = lifetime ? 0 : days; + activateCY(p.getName(), slots, actDays); + + String tl = lifetime + ? "终身" : (days + "天"); + p.sendMessage("§a购买成功!获得 " + + tl + " +" + slots + "格 背包空间"); + } + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/QuestStorage.java b/backup/20260605_152529/src/main/java/Sdf1_login/QuestStorage.java new file mode 100644 index 0000000..45dbabf --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/QuestStorage.java @@ -0,0 +1,138 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class QuestStorage { + + private Connection conn; + + public QuestStorage(File dataFolder) { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + dataFolder, + "quest_data.db"); + conn = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getPath()); + initTable(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void initTable() + throws SQLException { + Statement stmt = + conn.createStatement(); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY " + + "(player, condition_key))"); + stmt.close(); + } + + /** + * 标记条件已完成(value = "1") + */ + public void setCondition( + String player, + String condition) { + setConditionRaw( + player, condition, "1"); + } + + /** + * 写入任意值(计数器等) + */ + public void setConditionRaw( + String player, + String condition, + String value) { + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO " + + "quest_conditions " + + "(player, " + + "condition_key, " + + "value, timestamp) " + + "VALUES " + + "(?, ?, ?, ?)"); + ps.setString(1, player); + ps.setString(2, condition); + ps.setString(3, value); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 查询条件值 + */ + public Object getCondition( + String player, + String condition) { + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value FROM " + + "quest_conditions " + + "WHERE player = ? " + + "AND " + + "condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + String val = + rs.getString("value"); + rs.close(); + ps.close(); + // 尝试解析为数字 + try { + if (val.contains(".")) { + return Double.parseDouble( + val); + } + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/QuestTracker.java b/backup/20260605_152529/src/main/java/Sdf1_login/QuestTracker.java new file mode 100644 index 0000000..f2eda67 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/QuestTracker.java @@ -0,0 +1,3662 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final QuestStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + private static boolean loggedStructureRegistry + = false; + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new QuestStorage( + plugin.getDataFolder()); + + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 岩岸/沙滩 + BIOME_CATEGORY.put("minecraft:stony_shore", + "石岸"); +// 海岸 + BIOME_CATEGORY.put("minecraft:mushroom_fields", + "蘑菇岛"); +// 苍白之园 + BIOME_CATEGORY.put( + "minecraft:pale_garden", "森林"); +// 末地小型岛屿 + BIOME_CATEGORY.put( + "minecraft:small_end_islands", "末地"); +// 主世界结构归类 + BIOME_CATEGORY.put("minecraft:village", "平原"); + BIOME_CATEGORY.put( + "minecraft:desert_pyramid", "沙漠"); + BIOME_CATEGORY.put( + "minecraft:jungle_pyramid", "森林"); + BIOME_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + + + } + + // 所有群系大类列表 + private static final String[] + ALL_CATEGORIES = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "石岸", "蘑菇岛", "地下", + "下界", "末地" + }; + + // ===== 结构映射(PaperMC 1.21.4 实际结构 key)===== + private static final Map + STRUCTURE_CATEGORY = + new HashMap<>(); + static { + // 主世界结构 + STRUCTURE_CATEGORY.put( + "minecraft:village_plains", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_desert", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_savanna", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_snowy", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_taiga", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:desert_pyramid", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:jungle_pyramid", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + STRUCTURE_CATEGORY.put( + "minecraft:igloo", "雪原"); + STRUCTURE_CATEGORY.put( + "minecraft:mansion", "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:pillager_outpost", + "掠夺者前哨站"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft_mesa", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:trail_ruins", + "远古遗迹"); + STRUCTURE_CATEGORY.put( + "minecraft:trial_chambers", + "试炼密室"); + STRUCTURE_CATEGORY.put( + "minecraft:monument", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_cold", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_warm", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck_beached", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:buried_treasure", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_desert", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_jungle", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_mountain", + "山地"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_ocean", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_swamp", + "沼泽"); + // 下界结构 + STRUCTURE_CATEGORY.put( + "minecraft:fortress", "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:bastion_remnant", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:nether_fossil", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_nether", + "下界"); + // 末地结构 + STRUCTURE_CATEGORY.put( + "minecraft:end_city", "末地"); + } + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + CATEGORY_KEYWORDS.put("石岸", "石岸"); + CATEGORY_KEYWORDS.put("沙滩", "石岸"); + CATEGORY_KEYWORDS.put("海岸", "石岸"); + + // ===== 结构名称关键词映射 ===== + // 主世界结构 + CATEGORY_KEYWORDS.put("村庄", "村庄"); + CATEGORY_KEYWORDS.put("沙漠神殿", "沙漠"); + CATEGORY_KEYWORDS.put("丛林神庙", "森林"); + CATEGORY_KEYWORDS.put("沼泽小屋", "沼泽"); + CATEGORY_KEYWORDS.put("雪屋", "雪原"); + CATEGORY_KEYWORDS.put("林地府邸", "森林"); + CATEGORY_KEYWORDS.put("掠夺者前哨站", "平原"); + CATEGORY_KEYWORDS.put("废弃矿井", "地下"); + CATEGORY_KEYWORDS.put("要塞", "地下"); + CATEGORY_KEYWORDS.put("远古城市", "地下"); + CATEGORY_KEYWORDS.put("古迹废墟", "地下"); + CATEGORY_KEYWORDS.put("试炼密室", "地下"); + CATEGORY_KEYWORDS.put("海底废墟", "海洋"); + CATEGORY_KEYWORDS.put("沉船", "海洋"); + CATEGORY_KEYWORDS.put("埋藏的宝箱", "海洋"); + CATEGORY_KEYWORDS.put("废弃传送门", "主世界"); + CATEGORY_KEYWORDS.put("化石", "主世界"); + + // 下界结构 + CATEGORY_KEYWORDS.put("下界要塞", "下界"); + CATEGORY_KEYWORDS.put("猪灵堡垒", "下界"); + CATEGORY_KEYWORDS.put("玄武岩结构", "下界"); + + // 末地结构 + CATEGORY_KEYWORDS.put("末地城", "末地"); + CATEGORY_KEYWORDS.put("末地船", "末地"); + + // 下界相关别名 + CATEGORY_KEYWORDS.put("地狱要塞", "下界"); + CATEGORY_KEYWORDS.put("下界堡垒", "下界"); + + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 优先用世界名判断维度 + if (worldName != null) { + String w = + worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + + // 用群系映射表 + if (rawKey != null) { + String lower = + rawKey.toLowerCase(); + // 带 minecraft: 前缀的直接查 + String cat = + BIOME_CATEGORY.get(lower); + if (cat != null) + return cat; + + // 不带前缀的尝试补全 + String prefixed = + "minecraft:" + lower; + cat = BIOME_CATEGORY.get(prefixed); + if (cat != null) + return cat; + + // 模糊匹配群系名 + for (Map.Entry entry + : BIOME_CATEGORY.entrySet()) { + if (entry.getKey() + .contains(lower) + || lower.contains( + entry.getKey() + .replace( + "minecraft:", + ""))) { + return entry.getValue(); + } + } + } + + return "主世界"; + } + + // ========== 结构检测 ========== + + /** + * 检测指定位置是否存在自然生成的结构 + * 返回结构分类(如"村庄"、"沙漠"等),未找到返回null + * 使用 PaperMC World.hasStructureAt() + Registry.STRUCTURE API + */ + private String detectStructureAt( + String worldName, double x, + double y, double z) { + try { + World w = Bukkit.getWorld(worldName); + if (w == null) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测: 世界 '" + + worldName + "' 不存在"); + return null; + } + + int bx = (int) Math.floor(x); + int by = (int) Math.floor(y); + int bz = (int) Math.floor(z); + + // 首次调用时打印所有注册表结构键 + if (!loggedStructureRegistry) { + plugin.getLogger().info( + "[QuestTracker] === 结构注册表诊断 ==="); + int count = 0; + for (Structure s : Registry.STRUCTURE) { + NamespacedKey key = + Registry.STRUCTURE.getKey(s); + plugin.getLogger().info( + "[QuestTracker] 注册表结构 #" + + count + ": " + key); + count++; + } + plugin.getLogger().info( + "[QuestTracker] 注册表共 " + + count + " 个结构"); + plugin.getLogger().info( + "[QuestTracker] === 诊断结束 ==="); + loggedStructureRegistry = true; + } + + // 第1步: 精确位置检查 + io.papermc.paper.math.Position exactPos = + io.papermc.paper.math.Position.block( + bx, by, bz); + for (Structure structure : Registry.STRUCTURE) { + boolean hasAt = + w.hasStructureAt(exactPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY.get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(精确): " + + keyStr + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + + // 第2步: Y轴偏移检查 (-5 到 +10) + for (int dy = -5; dy <= 10; dy++) { + if (dy == 0) continue; + int testY = by + dy; + io.papermc.paper.math.Position testPos = + io.papermc.paper.math.Position.block( + bx, testY, bz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + testPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(Y偏移" + + dy + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + } + + // 第3步: 水平偏移检查 (±1格) + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; + io.papermc.paper.math.Position offsetPos = + io.papermc.paper.math.Position + .block(bx + dx, by, + bz + dz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + offsetPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey( + structure); + String keyStr = (key != null) + ? key.toString() + : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(偏移" + + dx + "," + + dz + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat + : "未映射")); + if (cat != null) return cat; + } + } + } + } + + plugin.getLogger().info( + "[QuestTracker] 所有位置检查完毕, " + + "未匹配任何结构 (pos=" + + bx + "," + by + "," + bz + + ")"); + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测异常: " + + ex.getClass().getSimpleName() + + ": " + ex.getMessage()); + ex.printStackTrace(); + } + return null; + } + + /** + * 将结构中文名映射到对应的大类 + * 用于匹配任务条件中的结构名 + */ + private String mapStructureCategory( + String structureName) { + if (structureName == null) return ""; + + // 主世界结构 + if (structureName.contains("村庄")) + return "村庄"; + if (structureName.contains("沙漠神殿")) + return "沙漠"; + if (structureName.contains("丛林神庙") || structureName.contains("丛林神殿")) + return "森林"; + if (structureName.contains("沼泽小屋")) + return "沼泽"; + if (structureName.contains("雪屋") || structureName.contains("冰屋")) + return "雪原"; + if (structureName.contains("林地府邸")) + return "森林"; + if (structureName.contains("掠夺者前哨站")) + return "掠夺者前哨站"; + if (structureName.contains("废弃矿井") || structureName.contains("恶地废弃矿井")) + return "地下"; + if (structureName.contains("要塞")) + return "地下"; + if (structureName.contains("远古城市")) + return "地下"; + if (structureName.contains("远古遗迹") || structureName.contains("古迹废墟")) + return "远古遗迹"; + if (structureName.contains("试炼密室")) + return "试炼密室"; + if (structureName.contains("海底神殿") || structureName.contains("海洋神殿")) + return "海洋"; + if (structureName.contains("海底废墟") || structureName.contains("冻洋废墟") || structureName.contains("暖洋废墟")) + return "海洋"; + if (structureName.contains("沉船")) + return "海洋"; + if (structureName.contains("埋藏的宝箱") || structureName.contains("埋藏的宝藏")) + return "海洋"; + if (structureName.contains("废弃传送门")) + return "平原"; + if (structureName.contains("化石")) + return "主世界"; + + // 下界结构 + if (structureName.contains("下界要塞")) + return "下界"; + if (structureName.contains("猪灵堡垒") || structureName.contains("堡垒遗迹")) + return "下界"; + if (structureName.contains("玄武岩结构") || structureName.contains("下界化石")) + return "下界"; + + // 末地结构 + if (structureName.contains("末地城")) + return "末地"; + if (structureName.contains("末地船")) + return "末地"; + + return ""; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API或债券 + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage("§c无可领取的奖励"); + return; + } + + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) { + player.sendMessage( + "§7[可选奖励] 需完成所有可选条件: " + rt); + continue; + } + } + + // 按 + 分割子项 + String[] parts = rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } + } + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory().addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); + } + + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + public String getProgress( + String player, String rawCond) { + if (player == null || rawCond == null) + return "§7未知"; + String key = stripOptional(rawCond); + + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + + cur + "/1"; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "VILLAGER_TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + + } + + // ===== 击杀泛解析(支持中文列表) ===== +// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等 + if (key.contains("击杀") && key.contains("只")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + String mobPart = key; + java.util.regex.Matcher killM = + java.util.regex.Pattern + .compile("击杀\\d+只(.+)") + .matcher(key); + if (killM.find()) { + mobPart = killM.group(1).trim(); + } + + String[] mobNames = mobPart + .split("[、,,,]+"); + int totalKills = 0; + StringBuilder names = new StringBuilder(); + for (String name : mobNames) { + String trimmed = name.trim(); + if (trimmed.isEmpty()) continue; + String entityType = + chineseMobToEntity(trimmed); + if (!entityType.isEmpty()) { + totalKills += getCounter( + player, + "KILL:" + entityType); + if (names.length() > 0) + names.append("/"); + names.append(trimmed); + } + } + return "§7击杀" + names + ": §e" + + totalKills + "/" + needed; + } + +// 单一KILL:xxx:N 格式(原有逻辑保留) + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + int needed = parseInt(p[2]); + int cur = getCounter(player, + "KILL:" + p[1]); + return "§7击杀" + p[1] + ": §e" + + cur + "/" + needed; + } + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + // ===== 泛解析:击杀类进度 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < mobTypes.size(); i++) { + total += getCounter(player, + "KILL:" + mobTypes.get(i)); + if (i > 0) names.append("/"); + names.append(reverseMobName( + mobTypes.get(i))); + } + return "§7击杀" + names + ": §e" + + total + "/" + needed; + } + } + + // ===== 泛解析:村民交易进度 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + // ===== 泛解析:采集/挖矿进度 ===== + if (key.contains("采集") + || key.contains("挖")) { + int needed = + parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < matTypes.size(); i++) { + total += getCounter( + player, + "MINE:" + matTypes + .get(i)); + if (i > 0) + names.append("/"); + names.append( + translateMaterialName( + matTypes + .get(i))); + } + return "§7采集" + names + + ": §e" + total + + "/" + needed; + } + } + + // ===== 泛解析:合成进度 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "CRAFT"); + return "§7合成: §e" + cur + "/" + needed; + } + + // ===== 泛解析:钓鱼进度 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "FISH"); + return "§7钓鱼: §e" + cur + "/" + needed; + } + + return "§7" + key; + } + private String translateMaterialName( + String name) { + switch (name) { + case "COAL_ORE": + return "煤矿石"; + case "DEEPSLATE_COAL_ORE": + return "深层煤矿石"; + case "IRON_ORE": + return "铁矿石"; + case "DEEPSLATE_IRON_ORE": + return "深层铁矿石"; + case "COPPER_ORE": + return "铜矿石"; + case "DEEPSLATE_COPPER_ORE": + return "深层铜矿石"; + case "GOLD_ORE": + return "金矿石"; + case "DEEPSLATE_GOLD_ORE": + return "深层金矿石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "DEEPSLATE_DIAMOND_ORE": + return "深层钻石矿石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "DEEPSLATE_EMERALD_ORE": + return "深层绿宝石矿石"; + case "LAPIS_ORE": + return "青金石矿石"; + case "DEEPSLATE_LAPIS_ORE": + return "深层青金石矿石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "DEEPSLATE_REDSTONE_ORE": + return "深层红石矿石"; + case "NETHER_GOLD_ORE": + return "下界金矿石"; + case "NETHER_QUARTZ_ORE": + return "下界石英矿石"; + case "ANCIENT_DEBRIS": + return "远古残骸"; + case "COAL": + return "煤炭"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "RAW_COPPER": + return "粗铜"; + case "DIAMOND": + return "钻石"; + case "EMERALD": + return "绿宝石"; + case "LAPIS_LAZULI": + return "青金石"; + case "REDSTONE": + return "红石"; + case "QUARTZ": + return "石英"; + default: + return name; + } + } + + /** + * 中文物种名 → Bukkit EntityType 名 + * 支持击杀泛解析的模糊匹配 + */ + private String chineseMobToEntity(String cn) { + String n = cn.toLowerCase().trim(); + + // 直接别名表 + Map mobMap = new HashMap<>(); + mobMap.put("骷髅", "SKELETON"); + mobMap.put("僵尸", "ZOMBIE"); + mobMap.put("苦力怕", "CREEPER"); + mobMap.put("蜘蛛", "SPIDER"); + mobMap.put("洞穴蜘蛛", "CAVE_SPIDER"); + mobMap.put("小白", "SKELETON"); + mobMap.put("末影人", "ENDERMAN"); + mobMap.put("enderman", "ENDERMAN"); + mobMap.put("猪灵", "PIGLIN"); + mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE"); + mobMap.put("恶魂", "GHAST"); + mobMap.put("凋灵骷髅", "WITHER_SKELETON"); + mobMap.put("烈焰人", "BLAZE"); + mobMap.put("史莱姆", "SLIME"); + mobMap.put("岩浆怪", "MAGMA_CUBE"); + mobMap.put("女巫", "WITCH"); + mobMap.put("蠹虫", "SILVERFISH"); + mobMap.put("潜影贝", "SHULKER"); + mobMap.put("守卫者", "GUARDIAN"); + mobMap.put("远古守卫者", "ELDER_GUARDIAN"); + mobMap.put("溺尸", "DROWNED"); + mobMap.put("僵尸村民", "ZOMBIE_VILLAGER"); + mobMap.put("尸壳", "HUSK"); + mobMap.put("流浪者", "STRAY"); + mobMap.put("幻翼", "PHANTOM"); + mobMap.put("掠夺者", "PILLAGER"); + mobMap.put("卫道士", "VINDICATOR"); + mobMap.put("唤魔者", "EVOKER"); + mobMap.put("恼鬼", "VEX"); + mobMap.put("劫掠兽", "RAVAGER"); + mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY"); + mobMap.put("凋灵", "WITHER"); + mobMap.put("末影龙", "ENDER_DRAGON"); + + // 精确匹配 + if (mobMap.containsKey(n)) { + return mobMap.get(n); + } + + // 模糊匹配(包含关系) + for (Map.Entry en + : mobMap.entrySet()) { + if (n.contains(en.getKey()) + || en.getKey().contains(n)) { + return en.getValue(); + } + } + + // 尝试直接用英文名 + try { + return org.bukkit.entity.EntityType + .valueOf(n.toUpperCase() + .replace(" ", "_")) + .name(); + } catch (Exception e) { + return ""; + } + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + // 结构检测(覆盖纯生物群系分类) + // 地下检测:Y低于表面则归类为地下 +// 覆盖废弃矿井/试炼密室/要塞/远古城市等 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + + // ===== 结构检测(覆盖纯生物群系分类)===== + // 使用 PaperMC API 检测玩家所在位置的结构 + String structureCat = + detectStructureAt( + worldName, x, y, z); + if (structureCat != null + && !structureCat.isEmpty()) { + // 结构检测成功,结构分类优先 + // (站在结构上就不算"地下") + category = structureCat; + plugin.getLogger().info( + "[QuestTracker] 结构覆盖: " + + category + + " (结构: " + + structureCat + ")"); + } else { + plugin.getLogger().info( + "[QuestTracker] 未检测到结构: world=" + + worldName + ", pos=" + + String.format("%.0f,%.0f,%.0f", x, y, z)); + } + + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡")) + continue; + // 支持群系和结构两种打卡条件 + boolean isBiomeCond = + condKey.contains("群系") + || condKey + .contains("风景点"); + boolean isStructureCond = + condKey.contains("结构") + || condKey + .contains("村庄") + || condKey + .contains("神殿") + || condKey + .contains("神庙") + || condKey + .contains("小屋") + || condKey + .contains("雪屋") + || condKey + .contains("府邸") + || condKey + .contains("前哨站") + || condKey + .contains("矿井") + || condKey + .contains("要塞") + || condKey + .contains("城市") + || condKey + .contains("废墟") + || condKey + .contains("密室") + || condKey + .contains("沉船") + || condKey + .contains("宝箱") + || condKey + .contains("传送门") + || condKey + .contains("化石") + || condKey + .contains("堡垒"); + if (!isBiomeCond + && !isStructureCond) + continue; + String target = + extractBiome(condKey); + plugin.getLogger().info( + "[QuestTracker] 条件匹配: condKey=\"" + + condKey + "\", target=\"" + + target + "\", category=\"" + + category + "\", isBiomeCond=" + + isBiomeCond + ", isStructureCond=" + + isStructureCond); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + /* + * 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录 + */ + public String checkInAtPosition( + String player, String worldName, + double x, double y, double z) { + + List history = + loadCheckInPositions(player); + for (double[] pos : history) { + double dist = Math.sqrt( + Math.pow(x - pos[0], 2) + + Math.pow(y - pos[1], 2) + + Math.pow(z - pos[2], 2)); + if (dist <= 32) { + return "§c距离上次打卡点太近(" + + String.format( + "%.0f", dist) + + "格)"; + } + } + + // ===== 获取群系和分类 ===== + String biome = "minecraft:plains"; + String category = "平原"; + String structureCat = null; + try { + org.bukkit.World w = + Bukkit.getWorld(worldName); + if (w != null) { + org.bukkit.Location loc = + new org.bukkit.Location( + w, + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + biome = loc.getBlock() + .getBiome().getKey() + .toString(); + category = mapBiomeCategory( + biome, worldName); + + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + + // ===== 结构检测(覆盖纯生物群系分类)===== + structureCat = + detectStructureAt( + worldName, x, y, z); + plugin.getLogger().info( + "[QuestTracker] checkInAtPosition 结构检测: " + + structureCat + " at " + + String.format("%.0f,%.0f,%.0f", x, y, z)); + if (structureCat != null + && !structureCat.isEmpty()) { + // 结构分类优先于"地下" + category = structureCat; + } + + if ("下界".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.NETHER) { + category = "下界"; + } else if ("末地".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.THE_END) { + category = "末地"; + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 群系读取失败: " + + ex.getMessage()); + } + + plugin.getLogger().info( + "[QuestTracker] 打卡: " + + player + + " world=" + worldName + + " biome=" + biome + + " cat=" + category + + " pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + + saveCheckInPosition( + player, x, y, z); + recordCheckIn( + player, biome, category, + worldName, x, y, z); + List matched = + checkInBiome( + player, biome, + worldName, x, y, z); + + StringBuilder sb = new StringBuilder(); + sb.append("§a打卡成功!群系: §e") + .append(category); + if (structureCat != null && !structureCat.isEmpty()) { + sb.append("\n§a结构: §e").append(structureCat); + } + if (!matched.isEmpty()) { + sb.append("\n§a完成任务条件:"); + for (String m : matched) { + String qn = + findQuestName(m); + sb.append("\n§7• "); + if (qn != null) { + sb.append("§e[") + .append(qn) + .append("§7] §f"); + } + sb.append(m); + } + } + return sb.toString(); + } + + + /** + * 根据条件文本查找所属任务名 + */ + private String findQuestName( + String condition) { + for (String cat : Arrays.asList( + "新人任务", + "主线任务", + "支线任务")) { + for (QuestFile qf : + getQuests(cat)) { + if (qf.conditions.contains( + condition)) { + return qf.displayName; + } + } + } + return null; + } + + + /** + * 加载玩家所有历史打卡坐标 + * 格式: x1,y1,z1;x2,y2,z2;... + */ + private List loadCheckInPositions( + String player) { + List list = new ArrayList<>(); + Object val = storage.getCondition( + player, "checkin_all_pos"); + if (val == null) return list; + String data = val.toString(); + if (data.isEmpty()) return list; + String[] records = data.split(";"); + for (String r : records) { + String[] parts = r.split(","); + if (parts.length >= 3) { + try { + list.add(new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2])}); + } catch (NumberFormatException e) { + // 跳过损坏数据 + } + } + } + return list; + } + + /** + * 保存打卡坐标(追加到已有记录) + */ + private void saveCheckInPosition( + String player, double x, + double y, double z) { + List list = + loadCheckInPositions(player); + list.add(new double[]{x, y, z}); + StringBuilder sb = new StringBuilder(); + for (double[] pos : list) { + if (sb.length() > 0) sb.append(";"); + sb.append(pos[0]).append(",") + .append(pos[1]).append(",") + .append(pos[2]); + } + storage.setConditionRaw( + player, + "checkin_all_pos", + sb.toString()); + } + // ========== 中文生物名 → EntityType 映射 ========== + private static final Map + MOB_NAME_MAP = new HashMap<>(); + static { + MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER"); + MOB_NAME_MAP.put("骷髅", "SKELETON"); + MOB_NAME_MAP.put("僵尸", "ZOMBIE"); + MOB_NAME_MAP.put("苦力怕", "CREEPER"); + MOB_NAME_MAP.put("蜘蛛", "SPIDER"); + MOB_NAME_MAP.put("末影人", "ENDERMAN"); + MOB_NAME_MAP.put("女巫", "WITCH"); + MOB_NAME_MAP.put("烈焰人", "BLAZE"); + MOB_NAME_MAP.put("僵尸猪灵", + "ZOMBIFIED_PIGLIN"); + MOB_NAME_MAP.put("猪灵", "PIGLIN"); + MOB_NAME_MAP.put("恶魂", "GHAST"); + MOB_NAME_MAP.put("凋灵骷髅", + "WITHER_SKELETON"); + MOB_NAME_MAP.put("史莱姆", "SLIME"); + MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE"); + MOB_NAME_MAP.put("溺尸", "DROWNED"); + MOB_NAME_MAP.put("尸壳", "HUSK"); + MOB_NAME_MAP.put("流髑", "STRAY"); + MOB_NAME_MAP.put("幻翼", "PHANTOM"); + MOB_NAME_MAP.put("守卫者", "GUARDIAN"); + MOB_NAME_MAP.put("远古守卫者", + "ELDER_GUARDIAN"); + MOB_NAME_MAP.put("掠夺者", "PILLAGER"); + MOB_NAME_MAP.put("卫道士", "VINDICATOR"); + MOB_NAME_MAP.put("唤魔者", "EVOKER"); + MOB_NAME_MAP.put("恼鬼", "VEX"); + MOB_NAME_MAP.put("劫掠兽", "RAVAGER"); + MOB_NAME_MAP.put("潜影贝", "SHULKER"); + MOB_NAME_MAP.put("末影螨", "ENDERMITE"); + MOB_NAME_MAP.put("凋灵", "WITHER"); + MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON"); + MOB_NAME_MAP.put("监守者", "WARDEN"); + MOB_NAME_MAP.put("嗅探兽", "SNIFFER"); + MOB_NAME_MAP.put("僵尸村民", + "ZOMBIE_VILLAGER"); + } + + /** + * 从条件文本中提取所有匹配的生物类型 + * 按名称长度降序匹配,避免短名误匹配 + */ + private List extractMobTypes( + String text) { + List types = new ArrayList<>(); + List> sorted = + new ArrayList<>( + MOB_NAME_MAP.entrySet()); + sorted.sort((a, b) -> + b.getKey().length() + - a.getKey().length()); + String remaining = text; + for (Map.Entry entry + : sorted) { + if (remaining.contains( + entry.getKey())) { + if (!types.contains( + entry.getValue())) { + types.add(entry.getValue()); + } + remaining = remaining.replace( + entry.getKey(), ""); + } + } + return types; + } + + /** + * EntityType → 中文名 + */ + private String reverseMobName( + String entityType) { + for (Map.Entry entry + : MOB_NAME_MAP.entrySet()) { + if (entry.getValue() + .equals(entityType)) { + return entry.getKey(); + } + } + return entityType; + } + + /** + * 从条件文本中提取矿物名 + * "采集任意矿物(煤炭除外)1组" + * → [COAL_ORE, IRON_ORE, GOLD_ORE, ...] + */ + private List extractMaterialTypes( + String text) { + List types = new ArrayList<>(); + // 排除列表 + List excludes = + new ArrayList<>(); + if (text.contains("煤炭除外")) { + excludes.add("COAL"); + excludes.add("COAL_ORE"); + excludes.add("DEEPSLATE_COAL_ORE"); + } + // 通配符:"矿物" "矿石" + if (text.contains("矿物") + || text.contains("矿石")) { + String[] ores = { + "COAL_ORE", + "DEEPSLATE_COAL_ORE", + "IRON_ORE", + "DEEPSLATE_IRON_ORE", + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE", + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE", + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE", + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE", + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE", + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE", + "NETHER_GOLD_ORE", + "NETHER_QUARTZ_ORE", + "ANCIENT_DEBRIS" + }; + for (String ore : ores) { + if (!excludes.contains(ore)) { + types.add(ore); + } + } + return types; + } + // 单个矿物名 + if (text.contains("煤炭")) + types.add("COAL"); + if (text.contains("铁矿") + || text.contains("铁锭")) + types.add("RAW_IRON"); + if (text.contains("金矿") + || text.contains("金锭")) + types.add("RAW_GOLD"); + if (text.contains("钻石")) + types.add("DIAMOND"); + if (text.contains("绿宝石")) + types.add("EMERALD"); + if (text.contains("青金石")) + types.add("LAPIS_LAZULI"); + if (text.contains("红石")) + types.add("REDSTONE"); + if (text.contains("铜")) + types.add("RAW_COPPER"); + if (text.contains("下界石英") + || text.contains("石英")) + types.add("QUARTZ"); + return types; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + * "打卡村庄" → "村庄" + * "打卡沙漠神殿" → "沙漠" + */ + private String extractBiome(String text) { + if (text == null) return ""; + + // 优先匹配结构名称(更具体) + String structureResult = + mapStructureCategory(text); + if (!structureResult.isEmpty()) { + return structureResult; + } + + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点") + || key.contains("结构") + || key.contains("村庄") + || key.contains("神殿") + || key.contains("神庙") + || key.contains("小屋") + || key.contains("雪屋") + || key.contains("府邸") + || key.contains("前哨站") + || key.contains("矿井") + || key.contains("要塞") + || key.contains("城市") + || key.contains("废墟") + || key.contains("密室") + || key.contains("废墟") + || key.contains("沉船") + || key.contains("宝箱") + || key.contains("传送门") + || key.contains("化石") + || key.contains("堡垒"))) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; + } + return getCounter( + player, "biome_ALL") >= 1; + } + + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "VILLAGER_TRADE") + >= needed; + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "REPORT_VILLAGE") + >= needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return streak >= needed; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + // ===== 泛解析:击杀类中文条件 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + for (String mobType : mobTypes) { + total += getCounter(player, + "KILL:" + mobType); + } + return total >= needed; + } + } + + // ===== 泛解析:村民交易条件 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "TRADE") + >= needed; + } + + // ===== 泛解析:采集/挖矿类条件 ===== + if (key.contains("采集") || key.contains("挖")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + for (String mat : matTypes) { + total += getCounter(player, + "MINE:" + mat); + } + return total >= needed; + } + } + + // ===== 泛解析:合成类条件 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "CRAFT") + >= needed; + } + + // ===== 泛解析:钓鱼类条件 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "FISH") + >= needed; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public void onVillagerTrade(String player) { + String key = "VILLAGER_TRADE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/RadioDownloadListener.java b/backup/20260605_152529/src/main/java/Sdf1_login/RadioDownloadListener.java new file mode 100644 index 0000000..d3e744f --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/RadioDownloadListener.java @@ -0,0 +1,144 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player + .PlayerResourcePackStatusEvent; + +import java.util.UUID; +import java.util.concurrent + .ConcurrentHashMap; + +public class RadioDownloadListener + implements Listener { + + private final Main plugin; + private final java.util.logging.Logger log; + + private final ConcurrentHashMap + failCount = new ConcurrentHashMap<>(); + private final ConcurrentHashMap + blocked = new ConcurrentHashMap<>(); + + private static final int MAX_FAILED = 3; + + public RadioDownloadListener(Main plugin) { + this.plugin = plugin; + this.log = plugin.getLogger(); + } + + public void onPlayerQuit(UUID uuid) { + failCount.remove(uuid); + blocked.remove(uuid); + } + + @EventHandler + public void onStatus( + PlayerResourcePackStatusEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + String status = + e.getStatus().name(); + + log.info("[Radio] === 资源包状态 ==="); + log.info("[Radio] 玩家: " + + p.getName()); + log.info("[Radio] UUID: " + uuid); + log.info("[Radio] 状态: " + + status); + log.info("[Radio] 原始状态枚举: " + + e.getStatus().getClass() + .getSimpleName() + + "." + + e.getStatus().name()); + + if (blocked.containsKey(uuid)) { + log.info("[Radio] 已熔断,跳过: " + + p.getName()); + return; + } + + switch (status) { + case "SUCCESS": + log.info("[Radio] " + + p.getName() + + " ✓ 资源包加载成功"); + failCount.remove(uuid); + blocked.remove(uuid); + break; + + case "ACCEPTED": + log.info("[Radio] " + + p.getName() + + " 客户端已接受," + + "等待下载..."); + break; + + case "DECLINED": + log.warning("[Radio] " + + p.getName() + + " ✗ 拒绝了资源包"); + log.warning("[Radio] " + + "可能原因: " + + "1.客户端设置禁用资源包" + + " 2.已缓存拒绝记录" + + " 3.SSL证书不匹配"); + handleFailure(p, uuid); + break; + + case "FAILED_DOWNLOAD": + log.warning("[Radio] " + + p.getName() + + " ✗ 下载失败"); + log.warning("[Radio] " + + "可能原因: " + + "1.URL无法访问" + + " 2.SSL证书错误" + + " 3.网络超时" + + " 4.文件不存在(404)"); + handleFailure(p, uuid); + break; + + default: + log.info("[Radio] " + + p.getName() + + " 未知状态: " + + status); + break; + } + } + + private void handleFailure(Player p, UUID uuid) { + int failNum = failCount.getOrDefault(uuid, 0) + 1; + failCount.put(uuid, failNum); + + log.warning("[Radio] " + p.getName() + + " 失败次数: " + failNum + "/" + MAX_FAILED); + + if (failNum >= MAX_FAILED) { + blocked.put(uuid, true); + log.severe("[Radio] " + p.getName() + + " 已达最大失败次数,停止重试," + + "玩家继续留在服务器"); + // 不踢出,只记录熔断,玩家正常游玩 + p.sendMessage("§e§l[Radio] 资源包加载失败" + + ",已取消下载资源包,部分画面音频可能出现问题" + + "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员"); + + return; + } +// 打了包没测试 + p.sendMessage("§e§l[Radio] 下载失败," + + "重试... (" + failNum + "/" + MAX_FAILED + ")"); + + // 5秒后重试 + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline() && plugin.radio != null) { + plugin.radio.sendResourcePack(p); + } + }, 100L); + } +} \ No newline at end of file diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/RadioManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/RadioManager.java new file mode 100644 index 0000000..6fe5eda --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/RadioManager.java @@ -0,0 +1,1040 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.zip.*; +import java.util.Collections; +import org.bukkit.NamespacedKey; +import java.util.Set; + + +public class RadioManager { + + private final Main plugin; + private File radioDir; + private File oggDir; + private File packDir; + private BukkitTask mainRadioTask; + private final Map loginPlayers = + new ConcurrentHashMap<>(); + private String externalUrl = ""; + private int httpPort = 443; + private final Set pendingPack = + Collections.newSetFromMap( + new ConcurrentHashMap<>()); + private String lastPlayedSound = ""; + + private int radioIndex = 0; + + // ========== 登录阶段推送(管理员手动触发) ========== + public void startLoginRadio(Player p) { + loginPlayers.put(p.getName(), true); + } + + public void stopLoginRadio(Player p) { + loginPlayers.remove(p.getName()); + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + } + + private final List mainFiles = new ArrayList<>(); + private final List loginFiles = new ArrayList<>(); + + public RadioManager(Main plugin) { + this.plugin = plugin; + this.radioDir = new File( + plugin.getDataFolder(), "radio"); + this.oggDir = new File( + plugin.getDataFolder(), "radio/ogg"); + this.packDir = new File( + plugin.getDataFolder(), "radiopack"); + radioDir.mkdirs(); + oggDir.mkdirs(); + packDir.mkdirs(); + loadRadioConfig(); + } + // ========== 读取 radio_config.txt ========== + private void loadRadioConfig() { + File cfg = new File( + plugin.getDataFolder(), + "radio_config.txt"); + + // 不存在则创建默认配置 + if (!cfg.exists()) { + createRadioConfig(cfg); + plugin.getLogger().info( + "[Radio] 已生成 radio_config.txt"); + } + + // 读取 + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(cfg), + java.nio.charset + .StandardCharsets + .UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("#") + || line.trim().isEmpty()) + continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq) + .trim(); + String val = line.substring(eq + 1) + .trim(); + if (key.equals("resource-pack-url")) { + externalUrl = val; + } else if (key.equals("http-port")) { + try { + httpPort = Integer.parseInt( + val); + } catch (NumberFormatException ignored) { + } + } + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 读取radio_config.txt失败: " + + e.getMessage()); + } + + // 443是HTTPS端口,内嵌HTTP不能用 + if (httpPort == 443) httpPort = 8080; + + plugin.getLogger().info( + "[Radio] 配置: externalUrl=[" + + externalUrl + "] httpPort=" + + httpPort); + } + + private void createRadioConfig(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + java.nio.charset + .StandardCharsets + .UTF_8))) { + pw.println("# ============ Sdf1 Radio Config ============"); + pw.println("#"); + pw.println("# resource-pack-url"); + pw.println("# 填外部地址(HTTP或HTTPS均可)"); + pw.println("# 例: https://example.com/radiopack.zip"); + pw.println("# 留空 = 使用内置HTTP服务"); + pw.println("#"); + pw.println("# http-port"); + pw.println("# 内置HTTP端口(仅resource-pack-url为空时生效)"); + pw.println("#"); + pw.println("# ============ Settings ==============="); + pw.println(); + pw.println("resource-pack-url="); + pw.println("http-port=8080"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 创建radio_config.txt失败: " + + e.getMessage()); + } + } + + // ========== 初始化 ========== + public void init() { + loadRadioConfig(); + convertNonOgg(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); // 启动HTTP + plugin.getLogger().info( + "[Radio] 初始化完成: 全服=" + + mainFiles.size() + + " 登录=" + loginFiles.size()); + } + + + // ========== 转换非ogg文件 ========== + private void convertNonOgg() { + convertDir(radioDir); + convertDir(new File(radioDir, "login")); + convertDir(new File(radioDir, "reg")); + } + + private void convertDir(File src) { + if (!src.exists() || !src.isDirectory()) return; + File[] files = src.listFiles((d, n) -> { + String l = n.toLowerCase(); + return l.endsWith(".mp3") + || l.endsWith(".wav") + || l.endsWith(".flac"); + }); + if (files == null) return; + + for (File f : files) { + String oggName = f.getName() + .replaceAll("\\.(mp3|wav|flac)$", + ".ogg"); + File oggFile = new File(src, oggName); + if (oggFile.exists() + && oggFile.lastModified() + >= f.lastModified()) { + continue; + } + // 调用ffmpeg转换 + if (convertWithFfmpeg(f, oggFile)) { + plugin.getLogger().info( + "[Radio] 转换成功: " + + f.getName()); + } else { + plugin.getLogger().warning( + "[Radio] 转换失败: " + + f.getName() + + " (需要安装ffmpeg)"); + } + } + } + + private boolean convertWithFfmpeg( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-c:a", "libvorbis", + "-q:a", "5", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean finished = + p.waitFor(30, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + return false; + } + } + + // ========== 加载ogg文件列表 ========== + private void loadAllOggs() { + mainFiles.clear(); + loginFiles.clear(); + loadOggsFlat(radioDir, mainFiles); + loadOggsFlat( + new File(radioDir, "login"), + loginFiles); + loadOggsFlat( + new File(radioDir, "reg"), + loginFiles); + } + + private void loadOggsFlat( + File dir, List list) { + if (!dir.exists() || !dir.isDirectory()) + return; + File[] files = dir.listFiles( + (d, n) -> n.toLowerCase() + .endsWith(".ogg")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) list.add(f); + } + /** + * 解析 pack.png:优先读取插件目录, + * 没有则搜索图片并用ffmpeg转码 + */ + private File resolvePackIcon() { + File dataDir = plugin.getDataFolder(); + File packPng = new File(dataDir, "pack.png"); + + if (packPng.exists() + && packPng.length() > 0) { + plugin.getLogger().info( + "[Radio] 找到 pack.png (" + + packPng.length() + + " bytes)"); + return packPng; + } + + // 搜索可转换的图片文件 + String[] exts = {"png", "jpg", "jpeg", + "bmp", "webp", "tiff", "gif"}; + File[] found = dataDir.listFiles( + (dir, name) -> { + String low = name.toLowerCase(); + for (String ext : exts) { + if (low.endsWith("." + ext) + && !name.equals( + "pack.png")) { + return true; + } + } + return false; + }); + + if (found != null && found.length > 0) { + File src = found[0]; + plugin.getLogger().info( + "[Radio] 找到图片: " + + src.getName() + + " → 转码为 pack.png"); + if (convertToPackPng(src, packPng)) { + return packPng; + } + plugin.getLogger().warning( + "[Radio] 转码失败"); + return null; + } + + plugin.getLogger().warning( + "[Radio] 未找到 pack.png 或可转换图片"); + return null; + } + + /** + * ffmpeg 将任意图片转为 256x256 PNG + */ + private boolean convertToPackPng( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-vf", + "scale='min(256,iw)'" + + ":'min(256,ih)':" + + "force_original_aspect_ratio" + + "=decrease," + + "pad=256:256:" + + "(ow-iw)/2:" + + "(oh-ih)/2:" + + "color=0x00000000", + "-c:v", "png", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean ok = p.waitFor( + 30, TimeUnit.SECONDS); + if (!ok) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] ffmpeg转码失败: " + + e.getMessage()); + return false; + } + } + + // ========== 资源包构建 ========== + private void buildResourcePack() { + deleteDir(packDir); + packDir.mkdirs(); + + List allOggs = new ArrayList<>(); + collectOggs(radioDir, allOggs); + + if (allOggs.isEmpty()) { + plugin.getLogger().warning( + "[Radio] radio/ 目录下" + + "无 .ogg 文件,跳过资源包"); + return; + } + + // 正确路径:assets/minecraft/sounds/custom/ + File soundsDir = new File(packDir, + "assets/minecraft/sounds/custom"); + soundsDir.mkdirs(); + + // 生成 sounds.json + StringBuilder json = + new StringBuilder("{\n"); + for (int i = 0; i < allOggs.size(); i++) { + File ogg = allOggs.get(i); + String simpleName = + ogg.getName() + .replace(".ogg", ""); + File dest = new File(soundsDir, + simpleName + ".ogg"); + try { + Files.copy(ogg.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception e) { + // ignore + } + // 音频事件名: custom.xxx + // 文件路径: custom/xxx + json.append(" \"custom.") + .append(simpleName) + .append("\": {\n"); + json.append(" \"sounds\": [{\n"); + json.append(" \"name\":" + + " \"custom/") + .append(simpleName) + .append("\",\n"); + json.append( + " \"stream\": true\n"); + json.append(" }]\n"); + json.append(" }"); + if (i < allOggs.size() - 1) + json.append(","); + json.append("\n"); + } + json.append("}"); + + // sounds.json 放在 assets/minecraft/ 下 + try { + Files.write(new File(packDir, + "assets/minecraft/sounds.json") + .toPath(), + json.toString() + .getBytes("UTF-8")); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] sounds.json写入失败"); + } +// sounds.json 写入后... + +// ===== 新增:打包 pack.png 作为资源包图标 ===== + File iconPng = resolvePackIcon(); + if (iconPng != null && iconPng.exists()) { + try { + Files.copy(iconPng.toPath(), + new File(packDir, "pack.png") + .toPath(), + StandardCopyOption + .REPLACE_EXISTING); + plugin.getLogger().info( + "[Radio] pack.png 已加入资源包"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] pack.png复制失败: " + + e.getMessage()); + } + } + + // pack.mcmeta(pack_format=54) + try { + Files.write(new File(packDir, + "pack.mcmeta").toPath(), + ("{\n \"pack\": {\n" + + " \"pack_format\": 54,\n" + + " \"description\":" + + " \"§d§l草原探险服务器资源包,请勿拒绝资源包\"\n }\n}") + .getBytes("UTF-8")); + } catch (Exception e) { + // ignore + } + + try { + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + packZip(packDir, zipFile); + plugin.getLogger().info( + "[Radio] 资源包生成: " + + allOggs.size() + " 个音频"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] zip打包失败: " + + e.getMessage()); + } + } + + private void deleteDir(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) deleteDir(f); + else f.delete(); + } + } + dir.delete(); + } + + private void collectOggs( + File dir, List list) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) + collectOggs(f, list); + else if (f.getName().endsWith(".ogg")) + list.add(f); + } + } + + private void packZip(File root, + File zipFile) throws Exception { + try (ZipOutputStream zos = + new ZipOutputStream( + new FileOutputStream( + zipFile))) { + zipDir(root, root, zos); + } + } + + private void zipDir(File root, + File current, + ZipOutputStream zos) + throws Exception { + File[] files = current.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + zipDir(root, f, zos); + } else { + String path = root.toURI() + .relativize(f.toURI()) + .getPath(); + zos.putNextEntry(new ZipEntry(path)); + zos.write(Files.readAllBytes( + f.toPath())); + zos.closeEntry(); + } + } + } + + // ========== 资源包发送 ========== + + /** + * 发送资源包(带限流,不禁重试) + */ + private final Map lastSendTime = + new ConcurrentHashMap<>(); + + public void sendResourcePack(Player p) { + UUID uuid = p.getUniqueId(); + + // 最短间隔 5秒(不是10秒的全锁死) + Long last = lastSendTime.get(uuid); + if (last != null + && System.currentTimeMillis() - last + < 5000L) { + plugin.getLogger().info( + "[Radio] 跳过(5秒内): " + + p.getName()); + return; + } + lastSendTime.put(uuid, + System.currentTimeMillis()); + + String url = getResourcePackUrl(); + if (url == null || url.isEmpty()) { + plugin.getLogger().warning( + "[Radio] URL为空,跳过: " + + p.getName()); + return; + } + + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) { + plugin.getLogger().warning( + "[Radio] radiopack.zip不存在," + + "跳过: " + p.getName()); + return; + } + + try { + byte[] data = + Files.readAllBytes(zip.toPath()); + String hash = sha1(data); + long fileSize = zip.length(); + + plugin.getLogger().info( + "[Radio] 发送: " + p.getName() + + " url=" + url + + " size=" + fileSize + + " hash=" + hash); + + sendPackWithHash(p, url, hash); + + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] 发送异常: " + + p.getName()); + e.printStackTrace(); + } + } + + + + private void sendPackWithHash( + Player p, + String url, + String hash) { + + UUID uuid = UUID.fromString( + "a1b2c3d4-e5f6-7890-abcd-" + + "ef1234567890"); + + // 尝试5参 + Component + try { + Object component = + net.kyori.adventure.text + .Component.text( + "§d§l请下载自定义资源包"); + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + net.kyori.adventure + .text.Component + .class, + boolean.class) + .invoke( + p, uuid, url, + hash, component, + true); + plugin.getLogger().info( + "[Radio] 成功(5参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试5参 + String描述 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + String.class, + boolean.class) + .invoke( + p, uuid, url, + hash, + "Sdf1 Radio", + true); + plugin.getLogger().info( + "[Radio] 成功(5参String): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试3参 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class) + .invoke( + p, uuid, url, + hash); + plugin.getLogger().info( + "[Radio] 成功(3参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // ★ 全部失败 → 通知玩家 + 熔断 + plugin.getLogger().severe( + "[Radio] 所有签名失败: " + + p.getName() + + " 客户端版本: " + + p.getProtocolVersion()); + p.sendMessage( + "§e§l[Radio] §f资源包发送失败," + + "§7部分功能可能异常"); + p.sendMessage( + "§7可尝试: §e/rejoin §7或重启客户端"); + } + + + + private void sendPackFromLocal( + Player p, String url) { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) return; + try { + byte[] data = + Files.readAllBytes( + zip.toPath()); + String hash = sha1(data); + sendPackWithHash(p, url, hash); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 降级发送失败"); + } + } + + // 从URL下载字节 + private byte[] downloadBytes(String urlStr) + throws Exception { + java.net.URL url = + new java.net.URL(urlStr); + java.net.HttpURLConnection conn = + (java.net.HttpURLConnection) + url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + try (java.io.InputStream is = + conn.getInputStream()) { + return is.readAllBytes(); + } + } + + // SHA-1计算 + private String sha1(byte[] data) + throws Exception { + java.security.MessageDigest md = + java.security.MessageDigest + .getInstance("SHA-1"); + md.update(data); + byte[] hash = md.digest(); + StringBuilder sb = + new StringBuilder(); + for (byte b : hash) + sb.append(String.format( + "%02x", b)); + return sb.toString(); + } + + public void clearPending(UUID uuid) { + lastSendTime.remove(uuid); + } + + + + // ========== 内嵌HTTP服务 ========== + private com.sun.net.httpserver.HttpServer + httpServer; + + private void startHttpServer() { + // 如果配置了外部地址则不启动 + if (plugin.getConfig2() != null + && !plugin.getConfig2() + .broadcastServerUrl + .isEmpty()) { + return; + } + try { + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + httpServer = + com.sun.net.httpserver + .HttpServer.create( + new java.net + .InetSocketAddress( + port), 0); + httpServer.createContext( + "/radiopack.zip", + exchange -> { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (zip.exists()) { + byte[] data = + Files.readAllBytes( + zip.toPath()); + exchange.getResponseHeaders() + .set("Content-Type", + "application/zip"); + exchange.getResponseHeaders() + .set("Access-Control" + + "-Allow-Origin", + "*"); + exchange.sendResponseHeaders( + 200, data.length); + exchange.getResponseBody() + .write(data); + exchange.getResponseBody() + .close(); + } else { + byte[] empty = + "not found".getBytes(); + exchange.sendResponseHeaders( + 404, empty.length); + exchange.getResponseBody() + .write(empty); + exchange.getResponseBody() + .close(); + } + }); + httpServer.setExecutor(null); + httpServer.start(); + plugin.getLogger().info( + "[Radio] HTTP服务: http://0.0.0.0:" + + port + "/radiopack.zip"); + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] HTTP启动失败: " + + e.getMessage()); + } + } + + private void stopHttpServer() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + } + + public String getResourcePackUrl() { + // 有外部配置 → 随机生成URL + if (externalUrl != null + && !externalUrl.trim().isEmpty()) { + return generateRandomUrl(externalUrl); + } + // 无配置 → 用内置HTTP + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + return "http://127.0.0.1:" + + port + "/radiopack.zip"; + } + + + /** + * 从配置的域名列表中随机选一个 + * 生成随机、带时间戳的完整资源包URL + * + * 规则: + * *.example.com + * → 随机子域+时间戳 + * 例: a3f7k2.example.com/pack.zip?t=xxx&v=b9c1e4 + * + * cdn.example.com (无*) + * → 直接使用+时间戳 + * 例: cdn.example.com/pack.zip?t=xxx + */ + private String generateRandomUrl( + String raw) { + String[] parts = raw.split("\\|"); + String pick = + parts[(int)(Math.random() + * parts.length)].trim(); + + // 去掉协议 https:// http:// + String cleaned = pick + .replaceFirst("^https?://", ""); + // 去掉路径 /path/a?v=1 + if (cleaned.contains("/")) { + cleaned = cleaned.substring( + 0, cleaned.indexOf('/')); + } + + // 随机子域 + String hex = Long.toHexString( + Double.doubleToLongBits( + Math.random())); + String rand = hex.length() > 6 + ? hex.substring(2, 8) : hex; + rand = rand.replaceAll( + "[^a-z0-9]", "x"); + + String host; + if (cleaned.startsWith("*")) { + host = rand + + cleaned.substring(1); + } else { + host = cleaned; + } + + String ts = String.valueOf( + System.currentTimeMillis()); + String token = Integer.toHexString( + (int)(Math.random() + * 0xFFFFFF)); + + String url = "https://" + host + + "/radiopack.zip" + + "?t=" + ts + + "&v=" + token; + + plugin.getLogger().info( + "[Radio] 生成URL: " + + pick + " → " + url); + return url; + } + + + + // ========== 全服广播 ========== + public void startMainRadio() { + stopMainRadio(); + if (mainFiles.isEmpty()) { + plugin.getLogger().warning( + "[Radio] 无 .ogg 文件"); + return; + } + plugin.getLogger().info( + "[Radio] 启动全服广播 (" + + mainFiles.size() + " 个文件)"); + mainRadioTask = new BukkitRunnable() { + @Override + public void run() { + playNext(); + } + }.runTaskTimer(plugin, 100L, 2400L); + } + + private void playNext() { + if (mainFiles.isEmpty()) return; + File file = mainFiles.get( + radioIndex % mainFiles.size()); + String name = file.getName() + .replace(".ogg", ""); + lastPlayedSound = "custom." + name; + String key = "minecraft:" + lastPlayedSound; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + radioIndex++; + } + + // ========== 按名称播放 ========== + public void playToAll(String name) { + String key = findSongKey(name); + if (key == null) return; + lastPlayedSound = "custom." + name; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + } + + + public void playToPlayer( + Player p, String name) { + String key = findSongKey(name); + if (key == null) { + p.sendMessage("§c未找到: " + name); + return; + } + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + + public void playToLoginPlayers(String name) { + String key = findSongKey(name); + if (key == null) return; + for (String pName + : loginPlayers.keySet()) { + Player lp = Bukkit.getPlayer(pName); + if (lp != null && lp.isOnline()) { + lp.playSound(lp.getLocation(), + key, + SoundCategory.RECORDS, + 1.5f, 1.0f); + } + } + } + + + private String findSongKey(String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + return "minecraft:custom." + name; + } + } + plugin.getLogger().warning( + "[Radio] 未找到歌曲: " + name); + return null; + } + + + public boolean hasSong(String name) { + return findSongKey(name) != null; + } + + // ========== 手动播放 ========== + public boolean playSpecific( + Player sender, String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + String key = "sdf1radio:" + name; + for (Player p + : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + + name); + return true; + } + } + if (sender != null) + sender.sendMessage("§c未找到: " + name); + return false; + } + public void stopMainRadio() { + if (mainRadioTask != null) { + mainRadioTask.cancel(); + mainRadioTask = null; + } + if (lastPlayedSound != null + && !lastPlayedSound.isEmpty()) { + for (Player p : Bukkit.getOnlinePlayers()) { + p.stopSound(lastPlayedSound, + SoundCategory.RECORDS); + } + lastPlayedSound = ""; + } + } + +// 在 init() 或 registerEvents 中保存引用: +// this.listener = new RadioDownloadListener(plugin); +private RadioDownloadListener radioListener; + + + + // ========== 重载 ========== + public void reload() { + stopMainRadio(); + stopAllLoginRadio(); + stopHttpServer(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); + loadRadioConfig(); + } +} \ No newline at end of file diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/RefundRecord.java b/backup/20260605_152529/src/main/java/Sdf1_login/RefundRecord.java new file mode 100644 index 0000000..c0a3070 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/RefundRecord.java @@ -0,0 +1,62 @@ +package Sdf1_login; + +import org.bukkit.Material; +import java.util.UUID; + +public class RefundRecord { + public final UUID playerUUID; + public final String playerName; + public final String itemId; + public final String itemName; + public final Material material; + public final Material shulkerMaterial; + public int amount; + public int totalPaid; + public final long timestamp; + + // 普通购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, int amount, + int totalPaid, long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = null; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + // 盒装购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, + Material shulkerMat, + int amount, int totalPaid, + long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = shulkerMat; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + public boolean isExpired() { + return System.currentTimeMillis() + - timestamp > 300000L; + } + + public int getSecondsLeft() { + long left = 300000L + - (System.currentTimeMillis() + - timestamp); + return (int) Math.max(0, left / 1000); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/RiskControlManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/RiskControlManager.java new file mode 100644 index 0000000..4ddedcc --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/RiskControlManager.java @@ -0,0 +1,248 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警 + */ +public class RiskControlManager { + + private final Main plugin; + + // 每个玩家名 -> 连续失败次数 + private final Map failAttempts = + new ConcurrentHashMap<>(); + + // 每个IP -> 关联的可疑玩家列表 + private final Map> ipSuspicious = + new ConcurrentHashMap<>(); + + // 每个玩家 -> 上次验证时间(用于冷却) + private final Map verifyCooldown = + new ConcurrentHashMap<>(); + + // 配置项 + private static final int MAX_FAIL_ATTEMPTS = 5; + private static final int MAX_ACCOUNTS_PER_IP = 5; + private static final long COOLDOWN_MS = 60000L; + + public RiskControlManager(Main plugin) { + this.plugin = plugin; + } + + /** + * 记录一次失败的登录尝试 + * + * @param playerName 玩家名 + * @return 剩余尝试次数 + */ + public int recordFailAttempt(String playerName) { + int current = failAttempts + .getOrDefault(playerName, 0) + 1; + failAttempts.put(playerName, current); + return Math.max(0, MAX_FAIL_ATTEMPTS - current); + } + + /** + * 重置指定玩家的失败计数(登录成功时调用) + */ + public void resetFailAttempts(String playerName) { + failAttempts.remove(playerName); + } + + /** + * 获取指定玩家的失败次数 + */ + public int getFailAttempts(String playerName) { + return failAttempts + .getOrDefault(playerName, 0); + } + + /** + * 判断玩家是否已被锁定(失败次数达到上限) + */ + public boolean isLocked(String playerName) { + return failAttempts + .getOrDefault(playerName, 0) + >= MAX_FAIL_ATTEMPTS; + } + + /** + * 检查IP是否达到注册上限 + * 直接查数据库,不依赖 DatabaseManager 的方法 + */ + public boolean canRegisterFromIP(String ip) { + if (ip == null || ip.isEmpty()) return true; + try { + java.sql.Connection conn = + plugin.getDb().getDb(); + if (conn == null) return true; + java.sql.PreparedStatement ps = + conn.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address = ?"); + ps.setString(1, ip); + java.sql.ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count < 5; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + + /** + * 记录IP关联的可疑行为 + */ + public void recordSuspiciousIP(String ip, + String playerName) { + if (ip == null || ip.isEmpty()) return; + ipSuspicious + .computeIfAbsent(ip, + k -> ConcurrentHashMap.newKeySet()) + .add(playerName); + } + + /** + * 获取某IP关联的所有可疑玩家 + */ + public Set getSuspiciousAccounts(String ip) { + return ipSuspicious + .getOrDefault(ip, Collections.emptySet()); + } + + /** + * 检查是否在验证冷却期内 + */ + public boolean isOnCooldown(String playerName) { + Long last = verifyCooldown.get(playerName); + if (last == null) return false; + return System.currentTimeMillis() - last + < COOLDOWN_MS; + } + + /** + * 设置验证冷却时间 + */ + public void setCooldown(String playerName) { + verifyCooldown.put(playerName, + System.currentTimeMillis()); + } + + /** + * 当检测到可疑登录时触发报警 + * - 全服广播 + * - 有邮箱则发邮件 + * - 记录到数据库 + */ + public void triggerAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + + // 记录到数据库 + plugin.getDb().recordSecurityAlert( + name, suspiciousIP); + + // 记录IP可疑行为 + recordSuspiciousIP(suspiciousIP, name); + + // 全服广播 + String serverMsg = "§c§l[安全警告] §e" + name + + " §f的账号疑似遭到 §c" + suspiciousIP + + " §f的盗号尝试, 请各位玩家注意账号安全!"; + Bukkit.broadcastMessage(serverMsg); + + // 有邮箱则发邮件 + String emailAddr = (String) plugin.getDb() + .getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String subject = + "[安全警告] 您的账号疑似遭到盗号尝试"; + String body = "尊敬的玩家 " + name + + ":\n\n" + + "您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + suspiciousIP + + " 的盗号尝试。\n\n" + + "建议您:\n" + + "1. 立即修改密码" + + "(使用 /sdf1_login pw)\n" + + "2. 如非本人操作," + + "请联系管理员处理\n\n" + + "--Sdf1_login 安全系统"; + plugin.getEmail().sendTempPassword( + emailAddr, name, body); + } + } + + /** + * 处理登录失败时的风控逻辑 + * + * @param player 玩家 + * @param suspiciousIP 可疑来源IP + * @return true=已被锁定(应踢出), false=仍可继续尝试 + */ + public boolean handleLoginFailure(Player player, + String suspiciousIP) { + String name = player.getName(); + int remaining = recordFailAttempt(name); + + if (remaining <= 0) { + // 达到上限,触发报警 + triggerAlert(player, suspiciousIP); + player.kickPlayer( + "§c§l[风控] §f登录失败次数过多," + + " 账号已被临时锁定, 请联系管理员"); + return true; + } + + // 未达上限,提示剩余次数 + player.sendMessage( + "§c§l[风控] §f密码错误! " + + "剩余" + remaining + "次机会"); + return false; + } + + /** + * 登录成功后重置风控状态 + */ + public void onLoginSuccess(String playerName) { + resetFailAttempts(playerName); + } + + /** + * 清理过期的冷却记录(可由定时任务调用) + */ + public void cleanup() { + long now = System.currentTimeMillis(); + verifyCooldown.entrySet().removeIf( + e -> now - e.getValue() > COOLDOWN_MS * 10); + } + + /** + * 获取所有玩家的失败尝试记录(调试用) + */ + public Map getAllFailAttempts() { + return Collections.unmodifiableMap(failAttempts); + } + + /** + * 清除所有风控数据(管理员调试用) + */ + public void clearAll() { + failAttempts.clear(); + ipSuspicious.clear(); + verifyCooldown.clear(); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/SalesStatsManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/SalesStatsManager.java new file mode 100644 index 0000000..9d06a22 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/SalesStatsManager.java @@ -0,0 +1,170 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class SalesStatsManager { + + private final Main plugin; + + private final Map sessionSales = + new ConcurrentHashMap<>(); + private int sessionRevenue = 0; + private int sessionOrders = 0; + private int sessionItems = 0; + private int sessionRefunds = 0; + private int sessionRefundAmount = 0; + private long sessionStart = System.currentTimeMillis(); + + // 状态 + private boolean waitingForEmpty = false; + private boolean hasReported = false; + private BukkitRunnable waitTask = null; + + public SalesStatsManager(Main plugin) { + this.plugin = plugin; + startMonitor(); + } + + // ★ 每10秒检测一次 + private void startMonitor() { + new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 有玩家在线 → 重置等待状态 + if (waitingForEmpty) { + // 刚有人进来了,清除已汇报标记 + // 下次没人时会重新汇报 + } + waitingForEmpty = false; + hasReported = false; + cancelWaitTask(); + } else if (!waitingForEmpty && !hasReported) { + // 没人了,且还没汇报过 → 开始1分钟倒计时 + waitingForEmpty = true; + startWaitCountdown(); + } + } + }.runTaskTimer(plugin, 200L, 200L); + } + + // ★ 1分钟等待 + private void startWaitCountdown() { + cancelWaitTask(); + waitTask = new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 等待期间有人进来 → 取消,等下次没人 + waitingForEmpty = false; + hasReported = false; + waitTask = null; + cancel(); + return; + } + + // 1分钟没人 → 汇报一次 + if (!hasReported) { + hasReported = true; + report(); + } + waitingForEmpty = false; + waitTask = null; + cancel(); + } + }; + waitTask.runTaskLater(plugin, 1200L); // 60秒 + } + + private void cancelWaitTask() { + if (waitTask != null) { + waitTask.cancel(); + waitTask = null; + } + } + + // ===== 记录 ===== + + public void recordSale(String itemName, int price) { + sessionSales.merge(itemName, 1, Integer::sum); + sessionRevenue += price; + sessionOrders++; + sessionItems++; + } + + public void recordRefund(int amount) { + sessionRefunds++; + sessionRefundAmount += amount; + } + + // ===== 汇报 ===== + + public void report() { + if (sessionItems == 0 && sessionRefunds == 0) { + // plugin.getLogger().info("[Sales] 无数据,跳过"); + return; + } + + long minutes = + (System.currentTimeMillis() - sessionStart) + / 60000; + if (minutes < 1) minutes = 1; + + StringBuilder sb = new StringBuilder(); + sb.append("§6§l========== 销量汇总 =========="); + sb.append("\n§7时长: §f").append(minutes).append("分钟"); + sb.append("\n§7订单: §f").append(sessionOrders).append("笔"); + sb.append("\n§7商品: §f").append(sessionItems).append("件"); + sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚"); + sb.append("\n§7退款: §c").append(sessionRefunds) + .append("次(§c").append(sessionRefundAmount) + .append("枚§7)"); + sb.append("\n§6§l------------------------------"); + + if (!sessionSales.isEmpty()) { + sb.append("\n§7销量排行:"); + List> sorted = + new ArrayList<>(sessionSales.entrySet()); + sorted.sort((a, b) -> + b.getValue() - a.getValue()); + int rank = 1; + for (Map.Entry e : sorted) { + if (rank > 10) break; + sb.append("\n§e").append(rank).append(". ") + .append(e.getKey()) + .append(" §7x").append(e.getValue()); + rank++; + } + } + sb.append("\n§6§l=============================="); + + String msg = sb.toString(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.isOp()) { + for (String line : msg.split("\n")) + p.sendMessage(line); + } + } + plugin.getLogger().info("[Sales] 汇报完成"); + resetSession(); + } + + public void resetSession() { + sessionSales.clear(); + sessionRevenue = 0; + sessionOrders = 0; + sessionItems = 0; + sessionRefunds = 0; + sessionRefundAmount = 0; + sessionStart = System.currentTimeMillis(); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ShopCategory.java b/backup/20260605_152529/src/main/java/Sdf1_login/ShopCategory.java new file mode 100644 index 0000000..715e9ad --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ShopCategory.java @@ -0,0 +1,32 @@ +package Sdf1_login; + +import java.util.ArrayList; +import java.util.List; + +public class ShopCategory { + private final String name; + private final String fileName; + private final List items; + + public ShopCategory(String name, String fileName) { + this.name = name; + this.fileName = fileName; + this.items = new ArrayList<>(); + } + + public String getName() { return name; } + public String getFileName() { return fileName; } + public List getItems() { return items; } + + public ShopItem getItem(String id) { + for (ShopItem item : items) { + if (item.getId().equalsIgnoreCase(id)) + return item; + } + return null; + } + + public void addItem(ShopItem item) { + items.add(item); + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ShopItem.java b/backup/20260605_152529/src/main/java/Sdf1_login/ShopItem.java new file mode 100644 index 0000000..d336c7c --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ShopItem.java @@ -0,0 +1,49 @@ +package Sdf1_login; + +import org.bukkit.Material; + +public class ShopItem { + private final String id; + private final String displayName; + private final Material material; + private final int buyPrice; + private final int sellPrice; + private int stock; + private int hourlySales; + private int totalSales; + public ShopItem(String id, String displayName, + Material material, int buyPrice, + int sellPrice, int stock, + int hourlySales, int totalSales) { + this.id = id; + this.displayName = displayName; + this.material = material; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.stock = stock; + this.hourlySales = hourlySales; + this.totalSales = totalSales; + } + + public int getEffectiveSellPrice() { + if (sellPrice <= 0) return 0; + return sellPrice; + } + + public String getId() { return id; } + public String getDisplayName() { return displayName; } + public Material getMaterial() { return material; } + public int getBuyPrice() { return buyPrice; } + public int getSellPrice() { return sellPrice; } + public int getStock() { return stock; } + public int getHourlySales() { return hourlySales; } + public int getTotalSales() { return totalSales; } + + public void setStock(int s) { this.stock = s; } + public void setHourlySales(int s) { this.hourlySales = s; } + public void setTotalSales(int s) { this.totalSales = s; } + public void addSales(int n) { + this.hourlySales += n; + this.totalSales += n; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/ShopManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/ShopManager.java new file mode 100644 index 0000000..cdcc045 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/ShopManager.java @@ -0,0 +1,2610 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Base64; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.inventory.meta.PotionMeta; + + + + +public class ShopManager implements Listener { + + private final Main plugin; + private final List categories = + new ArrayList<>(); + private final List refundRecords = + new ArrayList<>(); + private final Map activeDiscount = + new HashMap<>(); + private final Map viewCat = + new HashMap<>(); + private final Map viewPage = + new HashMap<>(); + private final Map> shoppingCarts = + new HashMap<>(); + private final Set cartModeEnabled = + new HashSet<>(); + private final java.util.Random rng = + new java.util.Random(); + private final Map> pendingCheckout = + new HashMap<>(); + private final Set couponListening = + new HashSet<>(); + private final Map pendingCouponCode = + new HashMap<>(); + // ===== 物品NBT数据 ===== + private final Map itemNbtData = + new HashMap<>(); + private final Map itemStackCache = + new HashMap<>(); + // 替换字段 + private final Map couponApplyTime = new HashMap<>(); + + + + private static final Material[] SHULKER_COLORS = { + Material.SHULKER_BOX, + Material.WHITE_SHULKER_BOX, + Material.ORANGE_SHULKER_BOX, + Material.MAGENTA_SHULKER_BOX, + Material.LIGHT_BLUE_SHULKER_BOX, + Material.YELLOW_SHULKER_BOX, + Material.LIME_SHULKER_BOX, + Material.PINK_SHULKER_BOX, + Material.GRAY_SHULKER_BOX, + Material.LIGHT_GRAY_SHULKER_BOX, + Material.CYAN_SHULKER_BOX, + Material.PURPLE_SHULKER_BOX, + Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, + Material.RED_SHULKER_BOX, + Material.BLACK_SHULKER_BOX + }; + private static final Map EFFECT_MAP = new HashMap<>(); + static { + // ===== 基础效果 ===== + EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); + EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); + EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); + EFFECT_MAP.put("速度", PotionEffectType.SPEED); + EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); // 1.21+ 改名 + EFFECT_MAP.put("治疗", PotionEffectType.INSTANT_HEALTH); // 1.21+ 改名 + EFFECT_MAP.put("治愈", PotionEffectType.INSTANT_HEALTH); + EFFECT_MAP.put("伤害", PotionEffectType.INSTANT_DAMAGE); // 1.21+ 改名 + EFFECT_MAP.put("剧毒", PotionEffectType.POISON); + EFFECT_MAP.put("中毒", PotionEffectType.POISON); + EFFECT_MAP.put("回复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("再生", PotionEffectType.REGENERATION); + EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("抗性", PotionEffectType.RESISTANCE); // 1.21+ 改名 + EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); + EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); + EFFECT_MAP.put("急迫", PotionEffectType.HASTE); // 1.21+ 改名 + EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); // 1.21+ 改名 + EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); + EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); // ★ 补充 + EFFECT_MAP.put("迟缓", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); + EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); + EFFECT_MAP.put("幸运", PotionEffectType.LUCK); + EFFECT_MAP.put("不祥", PotionEffectType.UNLUCK); + EFFECT_MAP.put("发光", PotionEffectType.GLOWING); + EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); + EFFECT_MAP.put("生命加成", PotionEffectType.HEALTH_BOOST); + EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); + EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); + EFFECT_MAP.put("海豚关怀", PotionEffectType.DOLPHINS_GRACE); + EFFECT_MAP.put("跳跃", PotionEffectType.JUMP_BOOST); + // ★ 神龟药水是双效果,特殊处理,这里只做名称匹配入口 + EFFECT_MAP.put("神龟", PotionEffectType.RESISTANCE); + } + + public static class CartItem { + String itemId; + String displayName; + Material material; + int unitPrice; + int quantity; + } + + + private static final int PAGE_SIZE = 28; + private static final int[] ITEM_SLOTS = { + 10,11,12,13,14,15,16, + 19,20,21,22,23,24,25, + 28,29,30,31,32,33,34, + 37,38,39,40,41,42,43 + }; + + public ShopManager(Main plugin) { + this.plugin = plugin; + loadCategories(); + startHourlyTask(); + loadItemNbtData(); + startCleanupTask(); + } + // ===== 切换购物车模式 ===== + // ===== 切换购物车模式 ===== + public boolean toggleCartMode(Player p) { + UUID uuid = p.getUniqueId(); + if (cartModeEnabled.contains(uuid)) { + cartModeEnabled.remove(uuid); + p.sendMessage("§7购物车模式已关闭,恢复正常购买"); + return false; + } else { + cartModeEnabled.add(uuid); + p.sendMessage("§a购物车模式已开启,点击商品加入购物车"); + return true; + } + } + + public boolean isCartMode(Player p) { + return cartModeEnabled.contains(p.getUniqueId()); + } + private String itemToBase64(ItemStack item) { + try { + ByteArrayOutputStream os = + new ByteArrayOutputStream(); + BukkitObjectOutputStream bos = + new BukkitObjectOutputStream(os); + bos.writeObject(item); + bos.close(); + return Base64.getEncoder() + .encodeToString(os.toByteArray()); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToItem(String b64) { + try { + byte[] data = + Base64.getDecoder().decode(b64); + ByteArrayInputStream is = + new ByteArrayInputStream(data); + BukkitObjectInputStream bis = + new BukkitObjectInputStream(is); + ItemStack item = + (ItemStack) bis.readObject(); + bis.close(); + return item; + } catch (Exception e) { + return null; + } + } + + private void loadItemNbtData() { + itemNbtData.clear(); + itemStackCache.clear(); + File f = new File(plugin.getDataFolder(), + "shop/item_nbt.properties"); + if (!f.exists()) return; + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + 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 id = + line.substring(0, eq).trim(); + String b64 = + line.substring(eq + 1).trim(); + itemNbtData.put(id, b64); + ItemStack stack = base64ToItem(b64); + if (stack != null) { + itemStackCache.put(id, stack); + } + } + br.close(); + // plugin.getLogger().info("[Shop] NBT数据: " + // + itemStackCache.size() + "条"); + } catch (Exception ignored) {} + } + + private void saveItemNbtData() { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, "item_nbt.properties"); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 商品完整数据(自动生成)"); + for (Map.Entry e + : itemNbtData.entrySet()) { + pw.println(e.getKey() + "=" + + e.getValue()); + } + pw.flush(); + pw.close(); + } catch (Exception ignored) {} + } + + // ===== 加入/移除购物车 ===== + public boolean addToCart(Player p, ShopItem item) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts + .computeIfAbsent( + uuid, k -> new ArrayList<>()); + // ★ 始终用原价,折扣在结算时统一应用 + int price = item.getBuyPrice(); + for (CartItem ci : cart) { + if (ci.itemId.equals(item.getId())) { + ci.quantity += 1; + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + } + CartItem ci = new CartItem(); + ci.itemId = item.getId(); + ci.displayName = item.getDisplayName(); + ci.material = item.getMaterial(); + ci.unitPrice = price; + ci.quantity = 1; + cart.add(ci); + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + + + public int getCartCount(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int c = 0; + for (CartItem ci : cart) c += ci.quantity; + return c; + } + + public int getCartTotal(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int t = 0; + for (CartItem ci : cart) + t += ci.unitPrice * ci.quantity; + return t; + } + + public void cartAddOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + cart.get(index).quantity += 1; + } + + public void cartRemoveOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + CartItem ci = cart.get(index); + ci.quantity -= 1; + if (ci.quantity <= 0) cart.remove(index); + } + + public void clearCart(Player p) { + shoppingCarts.remove(p.getUniqueId()); + } + + // ===== 打开购物车页面 ===== + public void openCart(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§a§l购物车"); + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + for (int i = 0; i < cart.size() && i < 25; i++) { + CartItem ci = cart.get(i); + ItemStack is = new ItemStack(ci.material); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + ci.displayName + + " §7x" + ci.quantity); + im.setLore(java.util.Arrays.asList( + "§7单价: §f" + ci.unitPrice + "枚", + "§7小计: §6" + + (ci.unitPrice * ci.quantity) + + "枚", + "", + "§a左键+1 §c右键-1")); + is.setItemMeta(im); + } + g.setItem(10 + i, is); + } + if (cart.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7购物车为空")); + } + int total = getCartTotal(p); + int bal = plugin.getBonds().getBonds(p.getName()); + g.setItem(49, mkItem(Material.EMERALD, + "§6§l结算: §e" + total + " §6枚债券", + "§7余额: §f" + bal + "枚", + bal >= total ? "§a余额充足" : "§c余额不足", + "", "§a点击结算")); + g.setItem(48, mkItem( + Material.LIME_STAINED_GLASS_PANE, + "§7清空购物车")); + g.setItem(53, mkItem(Material.ARROW, "§7返回商店")); + + p.openInventory(g); + } + + // ===== 购物车点击处理 ===== + public boolean handleCartClick(Player p, int raw, + boolean left, + boolean right) { + if (raw == 53) { openShopMain(p); return true; } + if (raw == 48) { + clearCart(p); + p.sendMessage("§7购物车已清空"); + openCart(p); + return true; + } + if (raw == 49) { + checkoutCartInit(p); + return true; + } + if (raw >= 10 && raw < 35) { + int idx = raw - 10; + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + if (idx < cart.size()) { + if (right) cartRemoveOne(p, idx); + else cartAddOne(p, idx); + openCart(p); + } + return true; + } + return true; + } + + // ===== 结算入口:判断打包方式 ===== + private void checkoutCartInit(Player p) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts.get(uuid); + if (cart == null || cart.isEmpty()) { + p.sendMessage("§c购物车为空"); + return; + } + int total = getCartTotal(p); + int bal = plugin.getBonds() + .getBonds(p.getName()); + if (bal < total) { + p.sendMessage("§c余额不足,需要 §e" + + total + "§c枚"); + return; + } + int totalQty = 0; + for (CartItem ci : cart) totalQty += ci.quantity; + if (cart.size() == 1 && totalQty <= 64) { + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + deductAndGive(p, cart, + (int) Math.round(total * factor), + false, null, "", 0, + ecoPct, couponPct); + return; + } + pendingCheckout.put(uuid, + new ArrayList<>(cart)); + openPackingConfirm(p, total); + } + + + + // ===== 执行扣款+发货 ===== + private boolean deductAndGive(Player p, + List cart, int baseTotal, + boolean useShulker, Material color, + String packNote, int packFee, + int ecoPct, int couponPct) { + + UUID uuid = p.getUniqueId(); + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + String[] names = new String[cart.size()]; + int[] subs = new int[cart.size()]; + for (int i = 0; i < cart.size(); i++) { + CartItem ci = cart.get(i); + names[i] = ci.displayName; + subs[i] = ci.unitPrice * ci.quantity; + } + + if (baseTotal < originalTotal) { + double ratio = + (double) baseTotal / originalTotal; + int sum = 0; + for (int i = 0; i < subs.length; i++) { + if (i == subs.length - 1) { + subs[i] = baseTotal - sum; + } else { + subs[i] = (int) (subs[i] * ratio); + sum += subs[i]; + } + } + } + + boolean ok = plugin.getBonds().checkoutCart( + p.getName(), names, subs); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + if (packFee > 0) { + plugin.getBonds().deductBonds( + p.getName(), packFee, + "shop_packing", "", + "商店系统", "打包费"); + } + + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + if (si != null) { + if (si.getStock() >= 0) + si.setStock(si.getStock() + - ci.quantity); + si.addSales(ci.quantity); + } + } + + int newBal = plugin.getBonds() + .getBonds(p.getName()); + + + // ===== 优惠券码 ===== + String usedCouponCode = + pendingCouponCode.get(uuid); + + // ===== 记录订单 ===== + OrderManager.OrderRecord latestOrder = null; + try { + List orderItems = + new ArrayList<>(); + for (CartItem ci : cart) { + orderItems.add( + new OrderManager.OrderItem( + ci.displayName, + ci.material.name(), + ci.unitPrice, + ci.unitPrice, + ci.quantity)); + } + String dType = "none"; + String cCode = ""; + if (usedCouponCode != null) { + dType = "coupon"; + cCode = usedCouponCode; + } else if (ecoPct > 0) { + dType = "eco"; + } + plugin.getOrderManager().recordOrder( + p, orderItems, originalTotal, + baseTotal + packFee, + originalTotal - baseTotal, + dType, cCode, packFee, + useShulker ? (packFee > 0 + ? "custom" : "default") + : "none", + color != null ? color.name() : ""); + // 获取最新订单 + synchronized (plugin.getOrderManager() + .getOrders()) { + List all = + plugin.getOrderManager() + .getOrders(); + if (!all.isEmpty()) + latestOrder = + all.get(all.size() - 1); + } + // 散装打印小票 + if (!useShulker && latestOrder != null) { + plugin.getOrderManager() + .sendReceiptChat(p, latestOrder); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + // + ex.getMessage()); + } + +// ===== 发货 ===== + if (!useShulker) { + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + ItemStack give = (si != null) + ? getShopStack(si, ci.quantity) + : new ItemStack(ci.material, ci.quantity); + HashMap left2 = + p.getInventory().addItem(give); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } else { + List shulkers = + packShulkers(cart, color); + + // ★ 给潜影盒打订单ID标签 + if (latestOrder != null) { + for (ItemStack sh : shulkers) { + ItemMeta shm = sh.getItemMeta(); + if (shm != null) { + List lore = shm.getLore() != null + ? new ArrayList<>(shm.getLore()) + : new ArrayList<>(); + lore.add("§8#" + latestOrder.orderId); + shm.setLore(lore); + sh.setItemMeta(shm); + } + } + } + + // 小票书塞入 + if (!shulkers.isEmpty() + && latestOrder != null) { + ItemStack book = plugin.getOrderManager() + .createReceiptBook(latestOrder); + plugin.getOrderManager() + .addBookToShulker(shulkers.get(0), book); + } + // 发货 + for (ItemStack sh : shulkers) { + HashMap left2 = + p.getInventory().addItem(sh); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } + + + // ===== 退款记录 ===== + long now = System.currentTimeMillis(); + if (!useShulker) { + for (CartItem ci : cart) { + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + ci.itemId, ci.displayName, + ci.material, null, + ci.quantity, + ci.unitPrice * ci.quantity, + now)); + } + } else { + String summary = ""; + for (CartItem ci : cart) { + if (!summary.isEmpty()) summary += "+"; + summary += ci.displayName + + "x" + ci.quantity; + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + "cart_" + now, summary, + Material.SHULKER_BOX, color, + 1, baseTotal, now)); + } + + // ===== 核销优惠券 ===== + if (usedCouponCode != null) { + activeDiscount.remove(uuid); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCouponCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCouponCode + + " §7已核销"); + } + + shoppingCarts.remove(uuid); + pendingCheckout.remove(uuid); + return true; + } + + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + markShulkerUsed(item); + } + + // ★ 公共方法:标记潜影盒为已使用 + private void markShulkerUsed(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + // shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + @EventHandler + public void onPlayerDrop(org.bukkit.event.player.PlayerDropItemEvent e) { + ItemStack item = e.getItemDrop().getItemStack(); + if (item != null && item.getType().name().contains("SHULKER")) { + markShulkerUsed(item); + } + } + + + // ===== 潠影盒打包(指定颜色)===== + private List packShulkers( + List items, Material color) { + List result = new ArrayList<>(); + List all = new ArrayList<>(); + for (CartItem ci : items) { + int rem = ci.quantity; + while (rem > 0) { + int sz = Math.min(rem, 64); + all.add(new ItemStack(ci.material, sz)); + rem -= sz; + } + } + while (!all.isEmpty()) { + ItemStack shulker = new ItemStack(color); + org.bukkit.inventory.meta.BlockStateMeta meta = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (meta != null) { + org.bukkit.block.BlockState state = + meta.getBlockState(); + if (state instanceof org.bukkit.block.Container) { + org.bukkit.block.Container c = + (org.bukkit.block.Container) state; + List remain = new ArrayList<>(); + for (ItemStack stack : all) { + HashMap left = + c.getInventory().addItem(stack); + for (ItemStack l : left.values()) + remain.add(l); + } + all = remain; + meta.setBlockState(c); + shulker.setItemMeta(meta); + } + } + result.add(shulker); + } + + return result; + } + + // ===== 打包确认GUI ===== + public void openPackingConfirm(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l选择包装方式"); + g.setItem(10, mkItem(Material.CHEST, + "§a§l不打包", + "§7直接放入背包", + "§7适合少量物品")); + g.setItem(13, mkItem(Material.SHULKER_BOX, + "§6§l默认打包", + "§7随机颜色潜影盒", + "§7加收 §e5 §6枚债券")); + g.setItem(16, mkItem(Material.ENDER_CHEST, + "§b§l自选颜色", + "§7选择你喜欢的潜影盒颜色", + "§7加收 §e5 §6枚债券")); + g.setItem(22, mkItem(Material.PAPER, + "§7消费金额: §e" + baseTotal + "枚")); + + p.openInventory(g); + } + + public boolean handlePackingConfirm( + Player p, int raw) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + + if (raw == 10) { + ecoPct = 2; + } + + double factor = 1.0; + if (ecoPct > 0) + factor *= (100 - ecoPct) / 100.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + originalTotal * factor); + + int totalQty = 0; + for (CartItem ci : cart) + totalQty += ci.quantity; + boolean multiAndBig = (cart.size() > 1 + && originalTotal >= 100); + + if (raw == 10) { + deductAndGive(p, cart, discounted, + false, null, "", 0, + ecoPct, couponPct); + return true; + } + if (raw == 13) { + Material c = Material.SHULKER_BOX; + if (multiAndBig) { + deductAndGive(p, cart, discounted, + true, c, "", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, + true, c, "默认打包", 5, + ecoPct, couponPct); + } + return true; + } + if (raw == 16) { + pendingCheckout.put(uuid, cart); + openColorSelect(p, discounted); + return true; + } + return true; + } + + + // ===== 自选颜色GUI ===== + public void openColorSelect(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 54, "§b§l选择潜影盒颜色"); + String[] colorNames = { + "白色","橙色","品红","淡蓝", + "黄色","淡绿","粉色","灰色", + "淡灰","青色","紫色","蓝色", + "棕色","绿色","红色","黑色","原色" + }; + for (int i = 0; i < SHULKER_COLORS.length; i++) { + ItemStack is = new ItemStack(SHULKER_COLORS[i]); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + colorNames[i] + + "§6潜影盒"); + im.setLore(java.util.Arrays.asList( + "§7加收 §e5 §6枚债券", + "", + "§a点击选择")); + is.setItemMeta(im); + } + g.setItem(i, is); + } + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(45, mkItem(Material.PAPER, + "§7消费: §e" + baseTotal + + "枚 + §e5§6打包费")); + + p.openInventory(g); + } + + public boolean handleColorSelect( + Player p, int raw) { + if (raw == 49) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.get(uuid); + if (cart != null) { + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + openPackingConfirm(p, total); + } + return true; + } + if (raw < 0 || raw >= SHULKER_COLORS.length) + return false; + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + total * factor); + + Material selectedColor = SHULKER_COLORS[raw]; + if (selectedColor == Material.SHULKER_BOX) { + deductAndGive(p, cart, discounted, true, + Material.SHULKER_BOX, + "原色打包", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, true, + selectedColor, "自选颜色", 5, + ecoPct, couponPct); + } + return true; + } + + + + // ===== 加载分类 ===== + + public void loadCategories() { + // plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); // 过滤调试日志 + categories.clear(); + viewCat.clear(); // ★ 清缓存 + viewPage.clear(); + + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + + convertTxtToMd(dir); + + File[] allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + if (allMd == null || allMd.length == 0) { + createDefaultCategories(dir); + allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + } + if (allMd == null) { + // plugin.getLogger().warning( + // "[Shop] shop目录为空"); + return; + } + + /* plugin.getLogger().info("[Shop] === 开始加载 ==="); + plugin.getLogger().info("[Shop] 扫描到 " + + allMd.length + " 个md文件"); +*/ + for (File f : allMd) { + /* plugin.getLogger().info("[Shop] 读取: " + + f.getName());*/ + ShopCategory cat = parseFile(f); + if (cat != null) { + /* plugin.getLogger().info("[Shop] 分类: " + + cat.getName() + + " 商品数: " + + cat.getItems().size());*/ + if (!cat.getItems().isEmpty()) { + categories.add(cat); + } + } + } + + int totalItems = 0; + for (ShopCategory c : categories) + totalItems += c.getItems().size(); + + plugin.getLogger().info("[Shop] === 加载完成 ===" + + " 分类=" + categories.size() + + " 商品=" + totalItems); + } + + +// ===== txt → md 自动迁移 ===== + + private void convertTxtToMd(File dir) { + File[] txtFiles = dir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("coupons.txt")); + if (txtFiles == null) return; + for (File txt : txtFiles) { + String mdName = txt.getName() + .replace(".txt", ".md"); + File mdFile = new File(dir, mdName); + if (mdFile.exists()) continue; + + try { + List lines = new ArrayList<>(); + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(txt), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) + lines.add(line); + br.close(); + + String catName = txt.getName() + .replace(".txt", ""); + List out = new ArrayList<>(); + out.add("# " + catName); + out.add(""); + out.add("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + out.add("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + + for (String l : lines) { + l = l.trim(); + if (l.isEmpty() || l.startsWith("#")) + continue; + // 旧格式: ID | 名称 | 材质 | ... + // 保持原样转换为 md 表格行 + out.add("| " + l.replace("|", "|") + + " |"); + } + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(mdFile), + StandardCharsets.UTF_8)); + for (String o : out) + pw.println(o); + pw.flush(); + pw.close(); + + txt.delete(); + // plugin.getLogger().info( + // "[Shop] 已迁移: " + // + txt.getName() + // + " → " + mdName); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 迁移失败: " + // + txt.getName()); + } + } + } + +// ===== 解析 md 文件 ===== + + private ShopCategory parseFile(File f) { + String name = f.getName() + .replace(".md", ""); + ShopCategory cat = + new ShopCategory(name, f.getName()); + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + boolean headerPassed = false; + boolean separatorPassed = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + // 空行跳过 + if (line.isEmpty()) continue; + + // 标题行:# 分类名 + if (line.startsWith("#")) { + String t = line.replaceFirst( + "^#+\\s*", "").trim(); + if (!t.isEmpty()) { + // 更新分类名(用文件内标题) + // cat.name 是 final,不改 + } + continue; + } + + // 表格分隔行:| --- | --- | + if (line.contains("---")) { + separatorPassed = true; + continue; + } + + // 表头行:| ID | 品名 | ... + if (line.startsWith("|")) { + if (!separatorPassed) { + headerPassed = true; + continue; + } + // 数据行 + ShopItem item = + parseTableRow(line); + if (item != null) + cat.addItem(item); + } + } + br.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 解析失败: " + f.getName() + // + " 原因: " + e.getMessage()); + return null; + } + return cat; + } + +// ===== 解析表格行 ===== + + private ShopItem parseTableRow(String row) { + // plugin.getLogger().info("[Shop] 行: " + row); // 过滤调试日志 + + String trimmed = row.trim(); + if (trimmed.startsWith("|")) + trimmed = trimmed.substring(1); + if (trimmed.endsWith("|")) + trimmed = trimmed.substring(0, + trimmed.length() - 1); + + String[] p = trimmed.split("\\|"); + // plugin.getLogger().info("[Shop] 列数=" + p.length); // 过滤调试日志 + + if (p.length < 6) { + // plugin.getLogger().warning( + // "[Shop] ✗ 列数不足, 跳过"); + return null; + } + + String rawId = p[0].trim(); + String rawName = p[1].trim(); + String rawMat = p[2].trim().toUpperCase(); + String rawBuy = p[3].trim(); + String rawSell = p[4].trim(); + String rawStock = p[5].trim(); + + // plugin.getLogger().info("[Shop] ID=" + // + rawId + " 名=" + rawName + // + " 材质=" + rawMat + // + " 购=" + rawBuy + " 售=" + rawSell + // + " 库=" + rawStock); // 过滤调试日志 + + // 检查材质是否存在 + Material mat = Material.matchMaterial(rawMat); + if (mat == null) { + // plugin.getLogger().warning( + // "[Shop] ✗ 无效材质: " + rawMat + // + " (行: " + row + ")"); + return null; + } + + try { + ShopItem item = new ShopItem( + rawId, rawName, mat, + Integer.parseInt(rawBuy), + Integer.parseInt(rawSell), + Integer.parseInt(rawStock), + p.length > 6 + ? Integer.parseInt( + p[6].trim()) : 0, + p.length > 7 + ? Integer.parseInt( + p[7].trim()) : 0); + // plugin.getLogger().info("[Shop] ✓ 解析成功: " + rawId); // 过滤调试日志 + return item; + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] ✗ 数字解析失败: " + // + e.getMessage()); + return null; + } + } + + +// ===== 保存为 md ===== + + public void saveCategory(ShopCategory cat) { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, cat.getFileName()); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# " + cat.getName()); + pw.println(""); + pw.println("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + pw.println("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + for (ShopItem item : cat.getItems()) { + pw.println("| " + + item.getId() + + " | " + item.getDisplayName() + + " | " + item.getMaterial().name() + + " | " + item.getBuyPrice() + + " | " + item.getSellPrice() + + " | " + item.getStock() + + " | " + item.getHourlySales() + + " | " + item.getTotalSales() + + " |"); + } + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 保存失败: " + // + cat.getFileName()); + } + } + +// ===== 创建示例分类 ===== + + private void createDefaultCategories(File dir) { + writeMd(dir, "建筑材料.md", + "# 建筑材料\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| STONE | 石头 | STONE" + + " | 10 | -1 | -1 | 0 | 0 |\n" + + "| COBBLESTONE | 圆石 | COBBLESTONE" + + " | 5 | -1 | -1 | 0 | 0 |\n" + + "| OAK_PLANKS | 橡木木板 | OAK_PLANKS" + + " | 8 | 3 | -1 | 0 | 0 |\n" + + "| BRICKS | 砖块 | BRICKS" + + " | 12 | -1 | -1 | 0 | 0 |\n" + + "| GLASS | 玻璃 | GLASS" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK" + + " | 20 | 8 | -1 | 0 | 0 |\n"); + + writeMd(dir, "装备工具.md", + "# 装备工具\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| IRON_PICKAXE | 铁镐 | IRON_PICKAXE" + + " | 50 | 20 | -1 | 0 | 0 |\n" + + "| DIAMOND_PICKAXE | 钻石镐 | DIAMOND_PICKAXE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| IRON_SWORD | 铁剑 | IRON_SWORD" + + " | 40 | 16 | -1 | 0 | 0 |\n" + + "| DIAMOND_SWORD | 钻石剑 | DIAMOND_SWORD" + + " | 400 | 160 | -1 | 0 | 0 |\n" + + "| ELYTRA | 鞘翅 | ELYTRA" + + " | 2000 | 800 | -1 | 0 | 0 |\n"); + + writeMd(dir, "食物补给.md", + "# 食物补给\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| COOKED_BEEF | 熟牛排 | COOKED_BEEF" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| GOLDEN_APPLE | 金苹果 | GOLDEN_APPLE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| BREAD | 面包 | BREAD" + + " | 8 | 3 | -1 | 0 | 0 |\n"); + + // plugin.getLogger().info( + // "[Shop] 已创建3个示例分类(md)"); + } + + private void writeMd(File dir, String name, + String content) { + File f = new File(dir, name); + if (f.exists()) return; + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.print(content); + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 创建失败: " + name); + } + } + + /** + * 从商品名称+材质智能推断完整物品 + * + * "夜视药水" + POTION + * → PotionEffect(NIGHT_VISION, 600, 0) + * + * "喷溅型治疗药水" + SPLASH_POTION + * → PotionEffect(HEALING, 600, 0) + * + * "长效夜视之箭" + TIPPED_ARROW + * → PotionEffect(NIGHT_VISION, 1800, 0) + * + * "强效力量喷溅药水" + SPLASH_POTION + * → PotionEffect(STRENGTH, 600, 1) + */ + private ItemStack inferFromName( + String name, Material mat) { + // 只处理药水和箭 + if (mat != Material.POTION + && mat != Material.SPLASH_POTION + && mat != Material.LINGERING_POTION + && mat != Material.TIPPED_ARROW) { + return null; + } + + // 1. 找效果类型 + PotionEffectType effectType = null; + for (Map.Entry e + : EFFECT_MAP.entrySet()) { + if (name.contains(e.getKey())) { + effectType = e.getValue(); + break; + } + } + if (effectType == null) return null; + + // 2. 找时长 + int duration = 600; + if (name.contains("长效") || name.contains("长时") + || name.contains("持久") || name.contains("长命")) { + duration = 1800; + } +// ★ 瞬间效果药水:duration = 1 + if (name.contains("治疗") || name.contains("治愈") + || name.contains("伤害")) { + duration = 1; + } + + // 3. 找等级 + int amplifier = 0; + if (name.contains("强效") + || name.contains("强化") + || name.contains("加强")) { + amplifier = 1; + } + + // 4. 构造完整物品 + try { + ItemStack item = new ItemStack(mat); + PotionMeta meta = + (PotionMeta) item.getItemMeta(); + + // ★ 神龟药水:双效果 + if (name.contains("神龟")) { + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.RESISTANCE, + duration, amplifier + 2), + true); + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.SLOWNESS, + duration, amplifier + 3), + true); + } else { + // 普通药水:单效果 + meta.addCustomEffect( + new PotionEffect(effectType, + duration, amplifier), + true); + } + + item.setItemMeta(meta); + return item; + } catch (Exception e) { + return null; + } + } + + public void saveAll() { + for (ShopCategory cat : categories) + saveCategory(cat); + } + private ItemStack getShopStack( + ShopItem item, int amount) { + // 优先级1: 手动捕获的NBT数据 + ItemStack cached = + itemStackCache.get(item.getId()); + if (cached != null) { + ItemStack copy = cached.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级2: 根据名称智能推断 + ItemStack inferred = inferFromName( + item.getDisplayName(), + item.getMaterial()); + if (inferred != null) { + ItemStack copy = inferred.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级3: 纯材质(默认) + return new ItemStack( + item.getMaterial(), amount); + } + + // ===== 购买 ===== + + public boolean buyItem(Player p, ShopItem item, + int amount) { + // plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); + // plugin.getLogger().info("buyItem被调用"); + if (item == null) return false; + if (item.getStock() >= 0 + && item.getStock() < amount) { + p.sendMessage("§c库存不足,剩余: " + + item.getStock()); + return false; + } + if (p.getInventory().firstEmpty() == -1 + && !p.getInventory().contains( + item.getMaterial(), 1)) { + p.sendMessage("§c背包已满"); + return false; + } + + int unit = item.getBuyPrice(); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int total = Math.max(0, + unit * amount * (100 - disc) / 100); + + int balance = plugin.getBonds() + .getBonds(p.getName()); + if (balance < total) { + p.sendMessage("§c债券不足,需要 §e" + + total + "§c枚,当前 §e" + + balance + "§c枚"); + return false; + } + + boolean ok = plugin.getBonds().deductBonds( + p.getName(), total, + "shop_buy", item.getId(), + "商店系统", "购买" + + item.getDisplayName()); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + ItemStack give = getShopStack(item, amount); + HashMap left = + p.getInventory().addItem(give); + for (ItemStack drop : left.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + + if (item.getStock() >= 0) + item.setStock( + item.getStock() - amount); + item.addSales(amount); + + mergeOrAddRefund(p, item, amount, total); + Long applyTime = couponApplyTime.get(p.getUniqueId()); + if (applyTime != null + && System.currentTimeMillis() - applyTime > 300000L) { + // 超过5分钟,自动失效 + pendingCouponCode.remove(p.getUniqueId()); + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + p.sendMessage("§7优惠券已过期,请重新输入"); + disc = 0; // 重置折扣 + } + + + String discMsg = disc > 0 + ? " §7(优惠" + disc + "%)" : ""; + p.sendMessage("§a购买成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a-" + total + "枚债券" + + discMsg); + if (disc > 0) + activeDiscount.remove( + p.getUniqueId()); + // ★ 直购成功后核销优惠券 + String usedCode = pendingCouponCode + .remove(p.getUniqueId()); + if (usedCode != null) { + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCode + + " §7已核销"); + couponApplyTime.remove(p.getUniqueId()); + } + // ★ 记录订单(包裹try-catch,失败不影响购买) + try { + String usedCouponCode = + pendingCouponCode.get(p.getUniqueId()); + List orderItems = + new ArrayList<>(); + orderItems.add(new OrderManager.OrderItem( + item.getDisplayName(), + item.getMaterial().name(), + item.getBuyPrice(), + item.getBuyPrice(), amount)); + plugin.getOrderManager().recordOrder( + p, orderItems, + item.getBuyPrice() * amount, total, + item.getBuyPrice() * amount - total, + usedCouponCode != null ? "coupon" : "none", + usedCouponCode != null ? usedCouponCode : "", + 0, "none", ""); + // 打印小票 + List all = + plugin.getOrderManager().getOrders(); + if (!all.isEmpty()) { + plugin.getOrderManager().sendReceiptChat( + p, all.get(all.size() - 1)); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + ex.getMessage()); + } + + return true; + } + + + public void handleRefundItemClick(Player p, int raw) { + if (raw < 10 || raw >= 45) return; + int target = raw - 10; + int cnt = 0; + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { it.remove(); continue; } + if (cnt == target) { + requestRefund(p, r); + return; + } + cnt++; + } + } + } + + + // ===== 出售 ===== + + public boolean sellItem(Player p, ShopItem item, + int amount) { + if (item == null) return false; + + // ★ 先判断售价,禁止出售的不收物品 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return false; + } + + if (!p.getInventory().containsAtLeast( + getShopStack(item, 1), amount)) { + p.sendMessage("§c你没有足够的 §e" + + item.getDisplayName()); + return false; + } + + int total = item.getEffectiveSellPrice() * amount; + + p.getInventory().removeItem( + getShopStack(item, amount)); + plugin.getBonds().addBonds( + p.getName(), total, "shop_sell", + item.getId(), "商店系统", + "出售" + item.getDisplayName()); + if (item.getStock() >= 0) + item.setStock(item.getStock() + amount); + p.sendMessage("§a出售成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a+" + total + "枚债券"); + return true; + } + + // ===== 退款合并:30秒内同商品合并为一个订单 ===== + private boolean mergeOrAddRefund(Player p, ShopItem item, + int amount, int total) { + long now = System.currentTimeMillis(); + synchronized (refundRecords) { + for (int i = refundRecords.size() - 1; i >= 0; i--) { + RefundRecord r = refundRecords.get(i); + boolean same = r.playerUUID.equals(p.getUniqueId()) + && r.itemId.equals(item.getId()) + && (now - r.timestamp) < 30000L; + if (same) { + // final 字段不能改,只能删旧建新 + int newAmount = r.amount + amount; + int newTotal = r.totalPaid + total; + refundRecords.remove(i); + refundRecords.add(new RefundRecord( + r.playerUUID, r.playerName, + r.itemId, r.itemName, + r.material, newAmount, + newTotal, now)); + return true; + } + } + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + item.getId(), item.getDisplayName(), + item.getMaterial(), amount, total, now)); + return false; + } + + + + // ===== 退款 ===== + + public boolean requestRefund(Player p, RefundRecord rec) { + if (rec == null) return false; + if (!rec.playerUUID.equals(p.getUniqueId())) { + p.sendMessage("§c这不是你的购买记录"); + return false; + } + if (rec.isExpired()) { + p.sendMessage("§c已超过5分钟退款期限"); + synchronized (refundRecords) { + refundRecords.remove(rec); + } + return false; + } + + // 检测物品/潜影盒是否还在 + synchronized (refundRecords) { + if (rec.shulkerMaterial != null) { + int boxCount = countShulkers(p, rec.shulkerMaterial); + if (boxCount < 1) { + p.sendMessage("§c潜影盒已移出背包,无法退款"); + refundRecords.remove(rec); + return false; + } + } else { + int have = countDirect(p, rec.material); + if (have < rec.amount) { + p.sendMessage("§c物品已转移或消耗,无法退款"); + refundRecords.remove(rec); + return false; + } + } + } + + // 退款:扣物品/潜影盒 + if (rec.shulkerMaterial != null) { + // 盒装:扣一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == rec.shulkerMaterial) { + p.getInventory().clear(i); + break; + } + } + } else { + // 普通:扣商品 + p.getInventory().removeItem( + new ItemStack(rec.material, rec.amount)); + } + + // 退款:加债券 + plugin.getBonds().addBonds( + p.getName(), rec.totalPaid, + "refund", rec.itemId, + "商店系统", "退款" + rec.itemName); + + // 恢复库存 + ShopItem si = findItemById(rec.itemId); + if (si != null && si.getStock() >= 0) + si.setStock(si.getStock() + rec.amount); + + synchronized (refundRecords) { + refundRecords.remove(rec); + } + + p.sendMessage("§a退款成功: §e" + rec.itemName + + " §a+" + rec.totalPaid + "枚债券"); + return true; + } + + + + + public void setPlayerDiscount(Player p, + int pct) { + activeDiscount.put( + p.getUniqueId(), pct); + p.sendMessage("§a已设置 " + pct + + "% 折扣,下次购买生效"); + } + + // ===== 管理员操作 ===== + + public boolean addItemToCategory( + String catName, String id, + String displayName, Material mat, + int buyPrice, int sellPrice, + int stock) { + ShopCategory cat = + findCategory(catName); + if (cat == null) return false; + if (cat.getItem(id) != null) return false; + cat.addItem(new ShopItem(id, displayName, + mat, buyPrice, sellPrice, + stock, 0, 0)); + saveCategory(cat); + return true; + } + + public boolean removeItem(String id) { + for (ShopCategory cat : categories) { + ShopItem item = cat.getItem(id); + if (item != null) { + cat.getItems().remove(item); + saveCategory(cat); + return true; + } + } + return false; + } + + public boolean setStock(String id, int stock) { + ShopItem item = findItemById(id); + if (item == null) return false; + item.setStock(stock); + for (ShopCategory cat : categories) { + if (cat.getItem(id) != null) { + saveCategory(cat); + return true; + } + } + return false; + } + + public ShopCategory findCategory(String n) { + for (ShopCategory c : categories) + if (c.getName().equals(n)) return c; + return null; + } + + public ShopItem findItemById(String id) { + for (ShopCategory c : categories) { + ShopItem item = c.getItem(id); + if (item != null) return item; + } + return null; + } + + public List getCategories() { + return categories; + } + + // ===== 命令处理 ===== + + public boolean handleCommand( + CommandSender s, String[] a) { + if (a.length <= 1) { + if (s instanceof Player) { + openShopMain((Player) s); + } else { + s.sendMessage("§c仅玩家可使用"); + } + return true; + } + + String sub = a[1].toLowerCase(); + + if (sub.equals("reload")) { + loadCategories(); + s.sendMessage("§a商店已重载,共 " + + categories.size() + + " 个分类"); + return true; + } + + if (sub.equals("add")) { + if (a.length < 7) { + s.sendMessage( + "§e用法: shop add <分类>" + + " <名称> <材质>" + + " <购入价>" + + " [售出价-1自动]" + + " [库存-1无限]"); + return true; + } + Material mat; + try { + mat = Material.valueOf( + a[5].toUpperCase()); + } catch (Exception e) { + s.sendMessage( + "§c无效材质: " + a[5]); + return true; + } + int price = Integer.parseInt(a[6]); + int sell = a.length > 7 + ? Integer.parseInt(a[7]) : -1; + int stock = a.length > 8 + ? Integer.parseInt(a[8]) : -1; + if (addItemToCategory(a[2], a[3], + a[4], mat, price, sell, stock)) { + s.sendMessage("§a已添加: " + + a[4] + " → " + a[2]); + } else { + s.sendMessage("§c添加失败" + + "(分类不存在或ID重复)"); + } + return true; + } + + if (sub.equals("remove")) { + if (a.length < 3) { + s.sendMessage( + "§e用法: shop remove "); + return true; + } + if (removeItem(a[2])) { + s.sendMessage( + "§a已移除: " + a[2]); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("set")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop set " + + " <库存数(-1无限)>"); + return true; + } + int stock = Integer.parseInt(a[3]); + if (setStock(a[2], stock)) { + s.sendMessage("§a已设置 " + + a[2] + " 库存为 " + + stock); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("pro")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop pro" + + " <玩家名> <折扣%>"); + return true; + } + Player t = + Bukkit.getPlayerExact(a[2]); + if (t == null) { + s.sendMessage("§c玩家不在线"); + return true; + } + int pct = Integer.parseInt(a[3]); + setPlayerDiscount(t, pct); + s.sendMessage("§a已对 " + a[2] + + " 设置 " + pct + "% 折扣"); + return true; + } + + + s.sendMessage("§e商店子命令:" + + " reload / add / remove" + + " / set / pro "); + return true; + } + @EventHandler(priority = EventPriority.LOW) + public void onCouponChat( + org.bukkit.event.player.AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + if (!couponListening.contains(uuid)) return; + e.setCancelled(true); + couponListening.remove(uuid); + String code = e.getMessage().trim(); + if (code.isEmpty()) { + p.sendMessage("§c已取消输入"); + return; + } + // ★ 预处理:标准化输入 + String normalized = normalizeCouponInput(code); + if (normalized.isEmpty()) { + p.sendMessage("§c优惠券码无效(输入为空或仅含特殊字符)"); + couponListening.remove(uuid); + return; + } + + p.sendMessage("§7正在验证优惠券: §e" + code); + + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "validateCoupon", + String.class); + Object result = m.invoke(sdf1, normalized); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception ex) { + p.sendMessage("§c验证异常: " + + ex.getMessage()); + return; + } + if (discount <= 0) { + p.sendMessage("§c优惠券无效或已使用"); + return; + } + int offPct = 100 - discount; + int current = activeDiscount + .getOrDefault(uuid, 0); + if (offPct > current) { + pendingCouponCode.put(uuid, normalized); + activeDiscount.put(uuid, offPct); + couponApplyTime.put(uuid, System.currentTimeMillis()); + String zhe; + if (discount % 10 == 0) + zhe = (discount / 10) + "折"; + else + zhe = (discount / 10.0) + "折"; + p.sendMessage("§6§l[优惠券] §a验证通过!"); + p.sendMessage("§7折扣: §e" + zhe + + " §7(减" + offPct + "%)"); + p.sendMessage("§7结算时自动核销"); + final UUID u = uuid; + Bukkit.getScheduler().runTask(plugin, () -> { + Player pl = Bukkit.getPlayer(u); + if (pl != null && pl.isOnline()) { + openShopMain(pl); + } + }); + } else { + String curZhe; + if ((100 - current) % 10 == 0) + curZhe = ((100 - current) / 10) + "折"; + else + curZhe = ((100 - current) / 10.0) + "折"; + p.sendMessage("§c当前已有更优折扣: " + + curZhe + ",本次不生效"); + } + } + + /** + * 优惠券输入标准化 + * 全角→半角、去分隔符、转小写 + * "AAA-BBB-CCCC" → "aaaabbbbcccc" + * "AAA-BBB" → "aaabbb" + * "aaa_bbb_cccc" → "aaabbbcccc" + */ + private String normalizeCouponInput(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // 全角数字→半角 + if (c >= '\uFF10' && c <= '\uFF19') { + sb.append((char)(c - 0xFF10 + '0')); + } + // 全角字母→半角 + else if (c >= '\uFF21' && c <= '\uFF3A') { + sb.append((char)(c - 0xFF21 + 'A')); + } + else if (c >= '\uFF41' && c <= '\uFF5A') { + sb.append((char)(c - 0xFF41 + 'a')); + } + // ★ 保留中文字符(Unicode CJK统一汉字范围) + else if ((c >= '\u4E00' && c <= '\u9FFF') + || (c >= '\u3400' && c <= '\u4DBF')) { + sb.append(c); + } + // 保留半角字母和数字 + else if ((c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')) { + sb.append(c); + } + // 分隔符(- _ 空格等)丢弃 + } + return sb.toString().toLowerCase(); + } + + + + + + private String formatZhe(int discount) { + if (discount % 10 == 0) { + return (discount / 10) + "折"; + } + return (discount / 10.0) + "折"; + } + + public boolean redeemCoupon(Player p, String code) { + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + Object result = m.invoke(sdf1, code); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception e) { + return false; + } + if (discount <= 0) return false; + int offPct = 100 - discount; + int cur = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (offPct > cur) { + activeDiscount.put(p.getUniqueId(), offPct); + } + return true; + } + + /** + * 商店主界面点击"优惠券"按钮后调用 + * 开启聊天监听,等待玩家输入优惠券码 + */ + public void openCouponInput(Player p) { + UUID uuid = p.getUniqueId(); + // 先取消已有的待核销优惠券 + pendingCouponCode.remove(uuid); + couponListening.add(uuid); + // 关闭UI + Bukkit.getScheduler().runTask(plugin, () -> { + if (p.isOnline()) p.closeInventory(); + }); + p.sendMessage("§6§l[优惠券] §e请在聊天栏输入优惠券码"); + p.sendMessage("§715秒内输入,过期自动取消"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (couponListening.remove(uuid)) { + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§c优惠券输入已超时取消"); + } + } + }, 300L); + } + + + // ===== GUI: 主界面 ===== + + public void openShopMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l商店"); + int si = 0; + for (ShopCategory cat : categories) { + if (si >= ITEM_SLOTS.length) break; + ItemStack is = new ItemStack( + Material.CHEST); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l" + cat.getName()); + im.setLore(Arrays.asList( + "§7商品: §f" + + cat.getItems().size() + + " 种", + "", + "§a点击进入")); + is.setItemMeta(im); + } + g.setItem(ITEM_SLOTS[si], is); + si++; + } + // ★ 改为跳转订单中心 + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看订单记录、申请退款", + "", "§a点击进入")); + + // 在 openShopMain 方法中,原来的 slot 49/53 附近加: + + g.setItem(48, mkItem( + isCartMode(p) + ? Material.LIME_WOOL + : Material.GRAY_WOOL, + isCartMode(p) + ? "§a§l购物车模式 §7[开]" + : "§7§l购物车模式 §7[关]", + isCartMode(p) + ? "§a当前: 点击商品加入购物车" + : "§7当前: 直接购买", + "§7点击切换")); + g.setItem(50, mkItem(Material.CHEST, + "§6§l购物车 §7[" + getCartCount(p) + "件]", + "§7总价: §6" + getCartTotal(p) + "枚", + "", + "§a点击打开购物车")); + + // 在现有按钮之后,p.openInventory(g) 之前加: + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看购物订单记录", + "", "§a点击进入")); + + // 优惠券按钮 - slot 47 + String discStatus = "§7"; + int curDisc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (curDisc > 0) { + discStatus = "§a当前: " + formatZhe(100 - curDisc); + } + g.setItem(47, mkItem(Material.NAME_TAG, + "§6§l优惠券", + discStatus, + "", + "§a点击输入优惠券码", + "§7一次性使用,核销后失效")); + + g.setItem(53, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + // ===== GUI: 分类 ===== + public void openCategory(Player p, + ShopCategory cat, + int page) { + // ★ 每次重新获取最新对象 ★ + ShopCategory fresh = null; + for (ShopCategory c : categories) { + if (c.getName().equals(cat.getName())) { + fresh = c; + break; + } + } + if (fresh == null) { + openShopMain(p); + return; + } + cat = fresh; + viewCat.put(p.getUniqueId(), cat); + + int total = (int) Math.ceil( + (double) cat.getItems().size() + / PAGE_SIZE); + if (total < 1) total = 1; + if (page < 0) page = 0; + if (page >= total) page = total - 1; + viewPage.put(p.getUniqueId(), page); + + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l" + cat.getName()); + + + int start = page * PAGE_SIZE; + int end = Math.min(start + PAGE_SIZE, + cat.getItems().size()); + + for (int i = start; i < end; i++) { + int idx = i - start; + if (idx >= ITEM_SLOTS.length) break; + + // ★ item 保留原名,代表 ShopItem 数据 + ShopItem item = cat.getItems().get(i); + + // ★ is 是带完整元数据的 ItemStack(用于GUI图标) + ItemStack is = getShopStack(item, 1); + ItemMeta im = is.getItemMeta(); + + if (im != null) { + im.setDisplayName("§e§l" + + item.getDisplayName()); + List lore = new ArrayList<>(); + lore.add("§7购入: §a" + + item.getBuyPrice() + + "§7枚债券"); + lore.add("§7售出: §e" + + item.getEffectiveSellPrice() + + "§7枚债券"); + if (item.getStock() >= 0) + lore.add("§7库存: §f" + + item.getStock()); + else + lore.add("§7库存: §a无限"); + lore.add(""); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (disc > 0) { + int dis = item.getBuyPrice() + * (100 - disc) / 100; + lore.add("§7优惠价: §6" + + dis + "枚" + + " §7(-" + disc + "%)"); + } + lore.add("§a左键买1 §6右键卖1"); + lore.add("§aShift+左键买64" + + " §6Shift+右键卖64"); + im.setLore(lore); + is.setItemMeta(im); + } + + // ★ 用 is(ItemStack)放入GUI格子 + g.setItem(ITEM_SLOTS[idx], is); + } + + + g.setItem(4, mkItem(Material.BOOK, + "§e§l" + cat.getName(), + "§7共 " + cat.getItems().size() + + " 种商品")); + g.setItem(48, page > 0 + ? mkItem(Material.ARROW, + "§7上一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(49, mkItem(Material.PAPER, + "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 + ? mkItem(Material.ARROW, + "§7下一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, + "§7返回商店")); + + p.openInventory(g); + } + + // ===== GUI: 退款 ===== + + public void openRefundPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l退款中心"); + + // 位移检测:比对上次记录的背包内容 + validateRefunds(p); + + // 过期清理 + synchronized (refundRecords) { + refundRecords.removeIf(RefundRecord::isExpired); + } + + int idx = 0; + synchronized (refundRecords) { + for (RefundRecord r : refundRecords) { + if (idx >= 45) break; + if (!r.playerUUID.equals(p.getUniqueId())) continue; + if (r.isExpired()) continue; + + // 盒装用潜影盒图标,普通用商品图标 + Material iconMat = (r.shulkerMaterial != null) + ? r.shulkerMaterial : r.material; + + ItemStack is = new ItemStack(iconMat); + ItemMeta im = is.getItemMeta(); + if (im != null) { + int secs = r.getSecondsLeft(); + im.setDisplayName("§e" + r.itemName + + (r.shulkerMaterial != null + ? " §7[盒装]" : " x" + r.amount)); + + List lore = new ArrayList<>(); + lore.add("§7订单: §f#" + r.timestamp); + lore.add("§7支付: §a" + r.totalPaid + + "§7枚债券"); + + if (r.shulkerMaterial != null) { + int boxCount = countShulkers(p, + r.shulkerMaterial); + lore.add("§7潜影盒: §f" + + (boxCount >= 1 + ? "§a在背包中" + : "§c已移出")); + } else { + int have = countDirect(p, r.material); + lore.add("§7物品: §f" + have + + "/" + r.amount); + } + + lore.add("§7剩余: §e" + secs + "秒"); + lore.add(""); + lore.add("§c点击退款"); + im.setLore(lore); + } + is.setItemMeta(im); + g.setItem(10 + idx, is); + idx++; + } + } + + if (idx == 0) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无可退款记录")); + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + + p.openInventory(g); + } + // 统计背包中某物品数量 + private int countInInventory(Player p, Material mat) { + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == mat) { + count += is.getAmount(); + } + } + // 潜影盒也要检查里面 + for (ItemStack is : p.getInventory().getContents()) { + if (is != null + && is.getType().name() + .endsWith("_SHULKER_BOX")) { + count += is.getAmount(); + } + } + return count; + } + + // 只统计背包中直接可见的数量(不含盒内) + // 新代码:增加按完整物品匹配的方法 + private int countDirect(Player p, Material mat) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.getType() == mat) + count += is.getAmount(); + } + return count; + } + + private int countDirectByStack(Player p, + ItemStack target) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.isSimilar(target)) + count += is.getAmount(); + } + return count; + } + // 统计背包中潜影盒数量 + private int countShulkers(Player p, Material shulker) { + if (shulker == null) return Integer.MAX_VALUE; + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == shulker) + count += is.getAmount(); + } + return count; + } + + // 退款检测:位移 OR 消耗 → 不能退 + private void validateRefunds(Player p) { + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { + it.remove(); + continue; + } + + if (r.shulkerMaterial != null) { + // 盒装:检测潜影盒是否还在背包 + int now = countShulkers( + p, r.shulkerMaterial); + if (now < 1) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 潜影盒已移出背包,无法退款"); + } + } else { + // 普通:检测商品是否还在背包 + int now = countDirect(p, r.material); + if (now < r.amount) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 物品已被使用或移出,无法退款"); + } + } + } + } + } + + + public int getPlayerPage(java.util.UUID uuid) { + return viewPage.getOrDefault(uuid, 0); + } + + public ShopCategory getPlayerCategory(java.util.UUID uuid) { + return viewCat.get(uuid); + } + + // ===== 供GUIManager调用:商店主界面点击 ===== + public boolean handleMainShopClick(Player p, int raw) { + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true; } + + if (raw == 48) { + toggleCartMode(p); + openShopMain(p); + return true; + } + if (raw == 50) { + openCart(p); + return true; + } + if (raw == 53) { + plugin.getGui().openMain(p); + return true; + } + + // ★ 新增:优惠券按钮 + if (raw == 47) { + openCouponInput(p); + return true; + } + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true;} + int idx = slotToIndex(raw); + if (idx < 0 || idx >= categories.size()) + return true; + openCategory(p, categories.get(idx), 0); + return true; + } + + // ===== 供GUIManager调用:退款中心点击 ===== + public boolean handleRefundClick(Player p, int raw) { + if (raw == 49) { openShopMain(p); return true; } + return true; + } + + // ===== 供GUIManager调用:分类页点击 ===== + public boolean handleCategoryClick(Player p, String title, + int raw, boolean left, + boolean shift) { + // plugin.getLogger().info("[Shop] ★ handleCategoryClick raw=" + raw); + + ShopManager sm = this; + ShopCategory match = null; + for (ShopCategory c : categories) { + if (title.equals("§6§l" + c.getName())) { + match = c; + break; + } + } + if (match == null) { + // plugin.getLogger().info("[Shop] ★ 匹配失败, title=[" + title + "]"); + return false; + } + // plugin.getLogger().info("[Shop] ★ 匹配到: " + match.getName()); + + viewCat.put(p.getUniqueId(), match); + + if (raw == 53) { openShopMain(p); return true; } + int pg = getPlayerPage(p.getUniqueId()); + if (raw == 48) { openCategory(p, match, pg - 1); return true; } + if (raw == 50) { openCategory(p, match, pg + 1); return true; } + + int itemIdx = slotToIndex(raw); + if (itemIdx < 0) { + // plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); + return true; + } + int actual = itemIdx + pg * PAGE_SIZE; + if (actual < 0 || actual >= match.getItems().size()) { + // plugin.getLogger().info("[Shop] ★ 超出范围 actual=" + actual + " size=" + match.getItems().size()); + return true; + } + ShopItem si = match.getItems().get(actual); + // plugin.getLogger().info("[Shop] ★ 准备购买: " + si.getId() + " left=" + left); + + int amount = shift ? 64 : 1; + if (left) { + if (shift && isCartMode(p)) { addToCart(p, si); } + else if (shift) { buyItem(p, si, 64); } + else if (isCartMode(p)) { addToCart(p, si); } + else { buyItem(p, si, 1); } + } else { + if (shift) { sellAllOf(p, si); } + else { sellItem(p, si, 1); } + } + openCategory(p, match, pg); + return true; + } + + // ===== 批量出售某商品全部数量 ===== + private void sellAllOf(Player p, ShopItem item) { + if (item == null) return; + // ★ 补上禁止出售检查 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return; + } + if (item == null) return; + Material mat = item.getMaterial(); + int total = 0; + int count = 0; + // 遍历背包清点 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + total += is.getAmount(); + count++; + } + } + if (total <= 0) { + p.sendMessage("§c你没有可出售的 §e" + + item.getDisplayName()); + return; + } + // 清除背包中该物品 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + } + } + int money = item.getEffectiveSellPrice() * total; + plugin.getBonds().addBonds( + p.getName(), money, "shop_sell", + item.getId(), "商店系统", + "批量出售" + item.getDisplayName() + + " x" + total); + if (item.getStock() >= 0) + item.setStock(item.getStock() + total); + p.sendMessage("§a批量出售: §e" + + item.getDisplayName() + + " x" + total + + " §a+" + money + "枚债券"); + } + + + // ===== 定时 ===== + + private void startHourlyTask() { + new BukkitRunnable() { + @Override + public void run() { + // ★ 汇报交给 SalesStatsManager + if (plugin.getSalesStats() != null) { + plugin.getSalesStats().report(); + } + // 重置小时销量 + for (ShopCategory cat : categories) + for (ShopItem item : cat.getItems()) + item.setHourlySales(0); + saveAll(); + } + }.runTaskTimer(plugin, 72000L, 72000L); + } + + + private void startCleanupTask() { + new BukkitRunnable() { + @Override + public void run() { + synchronized (refundRecords) { + refundRecords.removeIf( + RefundRecord::isExpired); + } + } + }.runTaskTimer(plugin, 1200L, 1200L); + } + + // ===== 工具 ===== + + private int slotToIndex(int slot) { + for (int i = 0; i < ITEM_SLOTS.length; i++) + if (ITEM_SLOTS[i] == slot) return i; + return -1; + } + + private ItemStack mkItem(Material mat, + String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/TicketManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/TicketManager.java new file mode 100644 index 0000000..c21edc2 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/TicketManager.java @@ -0,0 +1,1644 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TicketManager { + + private final Main plugin; + + public static final String T_TICKET_MAIN = + "§6§l工单中心"; + public static final String T_TICKET_CREATE = + "§e§l创建工单"; + public static final String T_TICKET_MY = + "§b§l我的工单"; + public static final String T_TICKET_ADMIN = + "§c§l工单管理"; + public static final String T_TICKET_DETAIL = + "§a§l工单详情"; + + public static final String TYPE_BUG = + "bug反馈"; + public static final String TYPE_HELP = + "求助"; + public static final String TYPE_REPORT = + "举报"; + public static final String TYPE_APPLY = + "申请"; + public static final String TYPE_OTHER = + "其他"; + public static final String TYPE_DELETE = + "删号"; + public static final String TYPE_PASSWORD = + "修改密码"; + + + + private static final Map + PRICES = new LinkedHashMap<>(); + static { + PRICES.put(TYPE_BUG, 10); + PRICES.put(TYPE_HELP, 5); + PRICES.put(TYPE_REPORT, 8); + PRICES.put(TYPE_APPLY, 15); + PRICES.put(TYPE_OTHER, 5); + PRICES.put(TYPE_DELETE, 0); + PRICES.put(TYPE_PASSWORD, 0); + } + + + public TicketManager(Main plugin) { + this.plugin = plugin; + } + + // ========== GUI ========== + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_MAIN); + fillBg(g); + g.setItem(10, mk(Material.PAPER, + "§e创建工单", + "§7提交bug、求助、举报等")); + g.setItem(12, mk(Material.BOOK, + "§b我的工单", + "§7查看已提交的工单")); + g.setItem(14, mk(Material.CHEST, + "§6工单记录", + "§7查看历史工单")); + if (adm(p)) { + g.setItem(16, mk(Material.REDSTONE, + "§c工单管理", + "§7管理员专用")); + } + if (plugin.getDb() + .isServiceProvider( + p.getName())) { + g.setItem(18, mk(Material.EMERALD, + "§a服务商面板", + "§7接单、处理工单")); + } + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openCreate(Player p) { + Inventory g = Bukkit.createInventory( + null, 36, T_TICKET_CREATE); + fillBg(g); + g.setItem(10, mk(Material.BOOK, + "§cbug反馈")); + g.setItem(11, mk(Material.EMERALD, + "§a求助")); + g.setItem(12, mk(Material.BLAZE_POWDER, + "§6举报")); + g.setItem(13, mk(Material.NAME_TAG, + "§e申请")); + g.setItem(14, mk(Material.PAPER, + "§7其他")); + g.setItem(20, mk(Material.BARRIER, + "§c申请删号", + "§7申请删除账号")); + g.setItem(21, mk(Material.NETHER_WART, + "§d申请改密", + "§7请求管理员协助改密")); + g.setItem(31, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyTickets(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_MY); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByPlayer( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openDetail(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_DETAIL); + fillBg(g); + + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = sv(t, "requester"); + String title = sv(t, "title"); + String desc = sv(t, "description"); + String assigned = sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + int confirmed = nv(t, "admin_confirmed"); + + g.setItem(10, mk(Material.PAPER, + "§e§l" + (title.isEmpty() + ? type : title), + "§7类型: §f" + type, + "§7状态: " + stxt(status), + "§7提交者: §f" + requester, + "§7奖励: §e" + reward + " 积分")); + + g.setItem(12, mk(Material.BOOK, + "§7详细描述", + desc.isEmpty() ? "§7无描述" + : "§7" + (desc.length() > 40 + ? desc.substring(0, 40) + + "..." : desc))); + + if (!assigned.isEmpty()) { + g.setItem(14, mk(Material.PLAYER_HEAD, + "§7处理人: §f" + assigned, + confirmed == 1 + ? "§7评分: §e" + score + : "§7处理中")); + } else { + g.setItem(14, mk(Material.BARRIER, + "§7未分配")); + } + + // 驳回原因 + if ("closed".equals(status)) { + String reason = sv(t, "reject_reason"); + if (!reason.isEmpty()) { + g.setItem(13, mk(Material.BARRIER, + "§c驳回原因", + "§7" + reason)); + } + } + + // 回复按钮(非关闭/已完结状态) + if (!"closed".equals(status) + && !"withdrawn".equals(status) + && !"resolved".equals(status)) { + g.setItem(15, mk( + Material.WRITABLE_BOOK, + "§b回复工单", + "§7向对方发送消息")); + g.setItem(16, mk(Material.BOOK, + "§7查看回复记录")); + } + + // ===== 管理员按钮 ===== + if (adm(p)) { + if ("pending".equals(status)) { + // 待分配:可分配给服务商 + g.setItem(19, mk(Material.EMERALD, + "§a分配工单", + "§7指定服务商处理")); + // 也可自己处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && !assigned.equals("admin") + && !assigned.isEmpty()) { + // 服务商在处理:可取消指派收回 + g.setItem(19, mk(Material.BARRIER, + "§c取消指派", + "§7取消服务商指派,收回工单")); + // 也可自己接手处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && assigned.equals("admin")) { + // 管理员自己在处理:可标记完结 + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + if ("replied".equals(status) + && assigned.equals("admin")) { + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + // 驳回(非已关闭/已完结状态) + if (!"closed".equals(status) + && !"resolved".equals(status) + && !"withdrawn".equals(status)) { + g.setItem(21, mk(Material.RED_DYE, + "§c驳回工单", + "§7驳回并关闭此工单")); + } + } + + // ===== 服务商按钮 ===== + if (plugin.getDb() + .isServiceProvider(p.getName()) + && !TYPE_DELETE.equals(type) + && !TYPE_PASSWORD.equals(type)) { + if ("pending".equals(status) + && assigned.isEmpty()) { + // 待分配无人:抢单 + g.setItem(19, mk( + Material.GOLD_INGOT, + "§6抢单", + "§7接取此工单")); + } + if (assigned.equals(p.getName()) + && ("processing".equals(status) + || "replied".equals(status))) { + // 我在处理:标记完结/上报异常 + g.setItem(19, mk( + Material.LIME_DYE, + "§a标记完结", + "§7上报处理完成")); + g.setItem(20, mk( + Material.REDSTONE, + "§c上报异常", + "§7报告处理中的问题")); + } + } + + g.setItem(22, mk(Material.ARROW, + "§7返回")); + + ItemStack info = g.getItem(10); + if (info != null) { + ItemMeta im = info.getItemMeta(); + if (im != null) { + im.setCustomModelData(tid); + info.setItemMeta(im); + } + } + p.openInventory(g); + } + + + public void openAdminList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_ADMIN); + fillBg(g); + g.setItem(45, mk(Material.EMERALD, + "§a邀请服务商")); + g.setItem(46, mk(Material.BARRIER, + "§c移除服务商")); + g.setItem(47, mk(Material.CLOCK, + "§e服务商列表")); + g.setItem(48, mk(Material.NETHER_STAR, + "§e直接派单", + "§7管理员直接创建并派单")); + List> all = + new ArrayList<>(); + all.addAll(plugin.getDb() + .getTicketsByStatus("pending")); + all.addAll(plugin.getDb() + .getTicketsByStatus("processing")); + all.addAll(plugin.getDb() + .getTicketsByStatus("replied")); + all.addAll(plugin.getDb() + .getTicketsByStatus("completed")); + all.addAll(plugin.getDb() + .getTicketsByStatus("resolved")); + all.addAll(plugin.getDb() + .getTicketsByStatus("closed")); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + + public void openProviderPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§d§l服务商面板"); + fillBg(g); + g.setItem(10, mk(Material.CHEST, + "§e抢单大厅", + "§7查看可接工单")); + g.setItem(12, mk(Material.CLOCK, + "§b我的工单", + "§7我正在处理的工单")); + g.setItem(14, mk(Material.PAPER, + "§6历史工单", + "§7已完成的工单")); + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openGrabList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l抢单大厅"); + fillBg(g); + List> pending = + plugin.getDb() + .getTicketsByStatus( + "pending"); + List> list = + new ArrayList<>(); + for (Map t : pending) { + if (sv(t, "assigned_to").isEmpty()) { + list.add(t); + } + } + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyAssigned(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l我处理中的工单"); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByProvider( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + // ========== 业务 ========== + + public int submitTicket(Player p, + String type, String title, + String desc) { + int price = PRICES + .getOrDefault(type, 5); + int id = plugin.getDb().createTicket( + type, "pending", "normal", + p.getName(), title, desc, + price, ""); + if (id > 0) { + if (TYPE_DELETE.equals(type) + || TYPE_PASSWORD.equals(type)) { + plugin.getDb().updateTicketField( + id, "assigned_to", "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage( + "§a§l[工单] §f工单 #" + + id + " 已提交," + + "等待管理员处理"); + } else { + p.sendMessage( + "§a§l[工单] §f创建成功!" + + " #§e" + id + + " 奖励: §e" + + price + "积分"); + } + notifyAdmins(p.getName(), id); + } else { + p.sendMessage("§c创建失败"); + } + return id; + } + + + public void adminCreateAndAssign( + Player p, String type, + String title, String desc, + String provider, int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + int id = plugin.getDb().createTicket( + type, "processing", "normal", + p.getName(), title, desc, + price, ""); + if (id <= 0) { + p.sendMessage("§c创建失败"); + return; + } + plugin.getDb().assignTicket( + id, provider); + if (price > 0) { + plugin.getDb().updateTicketField( + id, "reward_amount", price); + } + p.sendMessage("§a工单 #" + id + + " 已创建并分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f管理员指派工单 #" + + id + " 给您"); + playAssignedSound(tp); + } + } + + public void grabTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!"pending".equals( + sv(t, "status"))) { + p.sendMessage("§c该工单已被接单"); + return; + } + if (!sv(t, "assigned_to").isEmpty()) { + p.sendMessage("§c该工单已被分配"); + return; + } + plugin.getDb().assignTicket( + tid, p.getName()); + p.sendMessage("§a抢单成功!工单 #" + tid); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f您的工单 #" + + tid + " 已被 " + + p.getName() + + " 接单"); + } + } + + public void assignToProvider(Player p, + int tid, String provider, + int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + plugin.getDb().assignTicket( + tid, provider); + if (price > 0) { + plugin.getDb().updateTicketStatus( + tid, "processing"); + plugin.getDb().updateTicketField( + tid, "reward_amount", price); + } + p.sendMessage("§a工单 #" + tid + + " 已分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f您有新工单: #" + + tid); + playAssignedSound(tp); + } + } + + public void confirmAndScore(Player p, + int tid, int score) { + if (score < 1 || score > 5) { + p.sendMessage("§c评分1-5"); + return; + } + plugin.getDb().confirmTicket( + tid, score); + p.sendMessage("§a工单 #" + tid + + " 已确认,评分: " + score); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已确认完结"); + } + } + settleTicket(tid); + } + + public void completeAsProvider(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "assigned_to") + .equals(p.getName())) { + p.sendMessage("§c这不是你的工单"); + return; + } + plugin.getDb().completeTicket(tid); + p.sendMessage("§a工单 #" + tid + + " 已标记完结,等待确认"); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已处理完成"); + } + } + + public void rejectTicket(Player p, + int tid, String reason) { + plugin.getDb().updateTicketStatus( + tid, "closed"); + plugin.getDb().updateTicketField( + tid, "reject_reason", reason); + p.sendMessage("§c工单 #" + tid + + " 已驳回并关闭"); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 被驳回: " + + reason); + } + } + } + + public void replyTicket(Player p, + int tid, String message) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + String status = sv(t, "status"); + if ("closed".equals(status) + || "withdrawn".equals(status) + || "resolved".equals(status)) { + p.sendMessage("§c该工单已关闭"); + return; + } + plugin.getDb().addTicketReply( + tid, p.getName(), message); + p.sendMessage("§a回复成功"); + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + String sender = p.getName(); + if (!requester.equals(sender)) { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + if (!assigned.isEmpty() + && !assigned.equals(sender)) { + Player ap = Bukkit.getPlayer( + assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + } + + public void showReplies(Player p, + int tid) { + List> replies = + plugin.getDb() + .getTicketReplies(tid); + if (replies.isEmpty()) { + p.sendMessage("§7暂无回复"); + return; + } + p.sendMessage("§e§l=== 回复记录 ==="); + SimpleDateFormat sdf = + new SimpleDateFormat( + "MM-dd HH:mm"); + for (Map r : replies) { + String sender = sv(r, "sender"); + String msg2 = sv(r, "message"); + long time = ((Number) r.get( + "create_time")).longValue(); + p.sendMessage("§7[" + sdf.format( + new Date(time)) + "] §f" + + sender + "§7: " + msg2); + } + p.sendMessage( + "§e§l================="); + } + + public void withdrawTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "requester") + .equals(p.getName()) + && !adm(p)) { + p.sendMessage("§c无权操作"); + return; + } + plugin.getDb().updateTicketStatus( + tid, "withdrawn"); + p.sendMessage("§a工单 #" + tid + + " 已撤回"); + } + + public void showProviderList(Player p) { + List> list = + plugin.getDb() + .getAllServiceProviders(); + if (list.isEmpty()) { + p.sendMessage("§7暂无服务商"); + return; + } + p.sendMessage( + "§e§l=== 服务商列表 ==="); + for (Map pro : list) { + p.sendMessage("§7- §f" + + pro.get("player_name") + + " §7角色: §e" + + pro.get("role")); + } + p.sendMessage( + "§e§l==================="); + } + + public void autoProcessCompleted() { + long timeoutMs = 10 * 60 * 1000L; + + // 自动处理删号工单(5分钟自动通过) + List> deleteTickets = + plugin.getDb() + .getTicketsByStatus( + "processing"); + for (Map t : deleteTickets) { + String dtype = sv(t, "type"); + if (!TYPE_DELETE.equals(dtype)) continue; + int did = nv(t, "id"); + int dconf = nv(t, "admin_confirmed"); + // 已经处理过了跳过 + if (dconf != 0) continue; + String dasg = sv(t, "assigned_to"); + // 只处理分配给admin的 + if (!"admin".equals(dasg)) continue; + long createTime = ((Number) t.get( + "create_time")).longValue(); + if (System.currentTimeMillis() + - createTime >= 5 * 60 * 1000L) { + processDeleteAccount(did, t); + } + } + + + // 已完结未确认→10分钟自动完结 + List> list = + plugin.getDb() + .getUnconfirmedCompleted( + timeoutMs); + for (Map t : list) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 已自动完结"); + String assigned = + sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) + pp.sendMessage( + "§e§l[工单] §f工单 #" + + id + + " 已自动完结"); + } + settleTicket(id); + } + + // 已回复但10分钟无互动→自动完结 + List> stale = + plugin.getDb() + .getRepliedStaleTickets( + timeoutMs); + for (Map t : stale) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 超时自动完结"); + settleTicket(id); + } + } + + + + // ========== GUI点击 ========== + public boolean handleClick(Player p, + String title, int slot) { + + if (title.equals(T_TICKET_MAIN)) { + if (slot == 10) openCreate(p); + else if (slot == 12) + openMyTickets(p); + else if (slot == 14) + openMyTickets(p); + else if (slot == 16) { + if (adm(p)) openAdminList(p); + else p.sendMessage("§c权限不足"); + } + else if (slot == 18) { + if (plugin.getDb() + .isServiceProvider(p.getName())) { + openProviderPanel(p); + } else { + p.sendMessage("§c权限不足"); + } + } + else if (slot == 22) + plugin.getGui().openMain(p); + return true; + } + + if (title.equals(T_TICKET_CREATE)) { + if (slot == 31) { + openMain(p); + return true; + } + String type = null; + if (slot == 10) type = TYPE_BUG; + else if (slot == 11) type = TYPE_HELP; + else if (slot == 12) type = TYPE_REPORT; + else if (slot == 13) type = TYPE_APPLY; + else if (slot == 14) type = TYPE_OTHER; + else if (slot == 20) type = TYPE_DELETE; + else if (slot == 21) type = TYPE_PASSWORD; + if (type != null) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_TITLE; + plugin.getChatInput() + .getState(p) + .ticketType = type; + if (TYPE_DELETE.equals(type)) { + p.sendMessage("§e请描述删号原因:"); + } else if (TYPE_PASSWORD.equals(type)) { + p.sendMessage("§e请描述需要协助的内容:"); + } else { + p.sendMessage("§e请输入工单标题:"); + } + } + return true; + } + + + if (title.equals(T_TICKET_MY)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals(T_TICKET_DETAIL)) { + return handleClickDetail(p, slot); + } + + if (title.equals(T_TICKET_ADMIN)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot == 45) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_INVITE_PROVIDER; + p.sendMessage("§e输入服务商玩家名:"); + return true; + } + if (slot == 46) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REMOVE_PROVIDER; + p.sendMessage("§e输入要移除的服务商:"); + return true; + } + if (slot == 47) { + showProviderList(p); + return true; + } + if (slot == 48) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ADMIN_TYPE; + p.sendMessage("§e选择工单类型:"); + p.sendMessage("§7bug/求助/举报/申请/其他"); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§d§l服务商面板")) { + if (slot == 22) { + plugin.getGui().openMain(p); + return true; + } + if (slot == 10) openGrabList(p); + else if (slot == 12) openMyAssigned(p); + else if (slot == 14) openMyTickets(p); + return true; + } + + if (title.equals("§6§l抢单大厅")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§b§l我处理中的工单")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + return false; + } + + private boolean handleClickDetail(Player p, + int slot) { + if (slot == 22) { + openMyTickets(p); + return true; + } + int id = gtFromDetail(p); + if (id <= 0) return true; + Map t = + plugin.getDb().getTicket(id); + if (t == null) return true; + + String st = sv(t, "status"); + int conf = nv(t, "admin_confirmed"); + String asg = sv(t, "assigned_to"); + + // 回复按钮 + if (slot == 15) { + if (!"closed".equals(st) + && !"withdrawn".equals(st) + && !"resolved".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPLY; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入回复内容:"); + } + return true; + } + + // 查看回复记录 + if (slot == 16) { + showReplies(p, id); + return true; + } + + // slot 19 + if (slot == 19) { + // 管理员:分配工单 + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ASSIGN; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入: 服务商名 价格"); + return true; + } + // 管理员:取消指派(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + cancelTicket(p, id); + openDetail(p, id); + return true; + } + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); + return true; + } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; + } + } + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && "pending".equals(st) + && asg.isEmpty()) { + grabTicket(p, id); + openDetail(p, id); + return true; + } + // 服务商:标记完结 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + completeAsProvider(p, id); + openDetail(p, id); + return true; + } + return true; + } + + // slot 20 + if (slot == 20) { + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:自己接手(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 服务商:上报异常 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPORT_ISSUE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入异常描述:"); + return true; + } + return true; + } + + // slot 21:驳回 + if (slot == 21) { + if (adm(p) && !"closed".equals(st) + && !"resolved".equals(st) + && !"withdrawn".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REJECT; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入驳回原因:"); + } + return true; + } + + return true; + } + + // ========== 聊天输入 ========== + + public boolean handleChatInput(Player p, + String msg, + ChatInputManager.InputType type) { + switch (type) { + case TICKET_TITLE: { + plugin.getChatInput() + .getState(p) + .ticketTitle = msg; + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return true; + } + case TICKET_DESC: { + String tt = plugin.getChatInput() + .getState(p).ticketType; + String tl = plugin.getChatInput() + .getState(p).ticketTitle; + plugin.getChatInput().reset(p); + submitTicket(p, tt, tl, msg); + return true; + } + case TICKET_SCORE: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + Map tk = + plugin.getDb() + .getTicket(tid2); + String st = tk != null + ? sv(tk, "status") + : ""; + if ("completed".equals(st)) { + plugin.getChatInput() + .reset(p); + confirmAndScore( + p, tid2, sc); + } else { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c工单尚未处理完成"); + } + } catch ( + NumberFormatException e) { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return true; + } + case TICKET_REJECT: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + plugin.getChatInput().reset(p); + rejectTicket(p, tid2, msg); + return true; + } + case TICKET_ASSIGN: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + plugin.getChatInput().reset(p); + assignToProvider(p, tid2, + prov, price); + runSync(() -> + openDetail(p, tid2)); + return true; + } + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return true; + } + plugin.getDb() + .addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return true; + } + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .isServiceProvider( + tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return true; + } + plugin.getDb() + .removeServiceProvider( + tgt); + p.sendMessage( + "§a已移除 " + tgt); + return true; + } + default: + return false; + } + } + + // ========== 私有 ========== + + private void settleTicket(int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) return; + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + ConfigManager cfg = + plugin.getConfig2(); + if (reward > 0) { + plugin.getDb().addPoints( + requester, reward); + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 积分奖励"); + } + } + if (!assigned.isEmpty()) { + int pts = (int) (score + * cfg.providerPointsPerScore); + if (pts > 0) { + plugin.getDb().addPoints( + assigned, pts); + } + double money = + cfg.baseEconomyReward + + score + * cfg.providerEconomyPerScore; + boolean ok = false; + try { + if (plugin.getEconomy() != null) { + OfflinePlayer op = + Bukkit.getOfflinePlayer( + assigned); + plugin.getEconomy() + .depositPlayer( + op, money); + ok = true; + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 经济失败: " + + e.getMessage()); + } + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (pts > 0) + pp.sendMessage("§7积分: §e" + + pts); + if (ok) + pp.sendMessage("§7经济: §a$" + + String.format( + "%.0f", money)); + } + } + } + + private void notifyAdmins(String creator, + int tid) { + String tag = + plugin.getConfig2().adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§e§l[工单] §f" + creator + + " 提交了新工单 #" + + tid); + } + } + } + + private void playAssignedSound(Player p) { + for (int i = 0; i < 3; i++) { + final int d = i * 15; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline()) { + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + }, d); + } + } + + private void runSync(Runnable task) { + Bukkit.getScheduler() + .runTask(plugin, task); + } + + private boolean adm(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory g) { + ItemStack glass = mk( + Material.GRAY_STAINED_GLASS_PANE, + " "); + for (int i = 0; i < g.getSize(); i++) { + if (g.getItem(i) == null + || g.getItem(i).getType() + == Material.AIR) { + g.setItem(i, glass); + } + } + } + + private void fillList(Inventory g, + List> list) { + if (list.isEmpty()) { + g.setItem(22, mk(Material.BARRIER, + "§7暂无工单")); + return; + } + int slot = 0; + for (Map t : list) { + if (slot >= 45) break; + g.setItem(slot, makeItem(t)); + slot++; + } + } + /** + * 执行删号流程 + */ + /** + * 执行删号流程 + */ + public void processDeleteAccount(int tid, + Map t) { + String requester = sv(t, "requester"); + plugin.getDb().updateTicketStatus( + tid, "resolved"); + plugin.getDb().updateTicketField(tid, + "admin_confirmed", 1); + Player target = + Bukkit.getPlayer(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + // 延迟执行删号 + final String name = requester; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + plugin.getDb().deleteUser(name); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 删号工单已执行"); + }, 100L); + } + + /** + * 管理员手动批准删号 + */ + public void approveDeleteTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!TYPE_DELETE.equals(sv(t, "type"))) { + p.sendMessage("§c这不是删号工单"); + return; + } + p.sendMessage("§a工单 #" + tid + + " 已批准,正在执行删号..."); + processDeleteAccount(tid, t); + } + + /** + * 管理员取消工单(服务商异常后) + */ + /** + * 管理员取消工单(解除服务商绑定,管理员可自行处理) + */ + public void cancelTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + // 解除服务商绑定,状态回退到pending + plugin.getDb().updateTicketField( + tid, "assigned_to", ""); + plugin.getDb().updateTicketStatus( + tid, "pending"); + p.sendMessage("§a工单 #" + tid + + " 已取消指派,回到待分配状态"); + // 通知原服务商 + String assigned = sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player ap = Bukkit.getPlayer(assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 已被管理员取消指派"); + } + } + } + + + /** + * 服务商查看历史工单 + */ + public void openProviderHistory(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l我的历史工单"); + fillBg(g); + List> all = + plugin.getDb() + .getTicketsByProviderAll( + p.getName()); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private ItemStack makeItem( + Map t) { + int id = nv(t, "id"); + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = + sv(t, "requester"); + String title = sv(t, "title"); + int reward = nv(t, "reward_amount"); + Material mat; + switch (status) { + case "processing": + mat = Material.YELLOW_DYE; + break; + case "replied": + mat = Material.CYAN_DYE; + break; + case "completed": + mat = Material.LIME_DYE; + break; + case "resolved": + mat = Material.LIME_DYE; + break; + case "closed": + mat = Material.GRAY_DYE; + break; + case "rejected": + mat = Material.RED_DYE; + break; + case "withdrawn": + mat = Material.GRAY_DYE; + break; + default: + mat = Material.ORANGE_DYE; + break; + } + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName("§e#" + id + + " " + (title.isEmpty() + ? type : title)); + List lore = + new ArrayList<>(); + lore.add("§7类型: §f" + type); + lore.add("§7状态: " + + stxt(status)); + lore.add("§7提交者: §f" + + requester); + lore.add("§7奖励: §e" + reward + + " 积分"); + lore.add(""); + lore.add("§7点击查看详情"); + im.setLore(lore); + im.setCustomModelData(id); + item.setItemMeta(im); + } + return item; + } + + private int gt(ItemStack item) { + if (item == null) return -1; + ItemMeta im = item.getItemMeta(); + if (im == null) return -1; + if (im.hasCustomModelData()) + return im.getCustomModelData(); + return -1; + } + + private int gtFromDetail(Player p) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(10); + return gt(it); + } + + private String stxt(String status) { + switch (status) { + case "pending": + return "§e待处理"; + case "processing": + return "§b处理中"; + case "replied": + return "§9已回复"; + case "completed": + return "§a待确认"; + case "resolved": + return "§a已完结"; + case "closed": + return "§7已关闭"; + case "rejected": + return "§c已驳回"; + case "withdrawn": + return "§7已撤回"; + default: + return "§7" + status; + } + } + + private String sv(Map m, + String key) { + Object v = m.get(key); + return v != null + ? String.valueOf(v) : ""; + } + + private int nv(Map m, + String key) { + Object v = m.get(key); + return v instanceof Number + ? ((Number) v).intValue() : 0; + } + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) { + if (!l.isEmpty()) + list.add(l); + } + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/UpdateChecker.java b/backup/20260605_152529/src/main/java/Sdf1_login/UpdateChecker.java new file mode 100644 index 0000000..c5c01bb --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/UpdateChecker.java @@ -0,0 +1,523 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = + "ypsdf1"; + private static final String GITHUB_REPO = + "sdf1_login"; + private static final String GITEE_OWNER = + "nihaoshidifu"; + private static final String GITEE_REPO = + "sdf1_login"; + + + // API 地址 + private static final String GITHUB_API = + "https://api.github.com/repos/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + + "/tags"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + + GITEE_OWNER + "/" + + GITEE_REPO + + "/tags"; + + // 仓库页面链接(给用户看的) + private static final String GITHUB_LINK = + "https://github.com/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + + GITEE_OWNER + "/" + + GITEE_REPO + "/releases"; + + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 3000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = + false; + private volatile int failCount = 0; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = + false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = + plugin.getDescription() + .getVersion(); + this.disabledFile = new File( + plugin.getDataFolder(), + "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = + new BufferedReader( + new FileReader( + disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = + "true".equals( + line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = + new PrintWriter( + new FileWriter( + disabledFile))) { + pw.println(autoCheckDisabled + ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) return; + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doCheck(null, true)); + } + + /** + * /sdf1_login update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + * 只有拿到版号才解除熔断 + */ + public void checkUpdate( + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> + doCheck(sender, + false)); + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, + boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled + || circuitTripped) { + return; + } + } + + // 通道1:GitHub + CheckResult gh = + tryChannel(GITHUB_API, + "GitHub", GITHUB_LINK); + + // 通道2:Gitee + CheckResult ge = + tryChannel(GITEE_API, + "Gitee", GITEE_LINK); + + // 选择成功的结果 + CheckResult chosen = null; + String failMsg = null; + + if (gh.isSuccess()) { + chosen = gh; + } else if (ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + // 拼接两个通道的失败原因 + StringBuilder sb = + new StringBuilder(); + if (gh.error != null) { + sb.append("§7GitHub: §c") + .append(gh.error); + } + if (ge.error != null) { + if (sb.length() > 0) + sb.append("\n"); + sb.append("§7Gitee: §c") + .append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_login] 更新检测" + + "已熔断,连续失败" + + failCount + "次"); + } + if (sender != null) { + final String msg = + sb.toString(); + final boolean tripped = + circuitTripped; + final int fc = failCount; + Bukkit.getScheduler() + .runTask(plugin, () -> { + sender.sendMessage( + "§c§l[更新] " + + "检测失败"); + sender.sendMessage(msg); + sender.sendMessage( + "§e失败次数: " + + fc + "/" + + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage( + "§c已熔断," + + "请重启服务器" + + "重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_login] 远程版本信号" + + " \"" + chosen.version + + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler() + .runTask(plugin, () -> + sender.sendMessage( + "§e[更新] 远程已" + + "禁用自动" + + "更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + Bukkit.getScheduler() + .runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage( + "§e[更新] 检测成功" + + " via: §a" + src); + sender.sendMessage( + "§e[更新] 本地版本:" + + " §f" + + currentVersion); + sender.sendMessage( + "§e[更新] 远程版本:" + + " §f" + rv); + + if (isNewer(rv, + currentVersion)) { + sender.sendMessage( + "§c[更新] 发现新" + + "版本: " + rv); + sender.sendMessage( + "§a请前往仓库下载" + + "更新:"); + sender.sendMessage( + "§a" + lnk); + } else { + sender.sendMessage( + "§a[更新] 已是" + + "最新版本"); + } + + // 显示另一通道的失败原因 + // (仅信息,不影响结果) + CheckResult other = + (src.equals("GitHub")) + ? ge : gh; + if (!other.isSuccess() + && other.error != null) { + sender.sendMessage( + "§7[更新] " + + other.source + + "不可用: " + + other.error); + } + }); + } + + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal( + String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) + || "关闭".equals(v) + || "false".equals(v) + || "禁用".equals(v); + } + + // ========== HTTP 请求(带超时) ========== + + // ========== 检测结果 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, + String source, + String link, + String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + +// ========== HTTP 请求 ========== + + private CheckResult tryChannel( + String apiUrl, + String sourceName, + String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) + url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; " + + "Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty( + "Accept", + "*/*"); + conn.setRequestProperty( + "Accept-Language", + "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": HTTP " + code); + } + + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn + .getInputStream())); + StringBuilder sb = + new StringBuilder(); + String line; + while ((line = reader.readLine()) + != null) { + sb.append(line); + } + reader.close(); + + String tag = + parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 无法解析版本号"); + } + + return new CheckResult(tag, + sourceName, link, null); + + } catch (java.net + .SocketTimeoutException e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 连接被拒绝"); + } catch (java.net + .UnknownHostException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": " + + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + + + /** + * 从 JSON 中提取 tag_name + */ + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + // 尝试 tag_name(releases格式) + int idx = + json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + // 尝试 "name"(tags格式) + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") + || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + + // ========== 版本比较 ========== + + private boolean isNewer(String remote, + String local) { + String[] r = remote.split("\\."); + String[] l = local.split("\\."); + int len = Math.max(r.length, l.length); + for (int i = 0; i < len; i++) { + int ri = i < r.length + ? parseNum(r[i]) : 0; + int li = i < l.length + ? parseNum(l[i]) : 0; + if (ri > li) return true; + if (ri < li) return false; + } + return false; + } + + private int parseNum(String s) { + try { + return Integer.parseInt( + s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/UserGroupManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/UserGroupManager.java new file mode 100644 index 0000000..adaf907 --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/UserGroupManager.java @@ -0,0 +1,370 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.util.*; +import java.util.Base64; +import java.util.concurrent.ConcurrentHashMap; + +public class UserGroupManager { + + private final Main plugin; + private Connection db; + private final Map groupConfigs = new ConcurrentHashMap<>(); + + public UserGroupManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadGroupConfigs(); + startExpiryChecker(); + } + + // ===== DB ===== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(plugin.getDataFolder(), "usergroup.db"); + db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_config (" + + "group_name TEXT PRIMARY KEY," + + "display_name TEXT DEFAULT ''," + + "display_color TEXT DEFAULT '§f'," + + "display_emoji TEXT DEFAULT ''," + + "priority INTEGER DEFAULT 0," + + "is_permanent INTEGER DEFAULT 1," + + "duration_minutes INTEGER DEFAULT 0," + + "pack_url TEXT DEFAULT ''," + + "pack_hash TEXT DEFAULT ''," + + "pack_prompt TEXT DEFAULT 'Custom Resource Pack'," + + "pack_size INTEGER DEFAULT 0)"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_member (" + + "player_name TEXT NOT NULL," + + "group_name TEXT NOT NULL," + + "added_by TEXT DEFAULT 'system'," + + "added_time INTEGER DEFAULT 0," + + "expiry_time INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, group_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e); + } + } + + // ===== 配置加载 ===== + + public void loadGroupConfigs() { + groupConfigs.clear(); + File dir = new File(plugin.getDataFolder(), "用户组"); + if (!dir.exists()) dir.mkdirs(); + File[] files = dir.listFiles((d, n) -> n.endsWith(".txt")); + if (files == null) return; + + for (File f : files) { + try { + List lines = java.nio.file.Files.readAllLines( + f.toPath(), StandardCharsets.UTF_8); + UserGroupConfig cfg = parseGroupConfig(lines); + if (cfg != null) { + groupConfigs.put(cfg.name, cfg); + saveGroupConfigToDB(cfg); + } + } catch (Exception e) { + plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName()); + } + } + } + + private UserGroupConfig parseGroupConfig(List lines) { + UserGroupConfig cfg = new UserGroupConfig(); + boolean inBlock = false; + + for (String raw : lines) { + String l = raw; + + // 块注释 + if (l.contains("")) { inBlock = false; continue; } + if (l.contains("/*")) { inBlock = true; continue; } + if (l.contains("*/")) { inBlock = false; continue; } + if (inBlock) continue; + + // 行注释 + l = l.trim(); + if (l.isEmpty()) continue; + if (l.startsWith("#") || l.startsWith("//")) continue; + + // 行内注释 + int h = l.indexOf('#'); + int s = l.indexOf("//"); + if (h >= 0) l = l.substring(0, h).trim(); + if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim(); + if (l.isEmpty()) continue; + + // 解析键值对 + String key = l; + String val = ""; + int idx = l.indexOf('\uff1a'); + if (idx < 0) idx = l.indexOf(':'); + if (idx >= 0) { + key = l.substring(0, idx).trim(); + val = l.substring(idx + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) + val = val.substring(1, val.length() - 1); + } + + switch (key) { + case "组名": + case "name": cfg.name = val; break; + case "显示名": + case "display": cfg.displayName = val; break; + case "颜色": + case "color": cfg.displayColor = val; break; + case "符号": + case "emoji": cfg.displayEmoji = val; break; + case "优先级": + case "priority": + try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "类型": + case "type": + cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent"); + break; + case "时长": + case "duration": + try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "资源包URL": + case "pack_url": cfg.packUrl = val; break; + case "资源包Hash": + case "pack_hash": cfg.packHash = val; break; + case "资源包提示": + case "pack_prompt": cfg.packPrompt = val; break; + case "资源包大小": + case "pack_size": + try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "本地文件": + case "local_file": cfg.localFile = val; break; + } + } + + if (cfg.name == null || cfg.name.isEmpty()) return null; + return cfg; + } + + private void saveGroupConfigToDB(UserGroupConfig cfg) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_config " + + "(group_name, display_name, display_color, display_emoji," + + " priority, is_permanent, duration_minutes," + + " pack_url, pack_hash, pack_prompt, pack_size)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, cfg.name); + ps.setString(2, cfg.displayName); + ps.setString(3, cfg.displayColor); + ps.setString(4, cfg.displayEmoji); + ps.setInt(5, cfg.priority); + ps.setInt(6, cfg.permanent ? 1 : 0); + ps.setInt(7, cfg.durationMinutes); + ps.setString(8, cfg.packUrl); + ps.setString(9, cfg.packHash); + ps.setString(10, cfg.packPrompt); + ps.setInt(11, cfg.packSize); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 玩家操作 ===== + + public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) { + UserGroupConfig cfg = groupConfigs.get(groupName); + if (cfg == null) return false; + long now = System.currentTimeMillis(); + long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000; + if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000; + else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_member " + + "(player_name, group_name, added_by, added_time, expiry_time)" + + " VALUES (?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, groupName); + ps.setString(3, addedBy); + ps.setLong(4, now); + ps.setLong(5, expiry); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + Player p = plugin.getServer().getPlayerExact(player); + if (p != null) sendResourcePack(p); + return true; + } + + public boolean removePlayer(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + int rows = ps.executeUpdate(); + ps.close(); + return rows > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public List> getPlayerGroups(String player) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM user_group_member WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + long now = System.currentTimeMillis(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("group_name", rs.getString("group_name")); + row.put("added_by", rs.getString("added_by")); + long expiry = rs.getLong("expiry_time"); + row.put("expiry_time", expiry); + row.put("expired", expiry > 0 && expiry <= now); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public UserGroupConfig getHighestGroup(String player) { + List> groups = getPlayerGroups(player); + long now = System.currentTimeMillis(); + UserGroupConfig best = null; + for (Map g : groups) { + long expiry = (long) g.get("expiry_time"); + if (expiry > 0 && expiry <= now) continue; + String name = (String) g.get("group_name"); + UserGroupConfig cfg = groupConfigs.get(name); + if (cfg != null && (best == null || cfg.priority > best.priority)) { + best = cfg; + } + } + return best; + } + + public boolean isPlayerInGroup(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + boolean exists = ps.executeQuery().next(); + ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ===== 过期自动移除 ===== + + private void startExpiryChecker() { + new BukkitRunnable() { + @Override + public void run() { + checkExpiry(); + } + }.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查 + } + + private void checkExpiry() { + try { + long now = System.currentTimeMillis(); + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, group_name FROM user_group_member " + + "WHERE expiry_time > 0 AND expiry_time <= ?"); + ps.setLong(1, now); + ResultSet rs = ps.executeQuery(); + List expired = new ArrayList<>(); + while (rs.next()) { + expired.add(new String[]{ + rs.getString("player_name"), + rs.getString("group_name")}); + } + rs.close(); ps.close(); + + for (String[] e : expired) { + PreparedStatement del = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + del.setString(1, e[0]); + del.setString(2, e[1]); + del.executeUpdate(); + del.close(); + plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 资源包分发 ===== + + public void sendResourcePack(Player p) { + UserGroupConfig cfg = getHighestGroup(p.getName()); + if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return; + + String url = cfg.packUrl; + String prompt = cfg.packPrompt != null + ? cfg.packPrompt : "Custom Resource Pack"; + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + try { + p.setResourcePack(url, new byte[0], prompt, false); + plugin.getLogger().info("[UserGroup] 资源包已发送: " + + p.getName() + " -> " + cfg.displayName); + } catch (Exception e) { + plugin.getLogger().warning( + "[UserGroup] 资源包发送失败: " + e.getMessage()); + } + }, 40L); + } + + public Map getGroupConfigs() { + return groupConfigs; + } + + // ===== 配置类 ===== + + public static class UserGroupConfig { + public String name; + public String displayName = ""; + public String displayColor = "§f"; + public String displayEmoji = ""; + public int priority = 0; + public boolean permanent = true; + public int durationMinutes = 0; + public String packUrl = ""; + public String packHash = ""; + public String packPrompt = "Custom Resource Pack"; + public int packSize = 0; + public String localFile = ""; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/VerificationManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/VerificationManager.java new file mode 100644 index 0000000..3fc4ceb --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/VerificationManager.java @@ -0,0 +1,158 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.function.Consumer; + +public class VerificationManager { + + private final Main plugin; + + public VerificationManager(Main plugin) { + this.plugin = plugin; + } + + public boolean isBedrockPlayer(Player p) { + String uuid = p.getUniqueId().toString(); + return uuid.contains("f851ba4") + || uuid.startsWith("00000000-0000-0000"); + } + + public boolean isOnlineMode() { + return Bukkit.getServer().getOnlineMode(); + } + + public void verifyPremiumAsync(Player player, + Consumer callback) { + Thread t = new Thread(() -> doVerify(player, callback), + "Sdf1_login-PremiumVerify"); + t.setDaemon(true); + t.start(); + } + + private void doVerify(Player player, + Consumer callback) { + String name = player.getName(); + String uuid = player.getUniqueId().toString(); + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证开始 ======"); + plugin.getLogger().info( + "[Sdf1_login] 玩家: " + name); + plugin.getLogger().info( + "[Sdf1_login] 当前UUID: " + uuid); + + try { + UUID offlineUuid = UUID.nameUUIDFromBytes( + ("OfflinePlayer:" + name) + .getBytes(StandardCharsets.UTF_8)); + String offlineStr = offlineUuid.toString(); + + plugin.getLogger().info( + "[Sdf1_login] 离线UUID: " + offlineStr); + + boolean isOfflineUuid = + player.getUniqueId().equals(offlineUuid); + + plugin.getLogger().info( + "[Sdf1_login] UUID匹配离线格式: " + + isOfflineUuid); + + if (!isOfflineUuid) { + plugin.getLogger().info( + "[Sdf1_login] 结论: 正版玩家"); + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(true); + return; + } + + plugin.getLogger().info( + "[Sdf1_login] UUID为离线格式," + + "查询Mojang API..."); + UUID mojangUuid = fetchMojangUuid(name); + + if (mojangUuid != null) { + plugin.getLogger().info( + "[Sdf1_login] Mojang返回UUID: " + + mojangUuid); + plugin.getLogger().info( + "[Sdf1_login] 结论: 盗版客户端" + + "使用了正版名字"); + } else { + plugin.getLogger().info( + "[Sdf1_login] Mojang无此名字"); + plugin.getLogger().info( + "[Sdf1_login] 结论: 纯离线玩家"); + } + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(false); + + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 验证异常: " + + e.getMessage()); + callback.accept(false); + } + } + + private UUID fetchMojangUuid(String name) { + HttpURLConnection conn = null; + try { + URL url = new URL( + "https://api.mojang.com/users/profiles/minecraft/" + + name); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setRequestMethod("GET"); + + int code = conn.getResponseCode(); + plugin.getLogger().info( + "[Sdf1_login] Mojang API响应: " + code); + + if (code == 200) { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn.getInputStream(), + StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String json = sb.toString(); + String uuidStr = json.replaceAll( + ".*\"id\"\\s*:\\s*\"([^\"]+)\".*", + "$1"); + plugin.getLogger().info( + "[Sdf1_login] Mojang原始UUID: " + + uuidStr); + + String formatted = uuidStr.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5"); + return UUID.fromString(formatted); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] Mojang查询异常: " + + e.getMessage()); + } finally { + if (conn != null) conn.disconnect(); + } + return null; + } +} diff --git a/backup/20260605_152529/src/main/java/Sdf1_login/WelcomeManager.java b/backup/20260605_152529/src/main/java/Sdf1_login/WelcomeManager.java new file mode 100644 index 0000000..68bd3ef --- /dev/null +++ b/backup/20260605_152529/src/main/java/Sdf1_login/WelcomeManager.java @@ -0,0 +1,163 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class WelcomeManager { + + private final Main plugin; + private final Random random = new Random(); + private final List registerMsgs = + new ArrayList<>(); + private final List loginMsgs = + new ArrayList<>(); + + public WelcomeManager(Main plugin) { + this.plugin = plugin; + loadMessages(); + } + + public void loadMessages() { + registerMsgs.clear(); + loginMsgs.clear(); + File f = new File( + plugin.getDataFolder(), + "欢迎仪式.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("注册") + || t.equals("注册:")) { + section = "register"; + continue; + } + if (t.equals("登录") + || t.equals("登录:")) { + section = "login"; + continue; + } + // 支持 = 和 : 两种分隔 + if (section.isEmpty()) { + if (t.startsWith("注册") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + registerMsgs.add( + colorize(v)); + continue; + } + if (t.startsWith("登录") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + loginMsgs.add( + colorize(v)); + continue; + } + continue; + } + if (!t.isEmpty()) { + if (section.equals("register")) + registerMsgs.add( + colorize(t)); + else + loginMsgs.add( + colorize(t)); + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[欢迎] 加载失败: " + + e.getMessage()); + } + plugin.getLogger().info( + "[欢迎] 加载完成: 注册=" + + registerMsgs.size() + + " 登录=" + loginMsgs.size()); + } + + public void onRegister(Player p) { + sendRandom(p, registerMsgs, "注册"); + } + + public void onLogin(Player p) { + sendRandom(p, loginMsgs, "登录"); + } + + private void sendRandom(Player p, + List msgs, String tag) { + if (msgs.isEmpty()) return; + String msg = msgs.get( + random.nextInt(msgs.size())); + msg = msg.replace("{player}", + p.getName()); + final String finalMsg = msg; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> Bukkit + .broadcastMessage( + finalMsg), 10L); + } + + private String colorize(String msg) { + return msg.replace("&", "\u00a7"); + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets + .UTF_8))) { + pw.println("# 欢迎仪式配置"); + pw.println("# &为颜色代码"); + pw.println("# {player} = 玩家名"); + pw.println(); + pw.println("注册:"); + pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]"); + pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]"); + pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]"); + pw.println(); + pw.println("登录:"); + pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]"); + pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]"); + pw.println("&b&l{player} &b回来了!&7[登录]"); + } catch (IOException ignored) { + } + } +} diff --git a/backup/20260605_152529/src/main/resources/plugin.yml b/backup/20260605_152529/src/main/resources/plugin.yml new file mode 100644 index 0000000..f6868d9 --- /dev/null +++ b/backup/20260605_152529/src/main/resources/plugin.yml @@ -0,0 +1,104 @@ +name: Sdf1_login +version: 1.0 +main: Sdf1_login.Main +api-version: '1.21' +commands: + sdf1_login: + description: Sdf1_login主命令 + usage: /sdf1_login + reg: + description: 注册 + usage: /reg <密码> + login: + description: 登录 + usage: /login <密码> + l: + description: 登录快捷版 + usage: /l <密码> + 注册: + description: 注册(中文) + usage: /注册 <密码> + 登录: + description: 登录(中文) + usage: /登录 <密码> + 玩家信息: + description: 打开面板(中文) + usage: /玩家信息 + 改密码: + description: 修改密码(中文) + usage: /改密码 <旧密码> <新密码> <确认新密码> + 设置邮箱: + description: 设置邮箱(中文) + usage: /设置邮箱 <邮箱地址> + 撤销: + description: 撤销操作(中文) + usage: /撤销 + 删除: + description: 删除玩家(中文) + usage: /删除 <玩家> <密码> + 找回密码: + description: 找回密码(中文) + usage: /找回密码 + 签到: + description: 每日签到(中文) + usage: /签到 + #oa: + # description: 申请删除账号 + # usage: /oa [玩家名] + 删除账号: + description: 申请删除账号(中文) + usage: /删除账号 [玩家名] + 通过: + description: 批准OA请求(中文) + usage: /通过 <编号> + 拒绝: + description: 拒绝OA请求(中文) + usage: /拒绝 <编号> [理由] + accept: + description: 批准OA请求 + usage: /accept <编号> + deny: + description: 拒绝OA请求 + usage: /deny <编号> [理由] + email: + description: 绑定邮箱 + usage: /email <邮箱> + 绑定: + description: 绑定邮箱(中文) + usage: /绑定 <邮箱> + 绑定邮箱: + description: 绑定邮箱 + usage: /绑定邮箱 + chat: + description: 聊天过滤管理 + usage: /chat + recycle: + description: 打开垃圾回收站 + usage: /recycle + 垃圾清理: + description: 手动清理掉落物 + usage: /垃圾清理 + menu: + description: 雪球菜单开关控制 + usage: /menu <开/关> + pvp : + description: PVP榜单 + usage: /pvp + cypay : + description: 草原探险债券 + usage: /cypay + printer : + description: 打印债券流水 + usage: /printer + shop : + description: 游戏商人 + usage: /shop + 商店: + description: 打开商店 + usage: /商店 + #testorder: + # description: 测试订单中心 + # usage: /testorder + protect: + description: 区域保护 + usage: /protect \ No newline at end of file diff --git a/backup/20260605_185500/CheckInManager.java.backup b/backup/20260605_185500/CheckInManager.java.backup new file mode 100644 index 0000000..b8e612c --- /dev/null +++ b/backup/20260605_185500/CheckInManager.java.backup @@ -0,0 +1,305 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class CheckInManager { + + private final Main plugin; + + public CheckInManager(Main plugin) { + this.plugin = plugin; + } + + // ===== 今日是否已签到 ===== + public boolean isCheckedInToday( + String name) { + String last = (String) plugin.getDb() + .getField(name, + "last_checkin_date"); + String today = new SimpleDateFormat( + "yyyy-MM-dd") + .format(new Date()); + return today.equals(last); + } + + // ===== 签到 ===== + // ===== 签到 ===== + public String checkIn(Player p) { + String name = p.getName(); + if (isCheckedInToday(name)) { + return plugin.getConfig2() + .msg("checkin_already"); + } + + Object streakObj = plugin.getDb() + .getField(name, "checkin_streak"); + int streak = streakObj != null + ? ((Number) streakObj).intValue() + : 0; + + String lastDate = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + + if (lastDate != null && !lastDate.isEmpty()) { + try { + java.util.Date lastD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(lastDate); + java.util.Date todayD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(today); + long diff = todayD.getTime() + - lastD.getTime(); + if (diff > 86400000L * 2) { + // 间隔超过2天,重置连续天数 + streak = 1; + } else if (diff > 86400000L) { + // 间隔1-2天(连续签到),天数+1 + streak++; + } + // 间隔<=1天(同一天),不改变连续天数 + } catch (Exception ignored) { + } + } else { + // 首次签到 + streak = 1; + } + + double multi = 1.0; + if (streak >= 21) multi = 1.5; + else if (streak >= 14) multi = 1.3; + else if (streak >= 7) multi = 1.1; + + int basePoints = + plugin.getConfig2().checkinPoints; + int points = (int) (basePoints * multi); + + plugin.getDb().addPoints(name, points); + plugin.getDb().setField(name, + "checkin_streak", streak); + plugin.getDb().setField(name, + "last_checkin_date", today); + + Object totalObj = plugin.getDb() + .getField(name, "total_checkin_days"); + int total = totalObj != null + ? ((Number) totalObj).intValue() + : 0; + plugin.getDb().setField(name, + "total_checkin_days", total + 1); + + // ★ 债券盲盒奖励(6参数版,写入流水)★ + int bondMin = + plugin.getConfig2().checkinBondMin; + int bondMax = + plugin.getConfig2().checkinBondMax; + if (bondMin <= 0) bondMin = 1; + if (bondMax < bondMin) bondMax = bondMin; + int bondReward = bondMin + + new java.util.Random() + .nextInt(bondMax - bondMin + 1); + plugin.getBonds().addBonds(name, bondReward, + "daily_sign", "", "签到系统", + "每日签到连续" + streak + "天"); + + return plugin.getConfig2() + .msg("checkin_success") + .replace("{points}", + String.valueOf(points)) + .replace("{streak}", + String.valueOf(streak)) + .replace("{multi}", + String.valueOf(multi)) + + " §e+" + bondReward + "债券"; + } + + // ===== 获取连续天数 ===== + public int getStreak(String name) { + Object val = plugin.getDb() + .getField(name, + "checkin_streak"); + if (val instanceof Number) { + return ((Number) val) + .intValue(); + } + return 0; + } + // 标准化日期 + private String normalizeDate(String input) { + String s = input.trim().replace("/", "-"); + if (s.matches("\\d{8}")) { + return s.substring(0, 4) + "-" + + s.substring(4, 6) + "-" + + s.substring(6, 8); + } + return s; + } + // ===== 补签入口 ===== + public String backCheckIn( + Player p, String dateStr) { + String name = p.getName(); + String normalized = + normalizeDate(dateStr); + + // 严格日期验证 + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd"); + sdf.setLenient(false); + java.util.Date target; + try { + target = sdf.parse(normalized); + } catch (Exception ex) { + return "§c无效日期: " + dateStr; + } + String reformat = sdf.format(target); + if (!reformat.equals(normalized)) { + return "§c无效日期: " + dateStr; + } + + String today = sdf.format(new Date()); + if (normalized.compareTo(today) >= 0) { + return "§c今天不需要补签"; + } + + String threeDaysAgo = getPastDate(3); + if (normalized.compareTo( + threeDaysAgo) < 0) { + return "§c只能补签3天内" + + "(最多到 " + threeDaysAgo + ")"; + } + + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + boolean alreadyBacked = false; + if (!existing.isEmpty()) { + for (String d : existing.split(",")) { + if (normalizeDate(d.trim()) + .equals(normalized)) { + alreadyBacked = true; + break; + } + } + } + if (alreadyBacked) { + return "§c该日期已补签: " + normalized; + } + + Object normalCheck = + plugin.getDb().getField( + name, "last_checkin_date"); + String lastCheckin = normalCheck != null + ? normalCheck.toString() : ""; + if (lastCheckin.equals(normalized)) { + return "§c该日期已签到: " + normalized; + } + + // ★ 补签消耗:3债券 + 10积分 + // 积分不够时:用 9债券(3倍)代替 ★ + int bondCost = 3; + int pointCost = 10; + int bondAltCost = 9; // 3倍债券替代 + + int myBonds = plugin.getBonds() + .getBonds(name); + Object ptsObj = plugin.getDb() + .getField(name, "points"); + int myPoints = ptsObj != null + ? ((Number) ptsObj).intValue() + : 0; + + + // 方案A:3债券 + 10积分 + boolean canA = myBonds >= bondCost + && myPoints >= pointCost; + // 方案B:9债券(积分不够时) + boolean canB = myBonds >= bondAltCost + && myPoints < pointCost; + + if (!canA && !canB) { + if (myPoints < pointCost + && myBonds < bondAltCost) { + return "§c补签费用不足" + + "(需3债券+10积分," + + "或9债券代替)" + + " 当前: " + + myBonds + "债券 " + + myPoints + "积分"; + } + if (myBonds < bondCost) { + return "§c债券不足(需" + + bondCost + "枚)"; + } + return "§c积分不足(需" + + pointCost + "分)"; + } + + // 执行扣款 + if (canA) { + // 方案A:3债券 + 10积分 + plugin.getBonds().deductBonds( + name, bondCost, + "admin_deduct", "", + "补签系统", "补签扣款"); + plugin.getDb().deductPoints( + name, pointCost); + } else { + // 方案B:9债券代替 + plugin.getBonds().deductBonds( + name, bondAltCost, + "admin_deduct", "", + "补签系统", "补签扣款(债券替代)"); + } + + doBackCheckIn(p, normalized); + + String costMsg = canA + ? bondCost + "债券+" + pointCost + "积分" + : bondAltCost + "债券(替代)"; + return "§a补签成功!日期: " + + normalized + + " §7消耗: " + costMsg; + } + + + + // ===== 执行补签 ===== + // ===== 执行补签 ===== + public void doBackCheckIn( + Player p, String dateStr) { + String name = p.getName(); + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + String updated = existing.isEmpty() + ? dateStr + : existing + "," + dateStr; + plugin.getDb().setField(name, + "backcheck_dates", updated); + int streak = getStreak(name); + plugin.getDb().setField(name, + "checkin_streak", streak + 1); + // ★ 补签不发放奖励,只记录 + } + + // ===== 获取N天前日期 ===== + private String getPastDate( + int daysAgo) { + Calendar cal = + Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, + -daysAgo); + return new SimpleDateFormat( + "yyyy-MM-dd") + .format(cal.getTime()); + } +} \ No newline at end of file diff --git a/backup/20260605_185500/DatabaseManager.java.backup b/backup/20260605_185500/DatabaseManager.java.backup new file mode 100644 index 0000000..cb2b8f7 --- /dev/null +++ b/backup/20260605_185500/DatabaseManager.java.backup @@ -0,0 +1,1758 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + + ")"); + + + // 安全添加 users 缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 inventory_data 列判断) + boolean hasOldTable = false; + try { + ResultSet rs = st.executeQuery( + "SELECT inventory_data " + + "FROM inventory_backups " + + "LIMIT 1"); + hasOldTable = true; + rs.close(); + } catch (SQLException e) { + hasOldTable = false; + } + + if (hasOldTable) { + // 旧表存在,迁移数据到新表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups_new (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 迁移旧数据 + try { + st.execute("INSERT INTO " + + "inventory_backups_new " + + "(player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience) " + + "SELECT player_name, " + + "inventory_data, " + + "armor_data, " + + "extra_data, " + + "player_level, " + + "player_exp " + + "FROM inventory_backups"); + } catch (SQLException ignored) { + } + st.execute("DROP TABLE " + + "inventory_backups"); + st.execute("ALTER TABLE " + + "inventory_backups_new " + + "RENAME TO " + + "inventory_backups"); + } else { + // 无旧表或已迁移,直接创建 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + } + + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + // PVP区域表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER DEFAULT 0" + + ")"); + +// PVP击杀总榜表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT NOT NULL," + + "region_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, region_name)" + + ")"); + + +// 菜单图标表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "menu_icons (" + + "player_name TEXT PRIMARY KEY," + + "icon_b64 TEXT DEFAULT ''," + + "item_name TEXT DEFAULT ''," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 垃圾回收站物品表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "item_data TEXT DEFAULT ''," + + "amount INTEGER DEFAULT 1," + + "create_time INTEGER DEFAULT 0" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +// ==================== 菜单图标 ==================== + + public void saveMenuIcon(String name, + String iconB64, String itemName) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO menu_icons " + + "(player_name, icon_b64, " + + "item_name, save_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, iconB64); + ps.setString(3, itemName); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT icon_b64 FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String b64 = null; + if (rs.next()) { + b64 = rs.getString("icon_b64"); + } + rs.close(); + ps.close(); + return b64; + } catch (SQLException e) { + return null; + } + } + + public void deleteMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM menu_icons " + + "WHERE player_name=? " + + "AND icon_b64 IS NOT NULL " + + "AND icon_b64 != '' " + + "LIMIT 1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public void createUser(String name, String hash, + String salt) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + public boolean checkPasswordOrTemp( + String name, String hash) { + // 先检查主密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean checkPassword(String name, + String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +// ==================== PVP区域 ==================== + + public void savePVPRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions " + + "(name, world, " + + "x1, y1, z1, " + + "x2, y2, z2, " + + "create_time) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map + getPVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getAllPVPRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + db.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deletePVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + +// ==================== PVP击杀 ==================== + + public int getPVPKills( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int kills = 0; + if (rs.next()) + kills = rs.getInt("kills"); + rs.close(); + ps.close(); + return kills; + } catch (SQLException e) { + return 0; + } + } + + public int getPVPDeaths( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int deaths = 0; + if (rs.next()) + deaths = rs.getInt("deaths"); + rs.close(); + ps.close(); + return deaths; + } catch (SQLException e) { + return 0; + } + } + + public void addPVPKill( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,1,0) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "kills=kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void addPVPDeath( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,0,1) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "deaths=deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getPVPKillTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT player_name, " + + "kills, deaths " + + "FROM pvp_kills " + + "WHERE " + + "region_name=? " + + "AND kills>0 " + + "ORDER BY kills " + + "DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Connection getDb() { + return db; + } +} diff --git a/backup/20260605_185500/Main.java.backup b/backup/20260605_185500/Main.java.backup new file mode 100644 index 0000000..ed02c8c --- /dev/null +++ b/backup/20260605_185500/Main.java.backup @@ -0,0 +1,5568 @@ +package Sdf1_login; + + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import org.bukkit.event.inventory.*; +import org.bukkit.event.player.PlayerInteractEvent; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.LinkedHashMap; +import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import java.util.Date; +import org.bukkit.command.CommandSender; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.scheduler.BukkitTask; + + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; + + + +public class Main extends JavaPlugin + implements CommandExecutor, Listener, + TabCompleter { + + private DatabaseManager db; + private MenuIconManager menuIconMgr; + private ConfigManager config; + private EmailManager email; + private LoginManager loginMgr; + private AFKManager afk; + private GiftManager gift; + private CheckInManager checkIn; + private InviteManager invite; + private PointsManager points; + public GUIManager gui; + private ChatInputManager chatInput; + private Economy economy; + private VerificationManager verification; + private IPGroupManager ipGroup; + private AccountRequestManager accountRequest; + private TicketManager ticket; + private ChatFilterManager chatFilter; + private GarbageManager garbage; + public RadioManager radio; + // 在 lastActivity 字段后面添加: + // [ADDED] 任务追踪系统 + private QuestTracker questTracker; + // [FIX] 垃圾箱防抖锁(记录上次操作时间戳) + private final Map garbageBusy = + new ConcurrentHashMap<>(); + // [ADDED] 群系检测节流 + private final Map lastBiomeCheck = + new ConcurrentHashMap<>(); + // 在 private WelcomeManager welcome; 字段(若不存在)添加: + private WelcomeManager welcome; + private final Set pendingAdminAuth = + new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + /* + 资源包下载部分控制,不经过RadioDownloadListener的熔断控制 + */ + private RadioDownloadListener radioDL; + private final Set chatInputActive = + ConcurrentHashMap.newKeySet(); + // 补签确认:玩家名 → "日期:总积分" + public final Map + pendingBackCheck = + new HashMap<>(); + public MenuManager menu; + + private final Set loggedIn = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Set needsPasswordChange = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map joinTime = + new ConcurrentHashMap<>(); + private final Map joinLoc = + new HashMap<>(); + private final Map ipAutoLogin = + new HashMap<>(); + private static final long IP_SKIP_MS = 300000L; + private SalesStatsManager salesStats; + + private final Map lastActivity = + new ConcurrentHashMap<>(); + private CommissionManager commission; + // 余额操作待处理 + private String balanceTarget = null; + private boolean balanceGiveMode = false; + private BondManager bondManager; + private CDKManager cdkManager; + private UserGroupManager userGroupManager; + private CypayCommand cypayCommand; + + public BondManager getBonds() { + return bondManager; + } + + public CDKManager getCDK() { + return cdkManager; + } + + public UserGroupManager getUserGroup() { + return userGroupManager; + } + + public CypayCommand getCypay() { + return cypayCommand; + } + + // 在 Main.java 字段声明区域(bondManager 附近)添加: +// 在 bondManager 字段附近添加: + private BondPrinter bondPrinter; + private OrderManager orderManager; + + public OrderManager getOrderManager() { + return orderManager; + } + + public AreaProtection areaProtection; + + + // 钱包流水查看目标(玩家名 → 查看目标) + private final Map walletViewTarget = + new ConcurrentHashMap<>(); + + // 流水格式化 + private static final java.text.SimpleDateFormat + TX_SDF = new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private ShopManager shopManager; //商店 + + + public CommissionManager getCommission() { + return commission; + } + + //pvp + private PVPManager pvpManager; + + private static class PwdRollback { + final String hash; + final String salt; + final long time; + + PwdRollback(String h, String s, long t) { + hash = h; + salt = s; + time = t; + } + } + + // 待删除的玩家名(仅保留最后一个) + private String pendingDeleteName = null; + private BukkitRunnable pendingDeleteTask = null; + + private final Map + pwdRollback = + new ConcurrentHashMap<>(); + private final Map menuChatField = + new java.util.concurrent.ConcurrentHashMap<>(); + private final Map menuChatIdx = + new java.util.concurrent.ConcurrentHashMap<>(); + + + public void recordPasswordChange(String name, + String oldHash, String oldSalt) { + pwdRollback.put(name, + new PwdRollback(oldHash, oldSalt, + System.currentTimeMillis())); + } + + // ===== Getters ===== + public DatabaseManager getDb() { + return db; + } + + + public ConfigManager getConfig2() { + return config; + } + + public EmailManager getEmail() { + return email; + } + + public Economy getEconomy() { + return economy; + } + + public LoginManager getLoginMgr() { + return loginMgr; + } + + public AFKManager getAfk() { + return afk; + } + + public GiftManager getGift() { + return gift; + } + + public CheckInManager getCheckIn() { + return checkIn; + } + + public InviteManager getInvite() { + return invite; + } + + public PointsManager getPoints() { + return points; + } + + public GUIManager getGui() { + return gui; + } + + public ChatInputManager getChatInput() { + return chatInput; + } + + public VerificationManager getVerification() { + return verification; + } + + public IPGroupManager getIPGroup() { + return ipGroup; + } + + public AccountRequestManager getAccountRequest() { + return accountRequest; + } + + public TicketManager getTicket() { + return ticket; + } + + public QuestTracker getQuestTracker() { + return questTracker; + } + + public ChatFilterManager getChatFilter() { + return chatFilter; + } + + public GarbageManager getGarbage() { + return garbage; + } + + public Set getLoggedIn() { + return loggedIn; + } + + public Set getNeedsPasswordChange() { + return needsPasswordChange; + } + + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e) { + if (areaProtection != null) { + Player p = e.getPlayer(); + areaProtection.onPlayerOffline( + p.getUniqueId(), p.getName(), ""); + if (areaProtection != null) { + areaProtection.onPlayerQuit(p.getName()); + } + + } + } + + + // public RadioManager radio; + @Override + public void onEnable() { + getDataFolder().mkdirs(); + salesStats = new SalesStatsManager(this); + + // ===== 1. 基础配置 ===== + config = new ConfigManager(getDataFolder()); + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + configMgr.loadMessages(); + orderManager = new OrderManager(this); + // ===== 2. 数据库 ===== + db = new DatabaseManager(getDataFolder()); + db.init(); + menuIconMgr = new MenuIconManager(this); + + + // ===== 3. 菜单 ===== + menu = new MenuManager(this); + menu.loadMenu(); + + // ===== 4. GUI(依赖 menu) ===== + gui = new GUIManager(this); + shopManager = new ShopManager(this); + Bukkit.getPluginManager().registerEvents(shopManager, this); + // shopManager.loadCategories(); // 构造器内已调用,此处重复 + + getServer().getPluginManager() + .registerEvents(gui, this); + + // ===== 5. 其他管理器 ===== + email = new EmailManager(config); + loginMgr = new LoginManager(this); + afk = new AFKManager(config, this); + gift = new GiftManager(this); + gift.loadStages(); + checkIn = new CheckInManager(this); + invite = new InviteManager(this); + points = new PointsManager(this); + chatInput = new ChatInputManager(); + verification = new VerificationManager(this); + ipGroup = new IPGroupManager(this, + config.maxAccountsPerIP); + accountRequest = + new AccountRequestManager(this); + accountRequest.setMode(config.approvalMode); + accountRequest.setAutoDelayMinutes( + config.autoApproveDelayMinutes); + ticket = new TicketManager(this); + commission = new CommissionManager(this); + questTracker = new QuestTracker(this); + chatFilter = new ChatFilterManager(this); + chatFilter.loadConfig(); + welcome = new WelcomeManager(this); + + // ===== 6. 垃圾箱 ===== + garbage = new GarbageManager(this); + garbage.init(); + garbage.loadConfig(); + garbage.startAutoCleanup(); + + // ===== 7. Radio ===== + radio = new RadioManager(this); + radio.init(); + radioDL = new RadioDownloadListener(this); + getServer().getPluginManager() + .registerEvents(radioDL, this); + + // ===== 铁块电梯 ===== + getServer().getPluginManager() + .registerEvents(new IronBlockElevator(), this); + + // ===== 8. 经济 ===== + setupEconomy(); + + // ===== 9. 注册命令 ===== + if (getCommand("sdf1_login") != null) { + getCommand("sdf1_login") + .setExecutor(this); + getCommand("sdf1_login") + .setTabCompleter(this); + } + if (getCommand("reg") != null) + getCommand("reg").setExecutor(this); + if (getCommand("login") != null) + getCommand("login").setExecutor(this); + if (getCommand("l") != null) + getCommand("l").setExecutor(this); + if (getCommand("签到") != null) + getCommand("签到") + .setExecutor(this); + if (getCommand("recycle") != null) + getCommand("recycle") + .setExecutor(this); + if (getCommand("垃圾清理") != null) + getCommand("垃圾清理") + .setExecutor(this); + /* if (getCommand("oa") != null) + getCommand("oa").setExecutor(this);*/ + if (getCommand("menu") != null) + getCommand("menu").setExecutor(this); + if (getCommand("printer") != null) { + getCommand("printer").setExecutor(this); + getCommand("shop").setExecutor(this); + getCommand("商店").setExecutor(this); + // getCommand("testorder").setExecutor(this); + if (getCommand("printer") != null) { + getCommand("printer").setExecutor(this); + getCommand("shop").setExecutor(this); + getCommand("商店").setExecutor(this); + } +// protect 命令独立注册 + if (getCommand("protect") != null) { + getCommand("protect").setExecutor(this); + getCommand("protect").setTabCompleter(this); + } + } + + // ===== 10. 注册事件 ===== + getServer().getPluginManager() + .registerEvents(this, this); + + // ===== 11. 定时任务 ===== + afk.startCheck(); + startLoginReminder(); + startTimeoutCheck(); + startPasswordReminder(); + startTicketAutoProcess(); + radio.startMainRadio(); + new BukkitRunnable() { + public void run() { + if (questTracker == null) return; + for (Player p : + Bukkit.getOnlinePlayers()) { + if (!isFrozen(p)) + questTracker.onPlayTime( + p.getName(), 30000L); + } + } + }.runTaskTimer(this, 600L, 600L); +// [新增] Printer 文件清理(每小时检测一次) + new BukkitRunnable() { + public void run() { + if (bondPrinter != null) + bondPrinter.cleanupOldFiles(); + } + }.runTaskTimer(this, 1200L, 72000L); + + // 12 ===PVP=== + pvpManager = new PVPManager(this); + getServer().getPluginManager() + .registerEvents(pvpManager, this); +// PVP区域检测定时器 + getServer().getScheduler() + .runTaskTimer(this, + () -> pvpManager.tickRegions(), + 20L, 20L); + setupPVPTab(); + + // 13 ====cypay债券==== + // 债券系统(独立DB) + bondManager = new BondManager(this); + cdkManager = new CDKManager(this); + userGroupManager = new UserGroupManager(this); + cypayCommand = new CypayCommand(this); +/*加载CDK文件 + cdkManager.loadCDKsFromDir();*/ + // cypay 命令 + if (getCommand("cypay") != null) { + getCommand("cypay").setExecutor(cypayCommand); + getCommand("cypay").setTabCompleter(cypayCommand); + } + bondPrinter = new BondPrinter(this); + // cdkManager.loadCDKsFromDir(); +//14 ====商店==== + // ★ 构造器内已加载完毕,此处重复 + // if (shopManager != null) { + // shopManager.loadCategories(); + // } + + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + orderManager = new OrderManager(this); + +// 15 ======区域保护========= + areaProtection = new AreaProtection(this); + getServer().getPluginManager() + .registerEvents(areaProtection, this); + +// 效果清除定时器(只留一个) + getServer().getScheduler().runTaskTimer(this, () -> { + if (areaProtection != null) { + } + }, 20L, 20L); + +// 和平模式独立检测 + getServer().getScheduler().runTaskTimer(this, () -> { + if (areaProtection == null) return; + for (Player p : Bukkit.getOnlinePlayers()) { + AreaProtection.AreaConfig ac = areaProtection.getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null) { + // 无条件打印,不管 peaceMode 是啥 + /* getLogger().info("[和平定时器] " + p.getName() + + " 在[" + ac.name + + "] peaceMode=" + ac.peaceMode + + " 白名单=" + ac.peaceWhitelist);*/ + if (ac.peaceMode) { + areaProtection.banHostilesWithWhitelist(p, ac); + } + } + } + }, 40L, 40L); + areaProtection.startEnforceTask(); + if (areaProtection != null) { + areaProtection.reload(); + areaProtection.startEnforceTask(); + } + + + getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈"); + getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件\nSdf1_game娱乐游戏插件"); + // 大字画 + getLogger().info("\n" + + " ____ _ __ _ _ _ \n" + + " / ___| __| |/ _/ | | | ___ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | | |/ _ \\ / _` | | '_ \\ \n" + + " ___) | (_| | _| | | | (_) | (_| | | | | | \n" + + " |____/ \\__,_|_| |_|___|_|\\___/ \\__, |_|_| |_| \n" + + " ____ _ __ |_____| _ |___/ _ \n" + + " / ___| __| |/ _/ |_ __ | |_ _ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | '_ \\| | | | |/ _` | | '_ \\ \n" + + " ___) | (_| | _| | |_) | | |_| | (_| | | | | | \n" + + " |____/ \\__,_|_| |_| .__/|_|\\__,_|\\__, |_|_| |_|___ _ __ _ \n" + + " _ __ _____ |_|___ _ __ | |___/_ _ / ___| __| |/ _/ |\n" + + " | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" + + " | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" + + " | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" + + " |_| |___/ " + ); + } + + public BondPrinter getBondPrinter() { + return bondPrinter; + } + + public ShopManager getShopManager() { + return shopManager; + } + + private ConfigManager configMgr; + + public ConfigManager getConfigMgr() { + return configMgr; + } + + + private void setupPVPTab() { + if (getCommand("pvp") != null) { + getCommand("pvp") + .setTabCompleter( + (sender, cmd, + alias, args) -> { + List list = + new ArrayList<>(); + if (args.length == 1) { + String[] sub = { + "create", "stats", + "list", "delete", + "tool", "on", "off", + "tempban"}; + for (String s : sub) { + if (s.startsWith( + args[0] + .toLowerCase())) { + list.add(s); + } + } + } else if (args.length == 2) { + String sub = + args[0].toLowerCase(); + if (sub.equals("stats")) { + for (Player pp : Bukkit + .getOnlinePlayers()) { + if (pp.getName() + .toLowerCase() + .startsWith(args[1] + .toLowerCase())) { + list.add(pp.getName()); + } + } + } else if (sub.equals("delete") + || sub.equals("on") + || sub.equals("off") + || sub.equals("tempban")) { + for (String rn : + pvpManager + .getRegionNames()) { + if (rn.toLowerCase() + .startsWith(args[1] + .toLowerCase())) { + list.add(rn); + } + } + } + } + return list; + }); + } + } + + @Override + public void onDisable() { + if (shopManager != null) shopManager.saveAll(); + // 取消未执行的删除任务 + if (pendingDeleteTask != null) { + pendingDeleteTask.cancel(); + pendingDeleteTask = null; + getLogger().info( + "[Sdf1_login] 服务器关闭," + + "取消待删除: " + + pendingDeleteName); + pendingDeleteName = null; + } + if (db != null) db.close(); + if (garbage != null) garbage.close(); + if (questTracker != null) + questTracker.shutdown(); + if (areaProtection != null) { + areaProtection.stopEnforceTask(); + } + for (BukkitTask task : areaProtection.getPendingTasks()) { + task.cancel(); + } + areaProtection.saveWhitelists(); + areaProtection.stopEnforceTask(); + // 取消所有延时清理任务,立即清理 + if (areaProtection != null) { + for (Map.Entry entry + : areaProtection.getPendingClearTasks().entrySet()) { + entry.getValue().cancel(); + // 立即清理效果 + Player p = Bukkit.getPlayer(entry.getKey()); + if (p != null && p.isOnline()) { + List marked = + areaProtection.getPlayerMarkedEffects() + .remove(entry.getKey()); + if (marked != null) { + for (PotionEffectType type : marked) { + p.removePotionEffect(type); + } + } + } + } + areaProtection.stopEnforceTask(); + areaProtection.saveWhitelists(); + areaProtection.getAlreadyForced().clear(); + + } + } + + private void setupEconomy() { + if (getServer().getPluginManager() + .getPlugin("Vault") == null) return; + RegisteredServiceProvider rsp = + getServer().getServicesManager() + .getRegistration( + Economy.class); + if (rsp != null) + economy = rsp.getProvider(); + } + + // ===== Timers ===== + private void startTimeoutCheck() { + new BukkitRunnable() { + public void run() { + long now = System.currentTimeMillis(); + Iterator> it = + joinTime.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = + it.next(); + if (now - entry.getValue() + > config.loginTimeout + * 1000L) { + Player p = Bukkit.getPlayer( + entry.getKey()); + if (p != null && p.isOnline() + && !loggedIn.contains( + p.getName())) + p.kickPlayer(config.msg( + "login_timeout")); + it.remove(); + } + } + } + }.runTaskTimer(this, 80L, 80L); + } + + private void startLoginReminder() { + new BukkitRunnable() { + public void run() { + Iterator it = + joinTime.keySet().iterator(); + while (it.hasNext()) { + UUID uuid = it.next(); + Player p = Bukkit.getPlayer(uuid); + if (p == null || !p.isOnline()) { + it.remove(); + continue; + } + if (loggedIn.contains( + p.getName())) { + it.remove(); + continue; + } + // 已注册→提示登录,未注册→提示注册 + if (db.userExists(p.getName())) + p.sendMessage(config.msg( + "not_logged_in")); + else + p.sendMessage(config.msg( + "not_registered")); + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startPasswordReminder() { + new BukkitRunnable() { + public void run() { + for (Player p : + Bukkit.getOnlinePlayers()) { + if (needsPasswordChange.contains( + p.getName())) { + p.sendTitle("§c§l请修改密码", + "§f使用 /sdf1_login pw 修改密码", + 10, 40, 10); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 1.0f); + } + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startTicketAutoProcess() { + new BukkitRunnable() { + public void run() { + if (ticket != null) + ticket.autoProcessCompleted(); + } + }.runTaskTimer(this, 6000L, 6000L); + } + + // ===== Utils ===== + public String getPlayerIP(Player p) { + InetSocketAddress addr = p.getAddress(); + if (addr == null) return null; + return addr.getAddress().getHostAddress(); + } + + + public void resetActivity(Player p) { + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + } + + public boolean canAutoLogin(Player p) { + String ip = getPlayerIP(p); + if (ip == null) return false; + Long last = ipAutoLogin.get(ip); + return last != null + && System.currentTimeMillis() - last + < IP_SKIP_MS; + } + + public void recordIPLogin(Player p) { + String ip = getPlayerIP(p); + if (ip != null) + ipAutoLogin.put(ip, + System.currentTimeMillis()); + } + + public boolean isFrozen(Player p) { + return !loggedIn.contains(p.getName()); + } + private String debugCodePoints(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + if (i > 0) sb.append(" "); + sb.append(String.format("U+%04X", + (int) s.charAt(i))); + } + return sb.toString(); + } + + private boolean isAdmin(CommandSender sender) { + if (sender instanceof Player) + return ((Player) sender) + .getScoreboardTags() + .contains(config.adminTag); + return true; + } + + private void clearPlayer(Player p) { + p.getInventory().clear(); + p.getInventory().setArmorContents( + new ItemStack[4]); + p.getInventory().setExtraContents( + new ItemStack[1]); + p.setLevel(0); + p.setExp(0f); + } + + public void restoreInventory(Player p) { + String name = p.getName(); + try { + String[] backup = + db.loadInventoryBackup(name); + if (backup == null + || !isValidBackup(backup)) { + return; + } + + debugPrintBase64("恢复前", name, + backup[0]); + + ItemStack[] contents = + InventorySerializer + .fromBase64(backup[0]); + if (contents != null) + p.getInventory() + .setContents(contents); + + if (backup[1] != null + && !backup[1].isEmpty()) { + ItemStack[] armor = + InventorySerializer + .fromBase64( + backup[1]); + if (armor != null) + p.getInventory() + .setArmorContents( + armor); + } + + if (backup[2] != null + && !backup[2].isEmpty()) { + ItemStack[] extra = + InventorySerializer + .fromBase64( + backup[2]); + if (extra != null) + p.getInventory() + .setExtraContents( + extra); + } + + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) p.setLevel(lv); + double xp = Double.parseDouble( + backup[4]); + if (xp > 0) + p.setExp((float) xp); + + debugPrintBase64("恢复后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + getLogger().info( + "[Sdf1_login] " + name + + " 背包已恢复"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 还原背包失败: " + + name); + e.printStackTrace(); + } + } + + + public void backupInventory(Player p) { + String name = p.getName(); + int cnt = countItems(p) + + countArmor(p) + + countExtra(p); + if (cnt == 0) { + getLogger().warning( + "[Sdf1_login] " + name + + " 背包为空,跳过备份"); + return; + } + try { + String invB64 = + InventorySerializer.toBase64( + p.getInventory() + .getContents()); + String armorB64 = + InventorySerializer.toBase64( + p.getInventory() + .getArmorContents()); + String extraB64 = + InventorySerializer.toBase64( + p.getInventory() + .getExtraContents()); + db.saveInventoryBackup(name, invB64, + armorB64, extraB64, + p.getLevel(), p.getExp()); + getLogger().info( + "[Sdf1_login] " + name + + " 背包已备份 (物品:" + + cnt + ")"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 背包备份失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventory(Player p) { + String name = p.getName(); + try { + debugPrintBase64("清空前", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + // 清空背包(备份已在上次退出时保存) + p.getInventory().clear(); + p.getInventory() + .setArmorContents(null); + p.getInventory() + .setExtraContents(null); + p.setLevel(0); + p.setExp(0); + + debugPrintBase64("清空后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] hideInventory失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventoryDelayed(Player p, + int attempt) { + String name = p.getName(); + if (!p.isOnline()) return; + if (!isFrozen(p)) return; + + // 从数据库加载已有备份 + String[] existing = + db.loadInventoryBackup(name); + + // 调试打印 + debugPrintBase64("加入-加载DB", name, + existing != null + ? existing[0] : null); + + // 无有效备份 → 跳过清空背包 + if (existing == null + || !isValidBackup(existing)) { + getLogger().info( + "[Sdf1_login] " + name + + " 无有效备份," + + "跳过清空背包"); + return; + } + + // 有有效备份 → 清空当前背包 + hideInventory(p); + } + + + public void autoLogin(Player p, + String registerType) { + String name = p.getName(); + String ip = getPlayerIP(p); + String uuid = + p.getUniqueId().toString(); + String defaultPwd = "123456aA"; + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + if (!"manual".equals( + config.approvalMode)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + String salt = + PasswordUtils.generateSalt(); + String hash = PasswordUtils.hash( + defaultPwd, salt); + db.createUser(name, hash, salt); + db.setField(name, + "premium_uuid", uuid); + db.setField(name, + "register_ip", ip); + db.setField(name, + "register_type", registerType); + db.recordIP(name, ip); + } + + loggedIn.add(name); + p.setAllowFlight(false); + p.setFlying(false); + + + db.setLoggedIn(name, true); + db.setField(name, + "last_login_time", + System.currentTimeMillis()); + db.setField(name, + "last_online_check", + System.currentTimeMillis()); + db.recordIP(name, ip); + joinTime.remove(p.getUniqueId()); + recordIPLogin(p); + + // 只在有有效备份时恢复 + restoreInventory(p); + + giveMenuSnowball(p); + p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); + if (welcome != null) welcome.onLogin(p); + activateBeibao(p); + pushPendingAlerts(p); + + + } + + private void activateBeibao(Player p) { + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, p.getName(), + 0, 0); + } catch (Exception ignored) { + } + } + + public void triggerSecurityAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + db.recordSecurityAlert(name, suspiciousIP); + Bukkit.broadcastMessage( + "§c§l[安全警告] §e" + name + + " §f账号疑似遭到 §c" + + suspiciousIP + + " §f的盗号尝试"); + String emailAddr = + (String) db.getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String body = "玩家 " + name + + ":\n您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + + suspiciousIP + + " 的盗号尝试。\n" + + "建议立即修改密码。"; + final String to = emailAddr; + final String finalBody = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, name, + finalBody)); + } + } + + public void pushPendingAlerts(Player p) { + String name = p.getName(); + List> alerts = + db.getUnnotifiedAlerts(name); + if (alerts.isEmpty()) return; + p.sendMessage("§c§l========== 安全报警 =========="); + for (Map alert : alerts) { + p.sendMessage("§e时间: §f" + + alert.get("alert_date")); + p.sendMessage("§e可疑IP: §c" + + alert.get("alert_ip")); + db.markAlertNotified( + ((Number) alert.get("id")) + .intValue()); + } + p.sendMessage( + "§c建议使用 §e/sdf1_login pw §c修改密码"); + p.sendMessage( + "§c§l=================================="); + } + + public void handleReset(Player p) { + loginMgr.handleReset(p); + } + + public boolean dailySign(Player p) { + if (checkIn.isCheckedInToday(p.getName())) + return false; + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(p.getName()); + return true; + } + + public void openTaskPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l任务面板"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + + // 打卡按钮(独立,slot 11) + g.setItem(11, mkItem(Material.COMPASS, + "§b§l群系打卡", + "§7在不同群系打卡完成任务", + "§7打卡范围: 32格去重")); + + // 新人任务(slot 15) + g.setItem(15, mkItem(Material.BOOK, + "§e新人任务", + "§7完成新手阶段获取奖励")); + + // 返回 + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + public void openAccountRequestPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l账号请求"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + g.setItem(10, mkItem(Material.BARRIER, + "§c申请删除账号", + "§7申请删除自己名下的账号")); + g.setItem(13, mkItem(Material.PAPER, + "§6提交工单", + "§7提交bug反馈、求助、举报等")); + g.setItem(16, mkItem(Material.REDSTONE, + "§e找回密码", + "§7通过邮箱找回密码")); + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private void playSuccessSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private int countItems(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countArmor(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getArmorContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countExtra(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getExtraContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + /** + * 检查备份是否有效(包含非雪球的真实物品) + */ + public boolean isValidBackup(String[] backup) { + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) { + return false; + } + try { + ItemStack[] items = + InventorySerializer + .fromBase64(backup[0]); + if (items == null) return false; + for (ItemStack item : items) { + if (item == null + || item.getType() + == Material.AIR) + continue; + if (isMenuSnowball(item)) + continue; + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * 控制台打印 base64 前10位 + */ + private void debugPrintBase64(String label, + String name, String base64) { + if (base64 == null || base64.isEmpty()) { + getLogger().info( + "[BACKUP_DEBUG] " + label + + " " + name + + ": "); + return; + } + String preview = + base64.length() > 10 + ? base64.substring(0, 10) + : base64; + getLogger().info( + "[BACKUP_DEBUG] " + label + " " + + name + ": " + preview); + } + + private ItemStack mkItem(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + private String buildIPKickMessage(Player p, + String ip) { + List> accounts = + db.getAccountDetailsByIP(ip); + StringBuilder sb = new StringBuilder(); + sb.append("§c§l======== IP注册已满 ========\n"); + sb.append("§e您的IP §f").append(ip) + .append(" §e下已注册 §f") + .append(accounts.size()) + .append(" §e个账号:\n"); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int idx = 1; + for (Map acc : accounts) { + long regTime = ((Number) acc.get( + "register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format(new java.util.Date( + regTime)) + : "未知"; + sb.append("§7").append(idx) + .append(". §f") + .append(acc.get("player_name")) + .append(" §7注册于: §e") + .append(dateStr).append("\n"); + idx++; + } + sb.append("§7-----------------------------\n"); + sb.append("§e请使用已有账号登录\n"); + sb.append("§e登录后输入 §f/oa §e提交工单\n"); + sb.append("§c§l============================"); + return sb.toString(); + } + + /** + * 读取备份,返回物品名预览 + */ + private String getBackupItemPreview( + int backupId) { + try { + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) + return "§7空"; + ItemStack[] items = + InventorySerializer + .fromBase64( + backup[0]); + if (items == null) + return "§7空"; + Map counts = + new LinkedHashMap<>(); + for (ItemStack it : items) { + if (it == null + || it.getType() + == Material.AIR) + continue; + String name = translateMaterial( + it.getType().name()); + counts.merge(name, + it.getAmount(), + Integer::sum); + } + if (counts.isEmpty()) + return "§7空"; + StringBuilder sb = + new StringBuilder(); + int shown = 0; + for (Map.Entry en + : counts.entrySet()) { + if (shown >= 5) { + sb.append(" §7..."); + break; + } + if (shown > 0) + sb.append("§7, "); + sb.append("§f").append(en.getKey()) + .append("§7x") + .append(en.getValue()); + shown++; + } + return sb.toString(); + } catch (Exception e) { + return "§7解析失败"; + } + } + + private String translateMaterial( + String name) { + switch (name) { + case "GRASS_BLOCK": + return "草方块"; + case "OAK_LOG": + return "橡木原木"; + case "OAK_PLANKS": + return "橡木木板"; + case "STONE": + return "石头"; + case "COBBLESTONE": + return "圆石"; + case "DIRT": + return "泥土"; + case "SAND": + return "沙子"; + case "GRAVEL": + return "砂砾"; + case "DIAMOND": + return "钻石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "IRON_INGOT": + return "铁锭"; + case "GOLD_INGOT": + return "金锭"; + case "COAL": + return "煤炭"; + case "COAL_ORE": + return "煤矿石"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "OAK_LEAVES": + return "橡树树叶"; + case "OAK_SAPLING": + return "橡树树苗"; + case "STICK": + return "木棍"; + case "CRAFTING_TABLE": + return "工作台"; + case "FURNACE": + return "熔炉"; + case "CHEST": + return "箱子"; + case "TORCH": + return "火把"; + case "BEDROCK": + return "基岩"; + case "TNT": + return "TNT"; + case "REDSTONE": + return "红石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "LAPIS_LAZULI": + return "青金石"; + case "EMERALD": + return "绿宝石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "OBSIDIAN": + return "黑曜石"; + case "GLOWSTONE": + return "萤石"; + case "ARROW": + return "箭"; + case "BOW": + return "弓"; + case "IRON_SWORD": + return "铁剑"; + case "DIAMOND_SWORD": + return "钻石剑"; + case "IRON_PICKAXE": + return "铁镐"; + case "DIAMOND_PICKAXE": + return "钻石镐"; + case "IRON_AXE": + return "铁斧"; + case "DIAMOND_AXE": + return "钻石斧"; + case "IRON_HELMET": + return "铁头盔"; + case "IRON_CHESTPLATE": + return "铁胸甲"; + case "IRON_LEGGINGS": + return "铁护腿"; + case "IRON_BOOTS": + return "铁靴子"; + case "DIAMOND_HELMET": + return "钻石头盔"; + case "DIAMOND_CHESTPLATE": + return "钻石胸甲"; + case "DIAMOND_LEGGINGS": + return "钻石护腿"; + case "DIAMOND_BOOTS": + return "钻石靴子"; + case "BREAD": + return "面包"; + case "COOKED_BEEF": + return "牛排"; + case "COOKED_PORKCHOP": + return "熟猪排"; + case "COOKED_CHICKEN": + return "熟鸡肉"; + case "GOLDEN_APPLE": + return "金苹果"; + case "ENDER_PEARL": + return "末影珍珠"; + case "BLAZE_ROD": + return "烈焰棒"; + case "GHAST_TEAR": + return "恶魂之泪"; + case "NETHER_STAR": + return "下界之星"; + case "SHULKER_SHELL": + return "潜影壳"; + case "ELYTRA": + return "鞘翅"; + case "TOTEM_OF_UNDYING": + return "不死图腾"; + case "TRIDENT": + return "三叉戟"; + case "ENCHANTED_GOLDEN_APPLE": + return "附魔金苹果"; + default: + // 转中文名格式:SANDSTONE -> 沙石 + return name.toLowerCase() + .replace("_", " "); + } + } + + // ===== Events ===== + @EventHandler + public void onJoin(PlayerJoinEvent e) { + + Player p = e.getPlayer(); + String name = p.getName(); + String ip = getPlayerIP(p); + // 强制重发资源包(清掉客户端拒绝记录) + Bukkit.getScheduler() + .runTaskLater(this, () -> { + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + }, 20L); + // 重置聊天输入状态(防止上次退出时残留) + chatInput.reset(p); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); +// 改完了直接打包,要打sdf1一起 + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + + boolean isBedrock = + verification.isBedrockPlayer(p); + boolean isOnlineMode = + verification.isOnlineMode(); + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + } + if (isFrozen(p)) { + p.setAllowFlight(true); + } + + if (!"manual".equals(config.approvalMode) + && !db.userExists(name)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + // [ADDED] 资源包(延迟发送,不强制) + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + if (isOnlineMode) { + autoLogin(p, "premium"); + return; + } + if (isBedrock) { + autoLogin(p, "bedrock"); + return; + } + if (canAutoLogin(p)) { + autoLogin(p, "ip_reconnect"); + return; + } + afk.remove(p.getUniqueId()); + afk.recordAction(p.getUniqueId()); + hideInventoryDelayed(p, 0); + // [ADDED] 发送资源包 + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + + // [ADDED] 发送资源包(延迟5秒确保客户端就绪) + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + + + verification.verifyPremiumAsync(p, + isPremium -> { + if (isPremium) { + Bukkit.getScheduler() + .runTask(this, + () -> autoLogin(p, + "premium")); + } else { + Bukkit.getScheduler() + .runTask(this, () -> { + joinTime.put( + p.getUniqueId(), + System.currentTimeMillis()); + if (!db.userExists(name)) + p.sendMessage(config.msg( + "not_registered")); + else + p.sendMessage(config.msg( + "not_logged_in")); + }); + } + }); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player p = e.getPlayer(); + String name = p.getName(); + chatInput.reset(p); + + boolean wasLoggedIn = + loggedIn.contains(name); + + // 判断是否需要保存: + // 1. 已登录 → 保存当前背包 + // 2. 未登录但有真实物品 → 保存 + // (插件安装前的老玩家) + // 3. 未登录且背包被插件清空 → 不保存 + // (避免覆盖有效备份) + boolean hasRealItems = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (it != null + && it.getType() + != Material.AIR + && !isMenuSnowball(it)) { + hasRealItems = true; + break; + } + } + + if (wasLoggedIn || hasRealItems) { + try { + String contents = + InventorySerializer + .toBase64( + p.getInventory() + .getContents()); + String armor = + InventorySerializer + .toBase64( + p.getInventory() + .getArmorContents()); + String extra = + InventorySerializer + .toBase64( + p.getInventory() + .getExtraContents()); + int level = p.getLevel(); + double exp = p.getExp(); + + debugPrintBase64( + "退出保存", name, + contents); + db.saveInventoryBackup(name, + contents, armor, extra, + level, exp); + } catch (Exception ex) { + getLogger().severe( + "[Sdf1_login] " + + "退出保存背包失败: " + + name); + } + } else { + getLogger().info( + "[BACKUP_DEBUG] 退出跳过 " + + name + + ": 背包为空" + + "(插件已清空)"); + } + + if (wasLoggedIn) { + loggedIn.remove(name); + db.setLoggedIn(name, false); + recordIPLogin(p); + } + + lastActivity.remove(p.getUniqueId()); + joinTime.remove(p.getUniqueId()); + joinLoc.remove(p.getUniqueId()); + needsPasswordChange.remove(name); + // [ADDED] 清除 Radio 熔断状态 + // [ADDED] 清除 Radio 熔断状态 + if (radioDL != null) + radioDL.onPlayerQuit( + p.getUniqueId()); + pendingAdminAuth.remove(name); + } + + private static final String MENU_SNOWBALL_TAG = + "sdf1_menu"; + + public void giveMenuSnowball(Player p) { + Object val = db.getField( + p.getName(), "menu_snowball"); + if (val != null) { + int on = val instanceof Number + ? ((Number) val).intValue() : 1; + if (on == 0) return; + } + + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) return; + if (isCustomMenuTrigger(it)) return; + } + + ItemStack customIcon = + menuIconMgr.getIcon(p); + if (customIcon != null) { + tagAsMenuTrigger(customIcon); + // 加外观标识 + ItemMeta cm = customIcon.getItemMeta(); + if (cm != null) { + String originalName = + cm.getDisplayName(); + if (originalName == null + || originalName.isEmpty() + || originalName.equals( + customIcon.getType() + .name())) { + originalName = "自定义物品"; + } + cm.setDisplayName( + "\u00a7e\u00a7l[菜单] \u00a7f" + + originalName); + List cmLore = + cm.hasLore() + ? new ArrayList<>(cm.getLore()) + : new ArrayList<>(); + // 避免重复添加 + if (cmLore.isEmpty() + || !cmLore.get(0).contains( + "\u00a77右键打开主菜单")) { + cmLore.add(0, + "\u00a77右键打开主菜单"); + } + cm.setLore(cmLore); + customIcon.setItemMeta(cm); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, customIcon); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), customIcon); + } + return; + } + + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "\u00a7e\u00a7l[菜单] \u00a7f右键打开主菜单"); + List lore = new ArrayList<>(); + lore.add("\u00a77右键点击打开功能菜单"); + lore.add("\u00a78" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem(slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + + /** + * 手动发放雪球菜单(绕过开关检查) + */ + public void forceGiveMenuSnowball( + Player p) { + // 只检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + private boolean isMenuSnowball(ItemStack item) { + if (item == null + || item.getType() + != Material.SNOWBALL) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null) return false; + if (!im.hasLore()) return false; + List lore = im.getLore(); + if (lore == null) return false; + for (String line : lore) { + if (line.contains(MENU_SNOWBALL_TAG)) + return true; + } + return false; + } + // 运行时初始化版本(避免静态加载问题) + private NamespacedKey getTriggerKey() { + return new NamespacedKey(this, "menu_trigger"); + } + + /** + * 给物品打上菜单触发标记 + */ + private void tagAsMenuTrigger(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + meta.getPersistentDataContainer().set( + getTriggerKey(), + PersistentDataType.STRING, + "true"); + item.setItemMeta(meta); + } + + /** + * 检查物品是否带菜单触发标记 + */ + private boolean isCustomMenuTrigger( + ItemStack item) { + if (item == null) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return meta.getPersistentDataContainer() + .has(getTriggerKey(), + PersistentDataType.STRING); + } + + + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + + // 冻结处理(未登录玩家不能移动) + if (isFrozen(p)) { + if (from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + e.setCancelled(true); + p.setAllowFlight(true); + } + } + + private List filterTab(List opts, String prefix) { + List r = new ArrayList<>(); + for (String o : opts) { + if (o.toLowerCase().startsWith(prefix.toLowerCase())) { + r.add(o); + } + } + return r; + } + + + /* + 雪球菜单: + 同时检测左右手 + */ + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + ItemStack hand = p.getInventory() + .getItemInMainHand(); + ItemStack offhand = p.getInventory() + .getItemInOffHand(); + + if (isMenuSnowball(hand) + || isMenuSnowball(offhand)) { + e.setCancelled(true); + gui.openMain(p); + return; + } + + if (isCustomMenuTrigger(hand) + || isCustomMenuTrigger(offhand)) { + if (db.hasMenuIcon(p.getName())) { + e.setCancelled(true); + gui.openMain(p); + } + } + } + + + @EventHandler + public void onBlockBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + afk.recordAction(p.getUniqueId()); + // [ADDED] 任务计数 + if (questTracker != null) + questTracker.onBlockMined(p.getName(), + e.getBlock().getType().name()); + + } + + @EventHandler + public void onBlockPlace2(BlockPlaceEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) return; + afk.recordAction(p.getUniqueId()); + if (questTracker != null) + questTracker.onBlockPlaced( + p.getName(), + e.getBlock().getType().name()); + } + + @EventHandler + public void onChatTrack(AsyncPlayerChatEvent e) { + if (isFrozen(e.getPlayer())) return; + if (questTracker != null) + questTracker.onPlayerChat( + e.getPlayer().getName()); + } + + + @EventHandler + public void onBlockPlace(BlockPlaceEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + if (isFrozen(e.getPlayer())) { + e.setCancelled(true); + return; + } + ItemStack drop = + e.getItemDrop().getItemStack(); + if (isMenuSnowball(drop)) { + e.setCancelled(true); + } + } + + + @EventHandler + public void onPickup(PlayerPickupItemEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDamage(EntityDamageEvent e) { + if (e.getEntity() instanceof Player) { + if (isFrozen((Player) e.getEntity())) + e.setCancelled(true); + } + } + + @EventHandler + public void onEntityAttack( + EntityDamageByEntityEvent e) { + if (e.getDamager() instanceof Player + && isFrozen( + (Player) e.getDamager())) + e.setCancelled(true); + if (e.getEntity() instanceof Player + && isFrozen( + (Player) e.getEntity())) + e.setCancelled(true); + } + + // [ADDED] 击杀追踪 + @EventHandler + public void onEntityDeath(EntityDeathEvent e) { + if (questTracker == null) return; + Player killer = e.getEntity().getKiller(); + if (killer == null || isFrozen(killer)) return; + if (e.getEntity() instanceof Player) + questTracker.onPlayerKill(killer.getName()); + else + questTracker.onMobKill(killer.getName(), + e.getEntityType().name()); + } + + // [ADDED] 合成追踪 + @EventHandler + public void onCraft(CraftItemEvent e) { + if (e.getWhoClicked() instanceof Player) { + Player p = (Player) e.getWhoClicked(); + if (!isFrozen(p) && questTracker != null) + questTracker.onItemCrafted(p.getName()); + } + } + + // [ADDED] 钓鱼追踪 + @EventHandler + public void onFish(PlayerFishEvent e) { + Player p = e.getPlayer(); + if (p == null || isFrozen(p)) return; + if (questTracker != null + && e.getState() == PlayerFishEvent.State.CAUGHT_FISH) + questTracker.onFishCaught(p.getName()); + } + + // [ADDED] 村民交易追踪 + @EventHandler(priority = EventPriority.MONITOR, + ignoreCancelled = true) + public void onVillagerTrade(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) return; + if (questTracker == null) return; + + Inventory topInv = + e.getView().getTopInventory(); + if (topInv == null + || topInv.getHolder() == null) + return; + if (!(topInv.getHolder() + instanceof org.bukkit.entity.Villager)) + return; + + // 结果槽 = 2 + if (e.getRawSlot() != 2) return; + ItemStack result = topInv.getItem(2); + if (result == null + || result.getType() == Material.AIR) + return; + + final Player fp = p; + Bukkit.getScheduler().runTaskLater(this, + () -> { + if (fp.isOnline()) + questTracker.onVillagerTrade( + fp.getName()); + }, 2L); + } + + + + @EventHandler + public void onChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + String msg = e.getMessage(); + // ★ CDK/经济/债券输入拦截 — 必须在所有聊天处理之前 + if (this.getCDK() != null && this.getCDK().isListening(p.getName())) { + e.setCancelled(true); + this.getCDK().onChat(p, msg); + return; + } + + +// ===== 菜单聊天输入 ===== + if (getMenu().isEditing(p.getName())) { + e.setCancelled(true); + getMenu().onChat(p, msg); + return; + } + + // ===== 冻结检查 ===== + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + // ===== 聊天输入流程(改密码/绑邮箱等) ===== + if (chatInput.isInFlow(p)) { + e.setCancelled(true); + chatInput.handleInput(p, msg, this); + return; + } + + // ===== 管理员密码验证 ===== + if (pendingAdminAuth.contains(p.getName())) { + e.setCancelled(true); + pendingAdminAuth.remove(p.getName()); + if (msg.equals(config.adminPassword)) { + p.sendMessage("§a验证通过"); + p.sendMessage("§7输入的密码已验证"); + } else { + p.sendMessage("§c密码错误"); + } + return; + } + + // ===== 聊天过滤 - 开关检查 ===== + if (!chatFilter.isEnabled()) return; + + // ===== 白名单玩家 ===== + if (chatFilter.isPlayerWhitelisted( + p.getName())) return; + + // ===== 禁言检查 ===== + if (chatFilter.isMuted(p.getName())) { + e.setCancelled(true); + p.sendMessage(chatFilter.msg( + "chat_muted")); + return; + } + + // ===== URL检测 ===== + List urls = + chatFilter.extractUrls(msg); + if (urls.isEmpty()) return; + + // 过滤白名单 + List blocked = new ArrayList<>(); + for (String url : urls) { + if (!chatFilter.isWhitelisted(url)) { + blocked.add(url); + } + } + if (blocked.isEmpty()) return; + + // 取消消息 + e.setCancelled(true); + + for (String url : blocked) { + p.sendMessage(chatFilter.msg( + "chat_url_blocked", + "url", url)); + } + + // 增加违规计数 + chatFilter.incrementViolation(p.getName()); + int count = chatFilter.getViolationCount( + p.getName()); + p.sendMessage(chatFilter.msg( + "chat_url_violation", + "count", String.valueOf(count))); + + // 通知管理员 + if (chatFilter.isNotifyAdmin()) { + String notify = chatFilter.msg( + "chat_url_admin_notify", + "player", p.getName(), + "url", blocked.get(0), + "count", String.valueOf(count)); + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.hasPermission( + "sdf1.admin") + || op.isOp()) { + op.sendMessage(notify); + } + } + } + + // 通报 + if (chatFilter.isNotifyAll()) { + Bukkit.broadcastMessage( + chatFilter.msg( + "chat_url_broadcast", + "player", + p.getName())); + } + + // 处罚 + chatFilter.applyPunishment(p, count); + // ===== 补签确认 ===== + if (pendingBackCheck.containsKey( + p.getName())) { + e.setCancelled(true); + String data = pendingBackCheck + .remove(p.getName()); + String[] parts = data.split(":"); + String dateStr = parts[0]; + int totalPoints = + Integer.parseInt(parts[1]); + + if (msg.trim().equals("1") + || msg.trim() + .equalsIgnoreCase("yes") + || msg.trim().contains("是") + || msg.trim().contains("愿意")) { + int extra = totalPoints - 10; + if (extra > 0 + && !db.deductPoints( + p.getName(), extra)) { + db.addPoints(p.getName(), 10); + p.sendMessage("§c积分也不足," + + "10积分已退还"); + return; + } + checkIn.doBackCheckIn(p, dateStr); + p.sendMessage("§a补签成功!日期: " + + dateStr + " §7(扣除" + + totalPoints + + "积分,免经济)"); + playSuccessSound(p); + } else { + db.addPoints(p.getName(), 10); + p.sendMessage("§c补签已取消," + + "10积分已退还"); + } + return; + } + + } + + // 在 getter 区域添加 + public MenuManager getMenu() { + return menu; + } + public PVPManager getPVPManager() { + return pvpManager; + } + // 在 Main.java 中(与其他 getter 放一起) + public AreaProtection getAreaProtection() { + return areaProtection; + } + +// ==================== 我的钱包 ==================== + + public void openMyWallet(Player p) { + openWalletGUI(p, p.getName()); + } + + private void openWalletGUI(Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l我的钱包"); + + + ItemStack glass = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta gm = glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, glass); + + // 债券余额 (slot 4) + int balance = bondManager.getBonds(target); + g.setItem(4, mkItem(Material.GOLD_INGOT, + "§6§l债券余额: §e" + balance + " §6枚", + "§7当前持有的债券数量")); + + // 账户状态 (slot 10) + String status = bondManager + .getAccountStatus(target); + if ("frozen".equals(status)) { + g.setItem(10, mkItem(Material.REDSTONE, + "§c§l账户状态: 冻结中", + "§7无法进行任何资金交易")); + } else { + g.setItem(10, mkItem(Material.EMERALD, + "§a§l账户状态: 正常", + "§7账户运行正常")); + } + + // 同IP玩家 (slot 11) + if (target.equals(p.getName())) { + List sameIP = bondManager + .getSameIPPlayers(target); + List lore = new ArrayList<>(); + lore.add("§7同IP下注册的其他玩家:"); + if (sameIP.isEmpty()) { + lore.add("§a 无"); + } else { + for (String n : sameIP) + lore.add("§c - " + n); + } + lore.add("§7"); + lore.add("§7转账限制:"); + lore.add("§7 不能与同IP玩家转账"); + g.setItem(11, mkItem(Material.PAPER, + "§e§l同IP玩家", lore.toArray( + new String[0]))); + } + + // 近7天流水 (slot 12) + g.setItem(12, mkItem(Material.BOOK, + "§e近7天流水", + "§7点击查看最近7天交易记录")); + + // 近14天流水 (slot 13) + g.setItem(13, mkItem(Material.BOOK, + "§e近14天流水", + "§7点击查看最近14天交易记录", + "§8(系统最大支持14天)")); + + // 转账记录 (slot 14) + g.setItem(14, mkItem(Material.PAPER, + "§e转账记录", + "§7点击查看所有转账明细")); + + // 返回 (slot 22) + g.setItem(22, mkItem(Material.ARROW, + "§7← 返回")); + + walletViewTarget.put(p.getName(), target); + p.openInventory(g); + } + +// ==================== 流水记录GUI ==================== + + public void openWalletTransactions( + Player p, int days) { + String target = walletViewTarget.getOrDefault( + p.getName(), p.getName()); + openWalletTxGUI(p, target, days, false); + } + + public void openWalletTransfers(Player p) { + String target = walletViewTarget.getOrDefault( + p.getName(), p.getName()); + openWalletTxGUI(p, target, 14, true); + } + + private void openWalletTxGUI(Player p, + String target, + int days, + boolean transfersOnly) { + days = Math.min(days, 14); // 普通玩家最大14天 + String title = transfersOnly + ? "§e§l转账记录 - " + target + : "§e§l流水记录 - 近" + days + + "天 - " + target; + + Inventory g = Bukkit.createInventory( + null, 54, title); + + List> txs = transfersOnly + ? bondManager.getTransferTransactions( + target, days) + : bondManager.getTransactions( + target, days); + + int slot = 0; + for (Map tx : txs) { + if (slot >= 45) break; + + String type = (String) tx.get("type"); + int amount = ((Number) tx.get("amount")) + .intValue(); + String tgt = (String) tx.get("target_player"); + String reason = (String) tx.get("reason"); + int bal = ((Number) tx.get("balance_after")) + .intValue(); + long time = ((Number) tx.get("time")) + .longValue(); + + boolean isExpense = "shop_buy".equals(type) + || "shop_packing".equals(type); + boolean pos = isExpense ? false : (amount > 0); + String sign = pos ? "+" : (isExpense ? "-" : ""); + String color = pos ? "§a" : (isExpense ? "§c" : (amount < 0 ? "§c" : "")); +// 金额取绝对值显示 + int displayAmount = Math.abs(amount); + + + // 用 if-else 替代 switch 避免编译问题 + Material mat = Material.PAPER; + if (BondManager.TX_TRANSFER_OUT.equals(type)) { + mat = Material.RED_WOOL; + } else if (BondManager.TX_TRANSFER_IN + .equals(type)) { + mat = Material.LIME_WOOL; + } else if (BondManager.TX_ADMIN_GIVE + .equals(type)) { + mat = Material.GOLD_BLOCK; + } else if (BondManager.TX_ADMIN_DEDUCT + .equals(type)) { + mat = Material.REDSTONE_BLOCK; + } else if (BondManager.TX_REDEEM + .equals(type)) { + mat = Material.NAME_TAG; + } else if (BondManager.TX_SHOP_BUY + .equals(type)) { + mat = Material.CHEST; + } else if (BondManager.TX_FREEZE + .equals(type)) { + mat = Material.BARRIER; + } else if (BondManager.TX_UNFREEZE + .equals(type)) { + mat = Material.EMERALD_BLOCK; + } else if (pos) { + mat = Material.LIME_WOOL; + } else { + mat = Material.RED_WOOL; + } + String typeName = walletFormatType(type); + + List lore = new ArrayList<>(); + lore.add("§7类型: " + typeName); + lore.add("§7金额: " + color + sign + displayAmount + + " §6枚"); + + if (tgt != null && !tgt.isEmpty()) + lore.add("§7对象: §e" + tgt); + if (reason != null && !reason.isEmpty()) + lore.add("§7理由: §7" + reason); + lore.add("§7余额: §f" + bal + " §6枚"); + lore.add("§8" + TX_SDF.format( + new Date(time))); + + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName(color + sign + displayAmount + + " §6枚 - " + typeName); + + im.setLore(lore); + item.setItemMeta(im); + } + g.setItem(slot, item); + slot++; + } + + if (txs.isEmpty()) { + ItemStack empty = new ItemStack( + Material.BARRIER); + ItemMeta em = empty.getItemMeta(); + if (em != null) { + em.setDisplayName("§7暂无记录"); + empty.setItemMeta(em); + } + g.setItem(22, empty); + } else if (txs.size() > 45) { + g.setItem(49, mkItem(Material.PAPER, + "§7还有 " + (txs.size() - 45) + + " 条未显示")); + } + + g.setItem(53, mkItem(Material.ARROW, + "§7← 返回钱包")); + + p.openInventory(g); + } + + + private String walletFormatType(String type) { + if (type == null) return "未知"; + switch (type) { + case BondManager.TX_ADMIN_GIVE: + return "管理员发放"; + case BondManager.TX_ADMIN_DEDUCT: + return "管理员扣除"; + case BondManager.TX_TRANSFER_OUT: + return "转出"; + case BondManager.TX_TRANSFER_IN: + return "转入"; + case BondManager.TX_REDEEM: + return "CDK兑换"; + case BondManager.TX_SHOP_BUY: + return "商城购买"; + case BondManager.TX_DAILY_SIGN: + return "签到奖励"; + case BondManager.TX_FREEZE: + return "冻结"; + case BondManager.TX_UNFREEZE: + return "解冻"; + default: + return type; + } + } + + + // ===== Inventory Click(旧逻辑 + 新主菜单布局 + 新功能) ===== + @EventHandler + public void onInvClick(InventoryClickEvent e) { + if (e.isCancelled()) return; + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + String title = e.getView().getTitle(); + int slot = e.getRawSlot(); + + // ===== 工单 ===== + if (title.equals(TicketManager.T_TICKET_MAIN) + || title.equals( + TicketManager.T_TICKET_CREATE) + || title.equals( + TicketManager.T_TICKET_MY) + || title.equals( + TicketManager.T_TICKET_ADMIN) + || title.equals( + TicketManager.T_TICKET_DETAIL) + || title.equals("§d§l服务商面板") + || title.equals("§6§l抢单大厅") + || title.equals( + "§b§l我处理中的工单")) { + if (e.getClickedInventory() == null) + return; + if (e.getClickedInventory() + != e.getView().getTopInventory()) + return; + e.setCancelled(true); + ticket.handleClick(p, title, slot); + return; + } + + // ===== 垃圾箱 ===== + // ===== 垃圾回收站 ===== + if (title.equals("§6§l垃圾回收站")) { + if (e.getClickedInventory() == null) + return; + + // 玩家背包普通点击 → 完全不管 + if (e.getClickedInventory() + == p.getInventory() + && !e.isShiftClick()) { + return; + } + + // Shift+Click 从玩家背包存入 + if (e.isShiftClick() + && e.getClickedInventory() + == p.getInventory() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR + && !isMenuSnowball( + e.getCurrentItem())) { + e.setCancelled(true); + long now = System.currentTimeMillis(); + Long last = garbageBusy.get( + p.getUniqueId()); + if (last != null && now - last < 200L) + return; + garbageBusy.put(p.getUniqueId(), now); + ItemStack toSave = + e.getCurrentItem().clone(); + p.getInventory() + .setItem(e.getSlot(), null); + garbage.saveItem(toSave); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage(p, 0); + }, 1L); + return; + } + + // GUI 内点击 → 全部取消,由 garbage.handleClick 处理 + e.setCancelled(true); + + // 功能格 49-53 + if (slot >= 49 && slot <= 53) { + garbage.handleClick(p, title, slot); + return; + } + + // 光标有物品 → 存入 + if (slot < 54 + && e.getCursor() != null + && e.getCursor().getType() + != Material.AIR + && !isMenuSnowball( + e.getCursor())) { + ItemStack toSave = + e.getCursor().clone(); + p.getOpenInventory().setCursor(null); + garbage.saveItem(toSave); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage(p, 0); + }, 2L); + return; + } + + // 点击有 ID 的物品 → 取出 + if (slot < 54) { + ItemStack inSlot = + e.getInventory().getItem(slot); + if (inSlot != null + && inSlot.hasItemMeta() + && inSlot.getItemMeta() + .hasLore()) { + List lore = + inSlot.getItemMeta().getLore(); + if (lore != null && !lore.isEmpty() + && lore.get(0).startsWith( + "§7ID: #")) { + try { + String numStr = lore.get(0) + .replace( + "§7ID: #", "") + .trim(); + int dbId = + Integer.parseInt( + numStr); + if (garbage.removeItem( + dbId)) { + ItemStack give = + inSlot.clone(); + ItemMeta gm = + give.getItemMeta(); + if (gm == null) + gm = Bukkit + .getItemFactory() + .getItemMeta( + give + .getType()); + gm.setLore(null); + give.setItemMeta(gm); + p.getInventory() + .addItem(give); + e.getInventory() + .setItem( + slot, null); + } + } catch (Exception ex) { + p.sendMessage( + "§c取出失败"); + } + } + } + } + return; + } + + + // ===== 二级菜单(.txt)—— 移到前面 ===== + if (title.startsWith("§6§l") + && !title.equals(GUIManager.T_MAIN) + && !title.equals(GUIManager.T_ADMIN) + && !title.equals(GUIManager.T_MY_INFO) + && !title.equals(GUIManager.T_INVITE) + && !title.equals( + GUIManager.T_TASK_CENTER) + && !title.equals( + GUIManager.T_GIFT_STAGES) + && !title.equals("§6§l任务面板") + && !title.equals( + "§6§l垃圾回收站")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + // ★ bounds检查 ★ + if (slot < 45 + && slot < e.getInventory() + .getSize() + && e.getCurrentItem() != null) { + ItemMeta subLm = + e.getCurrentItem().getItemMeta(); + if (subLm != null + && subLm.getLore() != null) { + for (String subLn + : subLm.getLore()) { + if (subLn.contains( + "§7指令: §f")) { + String cmd = + subLn.replace( + "§7指令: §f", + "").trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals( + "null")) { + p.sendMessage( + "§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) + cmd = cmd.substring(1); + Bukkit.dispatchCommand( + p, cmd); + return; + } + } + } + } + return; + } + + // ===== [DEBUG] 标题追踪 ===== + { + String dt = title; + if (dt.contains("钱包") + || dt.contains("余额") + || dt.contains("流水") + || dt.contains("转账")) { + getLogger().info( + "[DEBUG-CLICK] " + + "player=" + p.getName() + + " title=[" + dt + "]" + + " slot=" + slot + + " codePoints=" + + debugCodePoints(dt)); + } + } + + // ===== 二级菜单(.txt)—— 排除所有固定GUI ===== + if (title.startsWith("§6§l") + && !title.equals(GUIManager.T_MAIN) + && !title.equals(GUIManager.T_ADMIN) + && !title.equals(GUIManager.T_MY_INFO) + && !title.equals(GUIManager.T_INVITE) + && !title.equals( + GUIManager.T_TASK_CENTER) + && !title.equals( + GUIManager.T_GIFT_STAGES) + && !title.equals("§6§l任务面板") + && !title.equals("§6§l垃圾回收站") + && !title.equals("§6§l我的钱包") + && !title.equals("§6§l余额操作")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot < 45 + && slot < e.getInventory() + .getSize() + && e.getCurrentItem() != null) { + ItemMeta subLm = e.getCurrentItem() + .getItemMeta(); + if (subLm != null + && subLm.getLore() != null) { + for (String subLn + : subLm.getLore()) { + if (subLn.contains( + "§7指令: §f")) { + String cmd = subLn + .replace("§7指令: §f", "") + .trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals("null")) { + p.sendMessage("§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) { + cmd = cmd.substring(1); + } + Bukkit.dispatchCommand(p, cmd); + return; + } + } + } + } + return; + } + + // ===== 我的钱包(唯一handler) ===== + if (title.equals("§6§l我的钱包")) { + e.setCancelled(true); + if (slot == 22) { + gui.openMain(p); + } else if (slot == 12) { + openWalletTransactions(p, 7); + } else if (slot == 13) { + openWalletTransactions(p, 14); + } else if (slot == 14) { + openWalletTransfers(p); + } + return; + } + + // ===== 流水/转账记录(唯一handler) ===== + if (title.startsWith("§e§l流水记录") + || title.startsWith("§e§l转账记录")) { + e.setCancelled(true); + if (slot == 53) { + openMyWallet(p); + } + return; + } + + // ===== 余额操作(唯一handler) ===== + if (title.equals("§d§l余额操作")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot >= 0 + && slot < e.getInventory().getSize()) { + ItemStack item = e.getInventory() + .getItem(slot); + if (item == null + || !item.hasItemMeta() + || !item.getItemMeta() + .hasDisplayName()) { + return; + } + // 在 slot == 49 的 return 之后、读取 item 之前 + // ItemStack item = e.getInventory().getItem(slot); + if (item == null) return; + Material mat = item.getType(); +// 以下是常见的装饰性玻璃板材质,按需扩展 + if (mat.name().contains("STAINED_GLASS") + || mat.name().contains("GLASS_PANE") + || mat == Material.LIGHT_BLUE_STAINED_GLASS_PANE + || mat == Material.GRAY_STAINED_GLASS_PANE + // ... 其他装饰物 + || mat == Material.BLACK_STAINED_GLASS_PANE) { + e.setCancelled(true); + return; // 装饰物品,不处理 + } + + String targetName = item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", ""); + if (targetName.equals(p.getName())) { + p.sendMessage("§c不能操作自己"); + return; + } + if (e.isLeftClick()) { + balanceTarget = targetName; + balanceGiveMode = true; + p.closeInventory(); + p.sendMessage( + "§e请输入金额和理由"); + p.sendMessage( + "§7例: +100 给款 / -100 扣款"); + } else if (e.isRightClick()) { + if (this.getCDK() != null) { + this.getCDK().requestInput( + p, "bond", + targetName); + } + p.closeInventory(); + p.sendMessage( + "§e【债券操作】对 §a" + + targetName + + " §e进行债券操作"); + p.sendMessage( + "§e输入 +金额 给债券," + + " -金额 扣债券"); + p.sendMessage( + "§7可附加理由: §a+100 " + + "活动奖励 §7不填默认:" + + "管理员手动调整"); + } + } + return; + } + + + + +// ===== 菜单管理 ===== + if (title.equals("§c§l菜单管理")) { + e.setCancelled(true); + if (slot == 26) { + gui.openMain(p); + } else if (slot == 22) { + gui.openEditor(p, -1); + } else if (slot >= 0 && slot < 9) { + List items = + getMenu().getItems(); + if (slot < items.size()) { + if (e.isLeftClick()) { + gui.openEditor(p, slot); + } else if (e.isRightClick()) { + MenuManager.MenuItem mi = + items.get(slot); + if ("OP".equals(mi.permType)) { + mi.permType = + "玩家"; + } else { + mi.permType = "OP"; + } + gui.openMenuManager(p); + p.sendMessage( + "§a" + mi.title + + " §7权限切换为: §e" + + mi.permType); + } + } + } + return; + } + + // ===== 全局 slot 检查(放后面) ===== + if (e.getClickedInventory() == null) return; + if (e.getClickedInventory() + != e.getView().getTopInventory()) + return; + if (slot < 0 || slot >= e.getInventory() + .getSize()) return; + if (e.getCurrentItem() == null) return; + + + // ==================== 群系传送(新) ==================== + if (title.equals(GUIManager.T_BIOME)) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot < 12) { + String[] cats = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沼泽", "河流", + "地下", "蘑菇岛", "下界", "末地" + }; + p.closeInventory(); + gui.teleportToBiome(p, cats[slot]); + } + return; + } + + // ===== 编辑器 ===== + if (title.equals(GUIManager.T_EDITOR)) { + e.setCancelled(true); + if (slot >= 27) return; + List el = + getMenu().getItems(); + int ei = el.size() - 1; + if (ei < 0) return; + MenuManager.MenuItem em = el.get(ei); + switch (slot) { + case 10: + getMenu().editTitle(p, ei); + break; + case 11: + getMenu().editCommand(p, ei); + break; + case 20: + em.permType = + "玩家".equals(em.permType) + ? "OP" : "玩家"; + gui.openEditor(p, ei); + break; + // 编辑器 case 22(保存) + case 22: + if (getMenu() != null) { + MenuManager.MenuItem last = + getMenu().getItems().get(ei); + if (last.command == null + || last.command.trim().isEmpty() + || last.command.trim() + .equals("/")) { + p.sendMessage( + "§c指令不能为空,请先填写指令"); + gui.openEditor(p, ei); + return; + } + getMenu().saveMenu(); + p.closeInventory(); + p.sendMessage("§a已保存!"); + gui.openMain(p); + } + break; + + case 24: + getMenu().loadMenu(); + // ★ 如果最后一项是空指令的新项,删掉 ★ + List cancelItems = + getMenu().getItems(); + if (!cancelItems.isEmpty()) { + MenuManager.MenuItem last = + cancelItems + .get(cancelItems.size() - 1); + if ((last.command == null + || last.command.trim().isEmpty() + || last.command.trim().equals("/")) + && "新菜单项" + .equals(last.title)) { + cancelItems.remove( + cancelItems.size() - 1); + } + } + p.closeInventory(); + p.sendMessage("§c已取消"); + gui.openMain(p); + break; + } + } + // ==================== 主菜单(新布局) ==================== + // ==================== 主菜单(新布局) ==================== + if (title.equals(GUIManager.T_MAIN)) { + e.setCancelled(true); + + // ★ 统一:所有自定义菜单项通过 lore 识别 ★ + if (e.getCurrentItem() != null) { + ItemMeta meta = e.getCurrentItem() + .getItemMeta(); + if (meta != null && meta.getLore() != null) { + for (String ln : meta.getLore()) { + if (ln.contains("§7指令: §f")) { + String cmd = ln.replace( + "§7指令: §f", "").trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals("null")) { + p.sendMessage("§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) { + cmd = cmd.substring(1); + } + if (cmd.endsWith(".txt")) { + gui.openSubMenu(p, + cmd.substring(1)); + } else { + Bukkit.dispatchCommand(p, cmd); + } + return; + } + } + } + } + + // 固定按钮 + if (slot == 10) { + gui.openMyInfo(p); + } else if (slot == 12) { + gui.openInvite(p); + } else if (slot == 14) { + p.openInventory(points.createShopGUI(p)); + } else if (slot == 16) { + gui.openBiomeMenu(p); + } else if (slot == 19) { + ticket.openMain(p); + } else if (slot == 20) { + openMyWallet(p); + } else if (slot == 21) { + dailySignWithReward(p); + gui.openMain(p); + } else if (slot == 23) { + gui.openTaskCenter(p); + } else if (slot == 25) { + if (p.isOp() || isAdmin(p)) { + garbage.openRecycle(p); + } + } else if (slot == 29) { + if (isAdmin(p)) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType.ADMIN_AUTH; + p.sendMessage("请输入管理密码:"); + } + } else if (slot == 31) { + if (p.isOp()) { + gui.openMenuManager(p); + } + } else if (slot == 34) { + if (p.isOp() || isAdmin(p)) { + gui.openBalanceOps(p); + } + } + + + return; + } + + // ==================== 新人礼包 ==================== + if (title.equals(GUIManager.T_GIFT_STAGES)) { + e.setCancelled(true); + if (slot == 26) { + gui.openTaskCenter(p); + } else if (slot >= 10 && slot <= 18) { + QuestTracker qt = getQuestTracker(); + if (qt != null) { + List quests = + qt.getQuests("新人任务"); + int idx = slot - 10; + if (idx >= 0 && idx < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(idx); + String pn = p.getName(); + if (qt.hasClaimed(pn, qf)) { + p.sendMessage( + "§c该阶段奖励已领取"); + } else if (!qt.isStageCompleted( + pn, qf)) { + p.sendMessage("§c条件未满足"); + } else { + qt.claimRewards(p, qf); + qt.markClaimed(pn, qf); + gui.openGiftStages(p); + playSuccessSound(p); + } + } + } else { + int stage = slot - 9; + if (gift.canClaim(p, stage)) { + gift.claimReward(p, stage); + gui.openGiftStages(p); + } else { + p.sendMessage(config.msg( + "gift_not_ready")); + } + } + } + return; + } + + // ==================== 主线/支线任务 ==================== + if (title.equals("§d§l主线任务") + || title.equals("§d§l支线任务")) { + e.setCancelled(true); + if (slot == e.getInventory().getSize() - 1) { + gui.openTaskCenter(p); + } else if (questTracker != null) { + String cat = title.contains("主线") + ? "主线任务" : "支线任务"; + List quests = + questTracker.getQuests(cat); + if (slot < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(slot); + if (!questTracker.isStageCompleted( + p.getName(), qf)) { + p.sendMessage("§c条件未满足"); + } else if (questTracker.hasClaimed( + p.getName(), qf)) { + p.sendMessage("§c已领取过"); + } else { + questTracker.claimRewards(p, qf); + questTracker.markClaimed( + p.getName(), qf); + gui.openTaskList(p, cat); + playSuccessSound(p); + } + } + } + return; + } + + // ==================== 我的信息 ==================== + if (title.equals(GUIManager.T_MY_INFO)) { + e.setCancelled(true); + // ★ 只在图标槽(slot 4)调用,不干扰按钮 + if (slot == 4 + && gui.handleMyInfoIconClick(p, e)) { + return; + } + if (slot == 26) gui.openMain(p); + + else if (slot == 10) { + if (checkIn.isCheckedInToday( + p.getName())) + p.sendMessage( + config.msg("checkin_already")); + else { + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn( + p.getName()); + playSuccessSound(p); + gui.openMyInfo(p); + } + } else if (slot == 11) { + chatInput.getState(p).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p.closeInventory(); + p.sendMessage("§e请输入当前密码:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .BACK_CHECKIN; + p.closeInventory(); + p.sendMessage("§e请输入补签日期:"); + } else if (slot == 13) { + String code = (String) db.getField( + p.getName(), "invite_code"); + if (code == null || code.isEmpty()) + code = invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + } else if (slot == 14) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SET_EMAIL; + p.closeInventory(); + p.sendMessage("§e请输入邮箱:"); + } else if (slot == 15) { + p.openInventory( + points.createShopGUI(p)); + } + return; + } + + + // ==================== 管理面板 ==================== + if (title.equals(GUIManager.T_ADMIN)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SMTP_CONFIG; + p.closeInventory(); + p.sendMessage("§e当前SMTP: " + + config.getSmtp("smtp地址")); + p.sendMessage( + "§e输入新地址(0跳过):"); + } else if (slot == 11) + gui.openUserManagement(p); + else if (slot == 12) gui.openMyInfo(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) { + if (e.getClick().isLeftClick()) { + config.afkEnabled = + !config.afkEnabled; + p.sendMessage(config.msg( + config.afkEnabled + ? "afk_set_enabled" + : "afk_set_disabled")); + config.saveSettings(); + gui.openAdmin(p); + } else if (e.getClick() + .isRightClick()) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_AFK_TIME; + p.sendMessage( + "§e输入挂机时长(分钟):"); + } + } + return; + } + + // ==================== 任务中心 ==================== + if (title.equals(GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) { + if (questTracker != null) { + Location loc = p.getLocation(); + String result = + questTracker + .checkInAtPosition( + p.getName(), + p.getWorld() + .getName(), + loc.getX(), + loc.getY(), + loc.getZ()); + p.sendMessage(result); + playSuccessSound(p); + } + } else if (slot == 14) + gui.openTaskList(p, "主线任务"); + else if (slot == 16) + gui.openTaskList(p, "支线任务"); + else if (slot == 18) + ticket.openMain(p); + else if (slot == 20) { + dailySignWithReward(p); + gui.openMain(p); + } + return; + } + + // ==================== 邀请 ==================== + if (title.equals(GUIManager.T_INVITE)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + String code = + invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + gui.openInvite(p); + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .INVITE_INPUT_CODE; + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } + return; + } + + // ==================== 用户管理 ==================== + if (title.equals(GUIManager.T_USER_MGMT)) { + e.setCancelled(true); + if (slot == 53) gui.openAdmin(p); + else if (slot >= 0 && slot < 45) { + ItemStack item = + e.getInventory().getItem(slot); + if (item != null && item.hasItemMeta() + && item.getItemMeta() + .hasDisplayName()) + gui.openUserDetail(p, + item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", "")); + } + return; + } + + // ==================== 用户详情 ==================== + if (title.startsWith("§e§l管理: ")) { + e.setCancelled(true); + String tgt = title.substring( + "§e§l管理: ".length()); + if (slot == 22) gui.openUserManagement(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_POINTS; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage( + "§e输入" + tgt + "的新积分:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_PWD; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage( + "§e输入" + tgt + "的临时密码:"); + } else if (slot == 14) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) { + forceGiveMenuSnowball(tp); + p.sendMessage( + "§a已发放雪球菜单给 " + + tgt); + } else { + p.sendMessage("§c玩家不在线"); + } + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_DELETE_CONFIRM; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage(config.msg( + "admin_delete_confirm")); + } + return; + } + + // ==================== 积分商城 ==================== + if (title.startsWith("§d§l积分商城")) { + e.setCancelled(true); + if (slot == 49) gui.openMain(p); + else if (slot == 50 && isAdmin(p)) + gui.openShopAdmin(p); + else points.handleClick(p, slot); + return; + } + + // ==================== 商城管理 ==================== + if (title.equals("§c§l商城管理")) { + e.setCancelled(true); + if (slot == 49) + p.openInventory( + points.createShopGUI(p)); + return; + } + + // ==================== CY背包商城 ==================== + if (title.startsWith("§d§lCY背包商城")) { + e.setCancelled(true); + int size = e.getInventory().getSize(); + if (slot == size - 1) + p.openInventory( + points.createShopGUI(p)); + else points.handleCYClick(p, slot); + return; + } + + // ==================== 任务面板 ==================== + if (title.equals("§6§l任务面板")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 11) { + Location loc = p.getLocation(); + String result = + questTracker.checkInAtPosition( + p.getName(), + p.getWorld().getName(), + loc.getX(), loc.getY(), + loc.getZ()); + p.sendMessage(result); + playSuccessSound(p); + openTaskPanel(p); + } else if (slot == 15) { + gui.openGiftStages(p); + } + return; + } + + // ==================== 账号请求面板 ==================== + if (title.equals("§e§l账号请求")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + p.closeInventory(); + p.sendMessage("§c§l[确认] §f请输入:"); + p.sendMessage("§e/sdf1_login del " + + p.getName()); + } else if (slot == 13) { + ticket.openCreate(p); + } else if (slot == 16) { + p.closeInventory(); + loginMgr.handleReset(p); + } + } + } + + /* + * 签到 + 盲盒经济奖励 + */ + public void dailySignWithReward(Player p) { + String name = p.getName(); + + if (checkIn.isCheckedInToday(name)) { + p.sendMessage( + config.msg("checkin_already")); + return; + } + + // ===== 1. 执行签到(内部已加积分) ===== + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(name); + + // ===== 2. 获取连续天数 ===== + int streak = checkIn.getStreak(name); + double multiplier = 1.0; + if (streak >= 21) { + multiplier = 1.3; + } else if (streak >= 14) { + multiplier = 1.2; + } else if (streak >= 7) { + multiplier = 1.1; + } + + // ===== 3. 经济奖励(不再重复加积分) ===== + if (economy == null) { + playSuccessSound(p); + return; + } + + // 固定金额模式 + if ("fixed".equals( + config.checkinRewardType) + && config.checkinRewardFixed > 0) { + final double reward = + config.checkinRewardFixed; + final double m = multiplier; + p.sendMessage("§e§l[签到] §f" + + "正在生成今日奖励..."); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + double rewardFinal = + Math.round( + reward * m + * 100.0) + / 100.0; + double before = + economy + .getBalance(p); + economy.depositPlayer( + p, rewardFinal); + double after = + economy + .getBalance(p); + p.sendMessage( + "§6§l[签到] §f" + + "获得奖励: §a§l$" + + String.format( + "%.2f", + rewardFinal)); + p.sendMessage("§7余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", after)); + playSuccessSound(p); + }, 30L); + return; + } + + // 盲盒范围模式 + if ("range".equals( + config.checkinRewardType) + && config.checkinRewardMin > 0 + && config.checkinRewardMax + > config.checkinRewardMin) { + final double min = + config.checkinRewardMin; + final double max = + config.checkinRewardMax; + final double m = multiplier; + getLogger().info( + "[签到] 盲盒: min=" + + min + " max=" + max + + " multi=" + m); + p.sendMessage("§e§l[签到盲盒] " + + "§f正在生成今日惊喜..."); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + double rand = + Math.random(); + double reward = min + + rand + * (max - min); + reward = Math.round( + reward * m * 100.0) + / 100.0; + getLogger().info( + "[签到] 盲盒结果: rand=" + + String.format( + "%.4f", rand) + + " reward=" + + reward); + double before = + economy + .getBalance(p); + economy.depositPlayer( + p, reward); + double after = + economy + .getBalance(p); + p.sendMessage( + "§6§l====== 签到盲盒 ======"); + p.sendMessage( + "§7开出: §a§l$" + + String.format( + "%.2f", + reward)); + p.sendMessage("§7余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", after)); + p.sendMessage( + "§6§l========================"); + playSuccessSound(p); + }, 40L); + return; + } + + // 无经济奖励 + playSuccessSound(p); + } + + public BondManager getBondManager() { + return bondManager; + } + + + @EventHandler + public void onVillagerTrade( + org.bukkit.event.inventory.TradeSelectEvent e) { + // TradeSelectEvent 在玩家点击交易时触发 + if (!(e.getWhoClicked() instanceof Player)) return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p) || questTracker == null) return; + questTracker.onVillagerTrade(p.getName()); + } + + @EventHandler + public void onInvDrag(InventoryDragEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + String t = e.getView().getTitle(); +// 禁止T_MY_INFO拖拽,只允许点击操作 + // 我的信息:允许拖入图标槽(31) + if (GUIManager.T_MY_INFO.equals( + e.getView().getTitle())) { + boolean toIconSlot = false; + for (int raw : e.getRawSlots()) { + if (raw == 31) { + toIconSlot = true; + break; + } + } + if (toIconSlot) { + ItemStack dragged = + e.getOldCursor().clone(); + String b64 = gui.serializeItem(dragged); + if (b64 != null) { + db.saveMenuIcon(p.getName(), b64, + dragged.getType().name()); + } + p.sendMessage("§a已保存菜单图标: " + + dragged.getType().name()); + // 刷新GUI显示 + Bukkit.getScheduler() + .runTaskLater(this, () -> + gui.openMyInfo(p), 2L); + } + e.setCancelled(true); + return; + } + + + // 回收站拖拽 + if (t.equals("§6§l垃圾回收站")) { + boolean toGUI = false; + for (int rawSlot : e.getRawSlots()) { + if (rawSlot < 54) { + toGUI = true; + break; + } + } + if (toGUI) { + ItemStack cur = e.getCursor(); + if (cur != null + && cur.getType() != Material.AIR + && !isMenuSnowball(cur)) { + e.setCancelled(true); + p.getOpenInventory().setCursor(null); + garbage.saveItem(cur.clone()); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage( + p, + garbage + .getRecyclePage( + p + .getName())); + }, 2L); + return; + } + } + return; + } + + + + //服务商工单系统 + if (t.equals(TicketManager.T_TICKET_MAIN) + || t.equals( + TicketManager.T_TICKET_CREATE) + || t.equals( + TicketManager.T_TICKET_MY) + || t.equals( + TicketManager.T_TICKET_ADMIN) + || t.equals( + TicketManager.T_TICKET_DETAIL) + || t.equals("§d§l服务商面板") + || t.equals("§6§l抢单大厅") + || t.equals( + "§b§l我处理中的工单") + || t.equals("§6§l我的历史工单") + || t.equals("§c§l商城管理")) { + e.setCancelled(true); + } + } + /** 验证玩家是否在主库中真实存在 */ + public boolean isRealPlayer(String name) { + if (name == null || name.isEmpty()) return false; + try { + Map user = + getDb().getUser(name); + return user != null && !user.isEmpty() + && user.containsKey("player_name"); + } catch (Exception e) { + return false; + } + } + + // ===== Commands ===== + @Override + public boolean onCommand(CommandSender sender, + Command cmd, String label, + String[] args) { + + String cmdName = cmd.getName().toLowerCase(); + // 区域防护(必须在最前面) + if (cmd.getName().equalsIgnoreCase("protect") + || cmd.getName().equals("区域保护")) { + if (areaProtection == null) { + sender.sendMessage("§c区域防护未初始化"); + return true; + } + if (args.length == 0) { + sender.sendMessage("§a§l===== 区域防护 ====="); + sender.sendMessage("§e/protect 工具 §7获取选区工具"); + sender.sendMessage("§e/protect 创建 <区域名> §7创建区域"); + sender.sendMessage("§e/protect 列表 §7查看所有区域"); + sender.sendMessage("§e/protect 重载 §7重载配置"); + sender.sendMessage("§e/protect add <玩家> §7全局加白"); + sender.sendMessage("§e/protect add <区域> <玩家> §7区域加白"); + sender.sendMessage("§e/protect remove <玩家> §7全局删白"); + sender.sendMessage("§e/protect remove <区域> <玩家> §7区域删白"); + sender.sendMessage("§e/protect additem <物品ID> §7全局加黑"); + sender.sendMessage("§e/protect additem <区域> <物品ID> §7区域加黑"); + sender.sendMessage("§e/protect removeitem <物品ID> §7全局删黑"); + sender.sendMessage("§e/protect removeitem <区域> <物品ID> §7区域删黑"); + sender.sendMessage("§e/protect list §7全局白名单"); + sender.sendMessage("§e/protect list <区域> §7区域白名单"); + sender.sendMessage("§e/protect listitem §7全局物品黑名单"); + sender.sendMessage("§e/protect listitem <区域> §7区域物品黑名单"); + sender.sendMessage("§e/protect expand [格数] §7扩建选区"); + sender.sendMessage("§e/protect contraction [格数] §7收缩选区"); + sender.sendMessage("§e/protect addname <区域> <名字> §7添加和平白名单生物"); + sender.sendMessage("§e/protect removename <区域> <名字> §7移除和平白名单生物"); + sender.sendMessage("§e/protect addwhite <玩家> §7添加模式排除玩家"); + sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家"); + sender.sendMessage("§e/protect listname <区域> §7查看和平白名单"); + sender.sendMessage("§e/protect listwhite §7查看模式排除名单"); + + return true; + } + // 其他子命令交给 areaProtection + try { + return areaProtection.handleCommand(sender, args); + } catch (Exception e) { + sender.sendMessage("§c执行出错: " + e.getMessage()); + e.printStackTrace(); + return true; + } + } + + + if (cmd.getName().equalsIgnoreCase("shop") + || cmd.getName().equals("商店")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可使用"); + return true; + } + if (shopManager != null) { + shopManager.openShopMain((Player) sender); + } else { + sender.sendMessage("§c商店系统未加载"); + } + return true; + } + + String sub = (args.length > 0) ? args[0].toLowerCase() : ""; + +// ===== 在 onCommand 方法体内,最开头加: ===== + if (cmd.getName().equalsIgnoreCase("sdf1_login") + && args.length > 0 + && args[0].equalsIgnoreCase("shop")) { + if (shopManager == null) { + sender.sendMessage("§c商店未加载"); + return true; + } + return shopManager.handleCommand(sender, args); + } + // set + if (sub.equals("set")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage("§e用法: /sdf1_login set <玩家> <临时密码>"); + return true; + } + String tgt = args[1]; + String tempPwd = args[2]; + if (!db.userExists(tgt)) { + sender.sendMessage("§c玩家不存在"); + return true; + } + String salt = (String) db.getField(tgt, "password_salt"); + String hash = PasswordUtils.hash(tempPwd, salt); + db.setField(tgt, "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + 300000L); + db.setField(tgt, "temp_pw_used", 0); + + // ★ 立即发邮件(踢人之前) + String emailAddr = (String) db.getField(tgt, "email"); + if (emailAddr != null && !emailAddr.isEmpty()) { + // 获取最近登录信息 + Object lastLoginObj = db.getField(tgt, "last_login_time"); + long lastLogin = lastLoginObj != null + ? ((Number) lastLoginObj).longValue() : 0; + String lastIP = (String) db.getField(tgt, "last_ip"); + if (lastIP == null || lastIP.isEmpty()) + lastIP = "未知"; + + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String loginTimeStr = lastLogin > 0 + ? sdf.format(new java.util.Date(lastLogin)) + : "未知时间"; + + final String to = emailAddr; + final String fName = tgt; + final String pwd = tempPwd; + final String ipInfo = lastIP; + final String timeInfo = loginTimeStr; + final Main self = this; + + Bukkit.getScheduler().runTaskAsynchronously(this, + () -> { + // ★ 发送临时密码邮件(带登录信息) + boolean sent = email.sendTempPassword( + to, fName, pwd); + // ★ 额外发送安全通知邮件 + if (sent) { + String body = "玩家 " + fName + + ":\n\n" + + "管理员已为您的账号设置了临时密码。\n\n" + + "临时密码: " + pwd + "\n" + + "过期时间: 5分钟内\n\n" + + "最近登录记录:\n" + + " 时间: " + timeInfo + "\n" + + " IP地址: " + ipInfo + "\n\n" + + "如果您本人未操作,请忽略此邮件。\n" + + "如果您怀疑账号被盗,请立即修改密码。"; + email.sendBody(to, + "【安全通知】账号临时密码已设置", body); + } + Bukkit.getScheduler().runTask(self, () -> { + if (sent) { + sender.sendMessage("§a临时密码已发送至 " + + tgt + " 的邮箱"); + } + }); + }); + } else { + sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知"); + } + + // 踢人 + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c管理员已设置了临时密码,请用新密码登录"); + sender.sendMessage("§a已为 " + tgt + " 设置临时密码"); + return true; + } + + + if (label.equalsIgnoreCase("pvp")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + return pvpManager.onCommand(p, args); + } + + if (cmdName.equals("cypay")) { + if (args.length > 0) { + String target = args[0]; + if (!getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + } + return cypayCommand.onCommand( + sender, cmd, label, args); + } + + + + // ===== /printer [玩家名] ===== + if (label.equalsIgnoreCase("printer")) { + if (bondPrinter == null) { + sender.sendMessage("§cPrinter未初始化"); + return true; + } + if (!sender.isOp() + && !isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length == 0) { + // 打印全服 + sender.sendMessage( + "§e正在导出全服流水..."); + bondPrinter.printAll(sender); + return true; + } + if (args.length == 1) { + // 打印指定玩家 + String target = args[0]; + sender.sendMessage("§e正在导出 " + + target + " 的流水..."); + bondPrinter.printPlayer( + target, sender); + return true; + } + sender.sendMessage( + "§7用法: /printer [玩家名]"); + return true; + } + // ===== /chat 独立命令 ===== + if (cmdName.equals("chat") + || cmdName.equals("聊天")) { + if (sender instanceof Player) { + Player chatP = (Player) sender; + if (!chatP.getScoreboardTags() + .contains(config.adminTag)) { + sender.sendMessage("§c权限不足"); + return true; + } + } + if (args.length < 1) { + sender.sendMessage( + "§e/chat "); + return true; + } + String cs = args[0].toLowerCase(); + if (cs.equals("reload")) { + chatFilter.loadConfig(); + sender.sendMessage("§a聊天配置已重载"); + return true; + } + if (cs.equals("开") || cs.equals("on")) { + chatFilter.setEnabled(true); + chatFilter.saveConfig(); + sender.sendMessage("§aURL过滤已开启"); + return true; + } + if (cs.equals("关") || cs.equals("off")) { + chatFilter.setEnabled(false); + chatFilter.saveConfig(); + sender.sendMessage("§cURL过滤已关闭"); + return true; + } + if (cs.equals("add")) { + if (args.length < 2) { + sender.sendMessage("§e/chat add "); + return true; + } + chatFilter.addUrl(args[1]); + sender.sendMessage("§a已添加白名单: " + args[1]); + return true; + } + if (cs.equals("remove")) { + if (args.length < 2) { + sender.sendMessage("§e/chat remove "); + return true; + } + if (chatFilter.removeUrl(args[1])) + sender.sendMessage("§c已移除: " + args[1]); + else + sender.sendMessage("§c未找到: " + args[1]); + return true; + } + if (cs.equals("addplayer")) { + if (args.length < 2) { + sender.sendMessage("§e/chat addplayer <玩家>"); + return true; + } + chatFilter.addWhitelistPlayer(args[1]); + sender.sendMessage("§a白名单玩家: " + args[1]); + return true; + } + if (cs.equals("takeplayer")) { + if (args.length < 2) { + sender.sendMessage("§e/chat takeplayer <玩家>"); + return true; + } + if (chatFilter.removeWhitelistPlayer(args[1])) + sender.sendMessage("§c已移除白名单: " + args[1]); + else + sender.sendMessage("§c未找到: " + args[1]); + return true; + } + if (cs.equals("unmute")) { + if (args.length < 2) { + sender.sendMessage("§e/chat unmute <玩家>"); + return true; + } + chatFilter.unmutePlayer(args[1]); + sender.sendMessage("§a已解禁: " + args[1]); + return true; + } + if (cs.equals("reset")) { + if (args.length < 2) { + sender.sendMessage("§e/chat reset <玩家>"); + return true; + } + chatFilter.resetPlayer(args[1]); + sender.sendMessage("§a已重置: " + args[1]); + return true; + } + sender.sendMessage("§c未知子命令"); + return true; + } + if (label.equalsIgnoreCase("sdf1_login") + && args.length >= 2 + && args[0].equalsIgnoreCase("give")) { + if (!sender.isOp()) { + sender.sendMessage("§c仅OP可用"); + return true; + } + Player target = + Bukkit.getPlayer(args[1]); + if (target == null || !target.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + args[1]); + return true; + } + giveMenuSnowball(target); + sender.sendMessage("§a已发放菜单雪球给 " + + target.getName()); + target.sendMessage( + "§a管理员已发放菜单雪球给你"); + return true; + } + + if (cmdName.equals("recycle")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player pr = (Player) sender; + if (!pr.isOp() + && !pr.getScoreboardTags() + .contains(config.adminTag)) { + pr.sendMessage("§c权限不足"); + return true; + } + garbage.openRecycle(pr); + return true; + } + + + // /垃圾清理 + if (cmdName.equals("垃圾清理")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + int count = garbage.collectItems(); + sender.sendMessage("§a已清理 " + + count + " 个掉落物"); + return true; + } + // /menu 开关雪球菜单 + if (cmdName.equals("menu")) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length == 0) { + p2.sendMessage( + "§e用法: /menu <开/关>"); + return true; + } + String arg = args[0]; + if (arg.equals("开")) { + db.setField(p2.getName(), + "menu_snowball", 1); + p2.sendMessage( + "§a雪球菜单已开启," + + "下次进入将发放"); + } else if (arg.equals("关")) { + db.setField(p2.getName(), + "menu_snowball", 0); + p2.sendMessage( + "§c雪球菜单已关闭," + + "下次进入不再发放"); + } else { + p2.sendMessage( + "§e用法: /menu <开/关>"); + } + return true; + } + + if (cmdName.equals("reg") + || cmdName.equals("注册")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (db.userExists(p.getName())) { + p.sendMessage( + "§c您已注册过,请使用 /login"); + return true; + } + try { + loginMgr.handleRegister(p, args); + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onRegister(p); + } + + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] reg命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c注册出错,请联系管理员"); + } + return true; + } + + + if (cmdName.equals("login") + || cmdName.equals("l") + || cmdName.equals("登录")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (loggedIn.contains(p.getName())) { + p.sendMessage("§c您已登录"); + return true; + } + try { + loginMgr.handleLogin(p, args); + // [FIX] 手动登录后关闭飞行(仅一次) + if (loggedIn.contains(p.getName()) + && p.getAllowFlight()) { + p.setAllowFlight(false); + p.setFlying(false); + } + + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onLogin(p); + } + + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] login命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c登录出错,请联系管理员"); + } + return true; + } + + // /找回密码 + if (cmdName.equals("找回密码")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + handleReset((Player) sender); + return true; + } + + // /签到 + if (cmdName.equals("签到")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // 绑定邮箱 + if (cmdName.equals("绑定邮箱")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 1) { + String emailAddr = args[0]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // /oa + if (cmdName.equals("oa")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + openAccountRequestPanel( + (Player) sender); + return true; + } + + // /玩家信息 + if (cmdName.equals("玩家信息")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + // ===== /sdf1_login ===== + if (!cmdName.equals("sdf1_login")) + return false; + + if (args.length == 0) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§e用法: /sdf1_login <子命令>"); + return true; + } + gui.openMain((Player) sender); + return true; + } + +// ===== del / confirm / cancel(最先处理) ===== + if (sub.equals("del")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login del <玩家>"); + return true; + } + String target = args[1]; + if (!db.userExists(target)) { + sender.sendMessage( + "§c玩家 " + target + " 不存在"); + return true; + } + if (pendingDeleteTask != null) { + pendingDeleteTask.cancel(); + String old = pendingDeleteName; + pendingDeleteTask = null; + pendingDeleteName = null; + getLogger().info( + "[Sdf1_login] 取消旧待删除: " + old); + } + pendingDeleteName = target; + pendingDeleteTask = new BukkitRunnable() { + @Override + public void run() { + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + if (name == null) return; + db.deleteUser(name); + getLogger().info( + "[Sdf1_login] 已删除: " + name); + Player tp = Bukkit.getPlayer(name); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c账号已被管理员删除"); + } + }; + pendingDeleteTask.runTaskLater(this, 1200L); + sender.sendMessage("§e§l[删除确认] 目标: §c" + + target); + sender.sendMessage("§760秒后执行," + + "§a/sdf1_login confirm §7确认," + + "§c/sdf1_login cancel §7取消"); + getLogger().warning("[Sdf1_login] 待删除: " + + target + " by " + sender.getName()); + return true; + } + + if (sub.equals("confirm")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (pendingDeleteName == null + || pendingDeleteTask == null) { + sender.sendMessage( + "§c当前没有待删除的账号"); + return true; + } + pendingDeleteTask.cancel(); + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + db.deleteUser(name); + sender.sendMessage("§a已立即删除: " + name); + Player tp = Bukkit.getPlayer(name); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c账号已被管理员删除"); + return true; + } + + if (sub.equals("cancel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (pendingDeleteName == null + || pendingDeleteTask == null) { + sender.sendMessage( + "§c当前没有待删除的账号"); + return true; + } + pendingDeleteTask.cancel(); + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + sender.sendMessage("§a已取消删除: " + name); + return true; + } + +// ===== stop ===== + if (sub.equals("stop")) { + if (!(sender instanceof Player)) { + if (args.length >= 2) { + db.clearTempPassword(args[1]); + sender.sendMessage("§a已报废 " + + args[1] + " 的临时密码"); + } else { + db.clearAllTempPasswords(); + sender.sendMessage("§a已报废所有临时密码"); + } + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String tgt = args[1]; + String myIP = getPlayerIP(p2); + String targetIP = + db.getPlayerIPByName(tgt); + if (targetIP == null) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + targetIP = getPlayerIP(tp); + } + if (targetIP == null + || !targetIP.equals(myIP)) { + p2.sendMessage("§c只能操作同IP下的玩家"); + return true; + } + db.clearTempPassword(tgt); + p2.sendMessage("§a已报废 " + tgt + " 的临时密码"); + } else { + db.clearTempPassword(p2.getName()); + p2.sendMessage("§a已报废您的临时密码"); + } + return true; + } + +// ===== reload ===== + if (sub.equals("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + chatFilter.loadConfig(); + if (radio != null) radio.reload(); + if (getMenu() != null) { + getMenu().loadMenu(); + } + if (shopManager != null) { + shopManager.loadCategories(); + } + if (shopManager != null) shopManager.loadCategories(); + if (areaProtection != null) { + areaProtection.clearAllPlayerAreas(); + } + if (questTracker != null) { + questTracker.reload(); + } + sender.sendMessage("§a§l配置已重载!"); + return true; + } + +// ===== back ===== + if (sub.equals("back")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + // ... 保持原有back代码不变 ... + if (args.length < 2) { + sender.sendMessage("§e用法: /sdf1_login back <玩家> [#编号]"); + return true; + } + // 原有back逻辑 + String backTarget = args[1]; + if (args.length >= 3 + && args[2].startsWith("#")) { + int backupId; + try { + backupId = Integer.parseInt( + args[2].replace("#", "")); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效编号"); + return true; + } + String[] backup = + db.getInventoryBackupById(backupId); + if (backup == null) { + sender.sendMessage("§c备份 #" + backupId + " 不存在"); + return true; + } + Player tp = Bukkit.getPlayer(backTarget); + if (tp != null && tp.isOnline()) { + try { + tp.getInventory().setContents( + InventorySerializer.fromBase64(backup[0])); + if (backup[1] != null && !backup[1].isEmpty()) + tp.getInventory().setArmorContents( + InventorySerializer.fromBase64(backup[1])); + if (backup[2] != null && !backup[2].isEmpty()) + tp.getInventory().setExtraContents( + InventorySerializer.fromBase64(backup[2])); + int lv = Integer.parseInt(backup[3]); + if (lv > 0) tp.setLevel(lv); + tp.sendMessage("§a管理员已恢复您的背包"); + sender.sendMessage("§a已恢复 " + backTarget + " (备份#" + backupId + ")"); + } catch (Exception e) { + sender.sendMessage("§c恢复失败: " + e.getMessage()); + } + } else { + db.saveInventoryBackup(backTarget, + backup[0], backup[1], backup[2], + Integer.parseInt(backup[3]), + Double.parseDouble(backup[4])); + sender.sendMessage("§a" + backTarget + " 离线,备份已写入"); + } + } else { + List> backups = + db.getInventoryBackups(backTarget, 5); + if (backups.isEmpty()) { + sender.sendMessage("§c" + backTarget + " 没有背包备份"); + return true; + } + sender.sendMessage("§e§l=== " + backTarget + " 背包备份 ==="); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat("MM-dd HH:mm:ss"); + for (Map b : backups) { + int bid = ((Number) b.get("id")).intValue(); + Object stObj = b.get("save_time"); + long st = stObj != null + ? ((Number) stObj).longValue() : 0; + String time = st > 0 + ? sdf.format(new java.util.Date(st)) + : "未知时间"; + String itemNames = getBackupItemPreview(bid); + sender.sendMessage("§7#" + bid + " §f" + time + " §7: " + itemNames); + } + sender.sendMessage("§7使用 §e/sdf1_login back " + backTarget + " #编号 §7恢复"); + } + return true; + } + +// ===== pw / password ===== + if (sub.equals("pw") || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate(newPwd)) { + p2.sendMessage(config.msg("password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), "password_salt"); + String hash = PasswordUtils.hash(oldPwd, salt); + boolean mainOk = db.checkPassword( + p2.getName(), hash); + boolean tempOk = db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg("password_wrong")); + return true; + } + String oldHash = (String) db.getField( + p2.getName(), "password_hash"); + String newSalt = PasswordUtils.generateSalt(); + String newHash = PasswordUtils.hash(newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg("password_same")); + return true; + } + recordPasswordChange(p2.getName(), oldHash, salt); + db.setField(p2.getName(), "password_hash", newHash); + db.setField(p2.getName(), "password_salt", newSalt); + if (!mainOk && tempOk) + db.clearTempPassword(p2.getName()); + // pw 命令改密码成功后 + needsPasswordChange.remove(p2.getName()); +// 清除临时密码 + if (!mainOk && tempOk) + db.clearTempPassword(p2.getName()); + + return true; + } + + if (args.length == 2) { + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), "password_salt"); + String hash = PasswordUtils.hash(oldPwd, salt); + boolean mainOk = db.checkPassword( + p2.getName(), hash); + boolean tempOk = db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg("password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType.CHANGE_PWD_STEP2; + chatInput.getState(p2).ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage("§e请输入新密码:"); + return true; + } + + chatInput.getState(p2).type = + ChatInputManager.InputType.CHANGE_PWD_STEP1; + p2.sendMessage("§e请输入当前密码:"); + return true; + } + +// ===== email ===== + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL_VERIFY; + chatInput.getState(p2).targetPlayer = emailAddr; + chatInput.getState(p2).ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + oldEmail); + p2.sendMessage("§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL_VERIFY; + chatInput.getState(p2).targetPlayer = emailAddr; + chatInput.getState(p2).ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = + email.sendVerifyCode(to, c); + Bukkit.getScheduler() + .runTask(this, () -> { + if (sent) { + fp.sendMessage("§a验证码已发送到 " + to); + fp.sendMessage("§e输入6位验证码(0取消):"); + } else { + fp.sendMessage("§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + +// ===== sign ===== + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + dailySignWithReward((Player) sender); + return true; + } + +// ===== reset ===== + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset((Player) sender); + return true; + } + +// ===== undo(密码撤销) ===== + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), "password_hash", rb.hash); + db.setField(p2.getName(), "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // radio子命令(控制台+玩家) + // sdf1_login radio → 停止全服广播 + // sdf1_login radio reload → 重载 + // sdf1_login radio <歌名> → 全服播放 + // sdf1_login radio <歌名> <玩家名> → 指定玩家播放 + // sdf1_login radio <歌名> login → 推送给登录中玩家 + if (sub.equals("radio")) { + if (args.length < 2) { + // 无参数 = 停止 + radio.stopMainRadio(); + sender.sendMessage("§c全服广播已停止"); + return true; + } + String arg1 = args[1]; + + // reload + if (arg1.equalsIgnoreCase("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + radio.reload(); + sender.sendMessage("§aRadio已重载"); + return true; + } + + // radio <歌名> → 全服播放 + // radio <歌名> <玩家名> → 指定玩家 + // radio <歌名> login → 登录阶段播放 + String songName = arg1; + + if (args.length >= 3) { + String target = args[2]; + + if (target.equalsIgnoreCase("login")) { + if (radio.hasSong(songName)) { + radio.playToLoginPlayers(songName); + sender.sendMessage( + "§a已推送 §e" + songName + + " §a给登录中玩家"); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + return true; + } + } else { + Player targetP = + Bukkit.getPlayer(target); + if (targetP != null + && targetP.isOnline()) { + if (radio.hasSong(songName)) { + radio.playToPlayer( + targetP, songName); + sender.sendMessage( + "§a已向 §e" + target + + " §a播放 §e" + + songName); + targetP.sendMessage( + "§6§l[广播] §f正在播放: §e" + + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + + songName); + } + } else { + sender.sendMessage( + "§c玩家不在线: " + + target); + } + return true; + } + } else { + if (radio.hasSong(songName)) { + radio.playToAll(songName); + sender.sendMessage( + "§a全服播放: §e" + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + } + return true; + } + + + // pw / password + if (sub.equals("pw") + || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + // /sdf1_login pw 旧密码 新密码 + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate( + newPwd)) { + p2.sendMessage(config.msg( + "password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + String oldHash = + (String) db.getField( + p2.getName(), + "password_hash"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg( + "password_same")); + return true; + } + recordPasswordChange( + p2.getName(), + oldHash, salt); + db.setField(p2.getName(), + "password_hash", newHash); + db.setField(p2.getName(), + "password_salt", newSalt); + if (!mainOk && tempOk) { + db.clearTempPassword( + p2.getName()); + } + needsPasswordChange.remove( + p2.getName()); + p2.sendMessage(config.msg( + "password_changed")); + return true; + } + + if (args.length == 2) { + // /sdf1_login pw 旧密码 + // 验证旧密码,然后聊天输入新密码 + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP2; + chatInput.getState(p2) + .ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage( + "§e请输入新密码:"); + return true; + } + + // /sdf1_login pw (无参数) + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p2.sendMessage( + "§e请输入当前密码:"); + return true; + } + + // email + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + // 已绑定:先验证密码 + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + // 未绑定:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // sign + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // reset + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset( + (Player) sender); + return true; + } + + // undo + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = + pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time + > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), + "password_hash", rb.hash); + db.setField(p2.getName(), + "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // take + if (sub.equals("take")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login take <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + if (!db.deductPoints(args[1], amount)) { + sender.sendMessage("§c积分不足"); + return true; + } + sender.sendMessage("§a已扣除 " + args[1] + + " " + amount + " 积分"); + return true; + } + + + // give - 手动发放雪球菜单 + if (sub.equals("give")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login give <玩家名>"); + return true; + } + Player tp = Bukkit.getPlayer( + args[1]); + if (tp == null || !tp.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + + args[1]); + return true; + } + forceGiveMenuSnowball(tp); + tp.sendMessage( + "§a管理员已为您发放菜单雪球"); + sender.sendMessage( + "§a已发放雪球菜单给 " + + args[1]); + return true; + } + + // shopadd + if (sub.equals("shopadd")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 4) { + sender.sendMessage( + "§e用法: /sdf1_login shopadd <名称> <价格> <命令>"); + return true; + } + String shopName = args[1]; + int price; + try { + price = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + String shopCmd = args[3]; + points.addItem(shopName, price, shopCmd); + sender.sendMessage("§a已添加商品: " + + shopName + " (" + price + + "积分)"); + return true; + } + + // shopdel + if (sub.equals("shopdel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login shopdel <序号>"); + return true; + } + try { + int idx = Integer.parseInt(args[1]); + points.removeItem(idx); + sender.sendMessage("§a已删除商品 #" + idx); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + } + return true; + } + + // get + if (sub.equals("get")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login get <玩家>"); + return true; + } + Map user = + db.getUser(args[1]); + if (user.isEmpty()) { + sender.sendMessage("§c玩家不存在"); + return true; + } + sender.sendMessage( + "§e=== " + args[1] + " ==="); + sender.sendMessage("§7积分: §e" + + user.getOrDefault("points", 0)); + sender.sendMessage("§7邮箱: §e" + + user.getOrDefault("email", "无")); + sender.sendMessage("§7签到天数: §e" + + user.getOrDefault( + "total_checkin_days", 0)); + sender.sendMessage("§7邀请码: §e" + + user.getOrDefault( + "invite_code", "无")); + return true; + } + + + // kick (挂机) + if (sub.equals("kick")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login kick <开关> [分钟]"); + return true; + } + String mode = args[1].toLowerCase(); + if (mode.equals("on") + || mode.equals("开") + || mode.equals("启用")) { + config.afkEnabled = true; + if (args.length >= 3) { + try { + config.afkTimeout = + Integer.parseInt( + args[2]) * 60; + } catch (NumberFormatException ignored) { + } + } + config.saveSettings(); + int minutes = + config.afkTimeout / 60; + sender.sendMessage( + "§a挂机踢出已开启,超时: " + + minutes + " 分钟"); + } else if (mode.equals("off") + || mode.equals("关") + || mode.equals("停用")) { + config.afkEnabled = false; + config.saveSettings(); + sender.sendMessage( + "§c挂机踢出已关闭"); + } else { + sender.sendMessage( + "§c无效参数 on/off"); + } + return true; + } + + // add + if (sub.equals("add")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login add <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + db.addPoints(args[1], amount); + sender.sendMessage("§a已为 " + args[1] + + " 增加 " + amount + " 积分"); + return true; + } + + // ticket + if (sub.equals("ticket") + || sub.equals("工单")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + ticket.openMain((Player) sender); + return true; + } + + sender.sendMessage("§c未知参数"); + return true; + } + + return true; + } + /** + * 供SDF1反射调用的CDK兑换 + * 返回: "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeemBondForExternal( + String playerName, String code) { + if (cdkManager == null) + return "fail:CDK未初始化"; + return cdkManager.redeem(code, playerName); + } + // 旧的(可能缺失或拼写错误) + public SalesStatsManager getSalesStats() { + return salesStats; + } + + /** + * 是否是垃圾站展示物品(带ID lore) + */ + private boolean isTrashDisplayItem( + ItemStack item) { + if (item == null + || item.getType() + == Material.AIR) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null || !im.hasLore()) + return false; + List lore = im.getLore(); + if (lore == null || lore.isEmpty()) + return false; + return lore.get(0) + .startsWith("§7ID: #"); + } + + private void sendChatHelp(Player p) { + p.sendMessage( + "§e§l======== 聊天过滤帮助 =========="); + p.sendMessage("§e/chat reload §7- 重载配置"); + p.sendMessage("§e/chat 开/关 §7- 开启/关闭聊天过滤"); + p.sendMessage("§e/chat add §7- 添加白名单URL"); + p.sendMessage("§e/chat remove §7- 移除白名单URL"); + p.sendMessage("§e/chat addplayer <玩家> §7- 添加白名单玩家"); + p.sendMessage("§e/chat takeplayer <玩家> §7- 移除白名单玩家"); + p.sendMessage("§e/chat unmute <玩家> §7- 解禁玩家"); + p.sendMessage("§e/chat reset <玩家> §7- 重置违规记录"); + p.sendMessage( + "§e§l=================================="); + } + + private int findEmptySlot(Inventory inv) { + for (int i = 0; i < 49; i++) { + ItemStack item = inv.getItem(i); + if (item == null + || item.getType() + == Material.AIR) + return i; + } + return -1; + } + + // ===== TabComplete ===== + @Override + public List onTabComplete( + CommandSender sender, Command cmd, + String label, String[] args) { + List list = new ArrayList<>(); + if (cmd.getName().equalsIgnoreCase( + "sdf1_login") + && args.length == 1) { + if (isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "pw", "email", + "sign", "reset", "undo", + "get", "del", "set", + "take", "add", "kick", + "ticket", "oa", "stop", + "shopadd", "shopdel", "back", "radio", "back", "shop")); + } else { + list.addAll(Arrays.asList( + "pw", "email", "sign", + "reset", "undo", + "ticket", "oa", "stop")); + } + } + if (cmd.getName().equalsIgnoreCase("protect") + || cmd.getName().equals("区域保护")) { + + String sub = args.length > 0 ? args[0].toLowerCase() : ""; + + // ===== 第一层:子命令 ===== + if (args.length == 1) { + List opts = Arrays.asList( + "工具", "创建", "列表", "重载", "删除", + "add", "remove", + "additem", "removeitem", + "on", "off", "tempon", + "expand", "contraction", + "list", "listitem", + "addname", "removename", "listname", + "addwhite", "removewhite", "listwhite"); + return filterTab(opts, args[0]); + } + + // ===== 第二层 ===== + if (args.length == 2) { + // add/remove: 区域名 + global + 在线玩家 + if (sub.equals("add") || sub.equals("remove")) { + List all = new ArrayList<>(); + all.add("global"); + all.addAll(areaProtection.getAreaNames()); + for (Player pl : Bukkit.getOnlinePlayers()) + all.add(pl.getName()); + return filterTab(all, args[1]); + } + // additem/removeitem: 区域名 + global + if (sub.equals("additem") || sub.equals("removeitem")) { + List all = new ArrayList<>(); + all.add("global"); + all.addAll(areaProtection.getAreaNames()); + return filterTab(all, args[1]); + } + // list/listitem: 区域名 + if (sub.equals("list") || sub.equals("listitem")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // expand/contraction: 数字 + if (sub.equals("expand") || sub.equals("contraction")) { + return Arrays.asList("1", "3", "5", "10", "20", "50"); + } + // 创建/删除: 区域名 + if (sub.equals("创建") || sub.equals("删除")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // addname/removename/listname: 区域名 + if (sub.equals("addname") || sub.equals("removename") + || sub.equals("listname")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // addwhite/removewhite/listwhite: 区域名 + if (sub.equals("addwhite") || sub.equals("removewhite") + || sub.equals("listwhite")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + } + + // ===== 第三层 ===== + if (args.length == 3) { + // add/remove: 第三层是玩家名 + if (sub.equals("add") || sub.equals("remove")) { + List players = new ArrayList<>(); + for (Player pl : Bukkit.getOnlinePlayers()) + players.add(pl.getName()); + return filterTab(players, args[2]); + } + // additem/removeitem: 第三层是物品ID + if (sub.equals("additem") || sub.equals("removeitem")) { + List items = Arrays.asList( + "TNT", "BROWN_MUSHROOM", "RED_MUSHROOM", + "MUSHROOM_STEW", "POISONOUS_POTATO", + "SPIDER_EYE", "POTION", "SPLASH_POTION", + "OMINOUS_BOTTLE", "ENDER_PEARL", + "BOW", "CROSSBOW", "ARROW", + "FLINT_AND_STEEL"); + return filterTab(items, args[2]); + } + // addname/removename: 第三层是生物名字(自由输入) + if (sub.equals("addname") || sub.equals("removename")) { + return null; + } + // addwhite/removewhite: 第三层是玩家名 + if (sub.equals("addwhite") || sub.equals("removewhite")) { + List players = new ArrayList<>(); + for (Player pl : Bukkit.getOnlinePlayers()) + players.add(pl.getName()); + return filterTab(players, args[2]); + } + } + + return null; + } + + + if (cmd.getName().equalsIgnoreCase("chat") + && args.length == 1 + && isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "add", "remove", + "addplayer", "takeplayer", + "unmute", "reset")); + } + if (cmd.getName().equalsIgnoreCase("cypay")) { + return cypayCommand.onTabComplete(sender, cmd, label, args); + } + + return list; + } +} \ No newline at end of file diff --git a/backup/20260605_185500/QuestStorage.java.backup b/backup/20260605_185500/QuestStorage.java.backup new file mode 100644 index 0000000..45dbabf --- /dev/null +++ b/backup/20260605_185500/QuestStorage.java.backup @@ -0,0 +1,138 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class QuestStorage { + + private Connection conn; + + public QuestStorage(File dataFolder) { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + dataFolder, + "quest_data.db"); + conn = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getPath()); + initTable(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void initTable() + throws SQLException { + Statement stmt = + conn.createStatement(); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY " + + "(player, condition_key))"); + stmt.close(); + } + + /** + * 标记条件已完成(value = "1") + */ + public void setCondition( + String player, + String condition) { + setConditionRaw( + player, condition, "1"); + } + + /** + * 写入任意值(计数器等) + */ + public void setConditionRaw( + String player, + String condition, + String value) { + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO " + + "quest_conditions " + + "(player, " + + "condition_key, " + + "value, timestamp) " + + "VALUES " + + "(?, ?, ?, ?)"); + ps.setString(1, player); + ps.setString(2, condition); + ps.setString(3, value); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 查询条件值 + */ + public Object getCondition( + String player, + String condition) { + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value FROM " + + "quest_conditions " + + "WHERE player = ? " + + "AND " + + "condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + String val = + rs.getString("value"); + rs.close(); + ps.close(); + // 尝试解析为数字 + try { + if (val.contains(".")) { + return Double.parseDouble( + val); + } + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backup/20260605_185500/QuestTracker.java.backup b/backup/20260605_185500/QuestTracker.java.backup new file mode 100644 index 0000000..0089ee2 --- /dev/null +++ b/backup/20260605_185500/QuestTracker.java.backup @@ -0,0 +1,3692 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final QuestStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + private static boolean loggedStructureRegistry + = false; + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new QuestStorage( + plugin.getDataFolder()); + + loadAllQuests(); + } + + // ========== 重载任务配置 ========== + public void reload() { + questCache.clear(); + loadAllQuests(); + plugin.getLogger().info( + "[QuestTracker] 任务配置已重载"); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 岩岸/沙滩 + BIOME_CATEGORY.put("minecraft:stony_shore", + "石岸"); +// 海岸 + BIOME_CATEGORY.put("minecraft:mushroom_fields", + "蘑菇岛"); +// 苍白之园 + BIOME_CATEGORY.put( + "minecraft:pale_garden", "森林"); +// 末地小型岛屿 + BIOME_CATEGORY.put( + "minecraft:small_end_islands", "末地"); +// 主世界结构归类 + BIOME_CATEGORY.put("minecraft:village", "平原"); + BIOME_CATEGORY.put( + "minecraft:desert_pyramid", "沙漠"); + BIOME_CATEGORY.put( + "minecraft:jungle_pyramid", "森林"); + BIOME_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + + + } + + // 所有群系大类列表 + private static final String[] + ALL_CATEGORIES = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "石岸", "蘑菇岛", "地下", + "下界", "末地" + }; + + // ===== 结构映射(PaperMC 1.21.4 实际结构 key)===== + private static final Map + STRUCTURE_CATEGORY = + new HashMap<>(); + static { + // 主世界结构 + STRUCTURE_CATEGORY.put( + "minecraft:village_plains", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_desert", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_savanna", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_snowy", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_taiga", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:desert_pyramid", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:jungle_pyramid", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + STRUCTURE_CATEGORY.put( + "minecraft:igloo", "雪原"); + STRUCTURE_CATEGORY.put( + "minecraft:mansion", "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:pillager_outpost", + "掠夺者前哨站"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft_mesa", + "地下"); + // 要塞可生成在任何主世界群系下,映射为"主世界" + // 结构检测返回"主世界"时跳过Y轴"地下"判定 + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", + "主世界"); + // 远古城市是结构,应分类为"远古城市"而非"地下" + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "远古城市"); + STRUCTURE_CATEGORY.put( + "minecraft:trail_ruins", + "远古遗迹"); + STRUCTURE_CATEGORY.put( + "minecraft:trial_chambers", + "试炼密室"); + STRUCTURE_CATEGORY.put( + "minecraft:monument", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_cold", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_warm", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck_beached", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:buried_treasure", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_desert", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_jungle", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_mountain", + "山地"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_ocean", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_swamp", + "沼泽"); + // 下界结构 + STRUCTURE_CATEGORY.put( + "minecraft:fortress", "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:bastion_remnant", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:nether_fossil", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_nether", + "下界"); + // 末地结构 + STRUCTURE_CATEGORY.put( + "minecraft:end_city", "末地"); + } + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + CATEGORY_KEYWORDS.put("石岸", "石岸"); + CATEGORY_KEYWORDS.put("沙滩", "石岸"); + CATEGORY_KEYWORDS.put("海岸", "石岸"); + + // ===== 结构名称关键词映射 ===== + // 主世界结构 + CATEGORY_KEYWORDS.put("村庄", "村庄"); + CATEGORY_KEYWORDS.put("沙漠神殿", "沙漠"); + CATEGORY_KEYWORDS.put("丛林神庙", "森林"); + CATEGORY_KEYWORDS.put("沼泽小屋", "沼泽"); + CATEGORY_KEYWORDS.put("雪屋", "雪原"); + CATEGORY_KEYWORDS.put("林地府邸", "森林"); + CATEGORY_KEYWORDS.put("掠夺者前哨站", "平原"); + CATEGORY_KEYWORDS.put("废弃矿井", "地下"); + CATEGORY_KEYWORDS.put("要塞", "主世界"); + CATEGORY_KEYWORDS.put("远古城市", "远古城市"); + CATEGORY_KEYWORDS.put("古迹废墟", "地下"); + CATEGORY_KEYWORDS.put("试炼密室", "地下"); + CATEGORY_KEYWORDS.put("海底废墟", "海洋"); + CATEGORY_KEYWORDS.put("沉船", "海洋"); + CATEGORY_KEYWORDS.put("埋藏的宝箱", "海洋"); + CATEGORY_KEYWORDS.put("废弃传送门", "主世界"); + CATEGORY_KEYWORDS.put("化石", "主世界"); + + // 下界结构 + CATEGORY_KEYWORDS.put("下界要塞", "下界"); + CATEGORY_KEYWORDS.put("猪灵堡垒", "下界"); + CATEGORY_KEYWORDS.put("玄武岩结构", "下界"); + + // 末地结构 + CATEGORY_KEYWORDS.put("末地城", "末地"); + CATEGORY_KEYWORDS.put("末地船", "末地"); + + // 下界相关别名 + CATEGORY_KEYWORDS.put("地狱要塞", "下界"); + CATEGORY_KEYWORDS.put("下界堡垒", "下界"); + + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 优先用世界名判断维度 + if (worldName != null) { + String w = + worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + + // 用群系映射表 + if (rawKey != null) { + String lower = + rawKey.toLowerCase(); + // 带 minecraft: 前缀的直接查 + String cat = + BIOME_CATEGORY.get(lower); + if (cat != null) + return cat; + + // 不带前缀的尝试补全 + String prefixed = + "minecraft:" + lower; + cat = BIOME_CATEGORY.get(prefixed); + if (cat != null) + return cat; + + // 模糊匹配群系名 + for (Map.Entry entry + : BIOME_CATEGORY.entrySet()) { + if (entry.getKey() + .contains(lower) + || lower.contains( + entry.getKey() + .replace( + "minecraft:", + ""))) { + return entry.getValue(); + } + } + } + + return "主世界"; + } + + // ========== 结构检测 ========== + + /** + * 检测指定位置是否存在自然生成的结构 + * 返回结构分类(如"村庄"、"沙漠"等),未找到返回null + * 使用 PaperMC World.hasStructureAt() + Registry.STRUCTURE API + */ + private String detectStructureAt( + String worldName, double x, + double y, double z) { + try { + World w = Bukkit.getWorld(worldName); + if (w == null) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测: 世界 '" + + worldName + "' 不存在"); + return null; + } + + int bx = (int) Math.floor(x); + int by = (int) Math.floor(y); + int bz = (int) Math.floor(z); + + // 首次调用时打印所有注册表结构键 + if (!loggedStructureRegistry) { + plugin.getLogger().info( + "[QuestTracker] === 结构注册表诊断 ==="); + int count = 0; + for (Structure s : Registry.STRUCTURE) { + NamespacedKey key = + Registry.STRUCTURE.getKey(s); + plugin.getLogger().info( + "[QuestTracker] 注册表结构 #" + + count + ": " + key); + count++; + } + plugin.getLogger().info( + "[QuestTracker] 注册表共 " + + count + " 个结构"); + plugin.getLogger().info( + "[QuestTracker] === 诊断结束 ==="); + loggedStructureRegistry = true; + } + + // 第1步: 精确位置检查 + io.papermc.paper.math.Position exactPos = + io.papermc.paper.math.Position.block( + bx, by, bz); + for (Structure structure : Registry.STRUCTURE) { + boolean hasAt = + w.hasStructureAt(exactPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY.get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(精确): " + + keyStr + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + + // 第2步: Y轴偏移检查 (-5 到 +10) + for (int dy = -5; dy <= 10; dy++) { + if (dy == 0) continue; + int testY = by + dy; + io.papermc.paper.math.Position testPos = + io.papermc.paper.math.Position.block( + bx, testY, bz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + testPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(Y偏移" + + dy + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + } + + // 第3步: 水平偏移检查 (±1格) + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; + io.papermc.paper.math.Position offsetPos = + io.papermc.paper.math.Position + .block(bx + dx, by, + bz + dz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + offsetPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey( + structure); + String keyStr = (key != null) + ? key.toString() + : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(偏移" + + dx + "," + + dz + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat + : "未映射")); + if (cat != null) return cat; + } + } + } + } + + plugin.getLogger().info( + "[QuestTracker] 所有位置检查完毕, " + + "未匹配任何结构 (pos=" + + bx + "," + by + "," + bz + + ")"); + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测异常: " + + ex.getClass().getSimpleName() + + ": " + ex.getMessage()); + ex.printStackTrace(); + } + return null; + } + + /** + * 将结构中文名映射到对应的大类 + * 用于匹配任务条件中的结构名 + */ + private String mapStructureCategory( + String structureName) { + if (structureName == null) return ""; + + // 主世界结构 + if (structureName.contains("村庄")) + return "村庄"; + if (structureName.contains("沙漠神殿")) + return "沙漠"; + if (structureName.contains("丛林神庙") || structureName.contains("丛林神殿")) + return "森林"; + if (structureName.contains("沼泽小屋")) + return "沼泽"; + if (structureName.contains("雪屋") || structureName.contains("冰屋")) + return "雪原"; + if (structureName.contains("林地府邸")) + return "森林"; + if (structureName.contains("掠夺者前哨站")) + return "掠夺者前哨站"; + if (structureName.contains("废弃矿井") || structureName.contains("恶地废弃矿井")) + return "地下"; + // 要塞可生成在任何主世界群系下,返回"主世界" + if (structureName.contains("要塞")) + return "主世界"; + // 远古城市是结构,应分类为"远古城市" + if (structureName.contains("远古城市")) + return "远古城市"; + if (structureName.contains("远古遗迹") || structureName.contains("古迹废墟")) + return "远古遗迹"; + if (structureName.contains("试炼密室")) + return "试炼密室"; + if (structureName.contains("海底神殿") || structureName.contains("海洋神殿")) + return "海洋"; + if (structureName.contains("海底废墟") || structureName.contains("冻洋废墟") || structureName.contains("暖洋废墟")) + return "海洋"; + if (structureName.contains("沉船")) + return "海洋"; + if (structureName.contains("埋藏的宝箱") || structureName.contains("埋藏的宝藏")) + return "海洋"; + if (structureName.contains("废弃传送门")) + return "平原"; + if (structureName.contains("化石")) + return "主世界"; + + // 下界结构 + if (structureName.contains("下界要塞")) + return "下界"; + if (structureName.contains("猪灵堡垒") || structureName.contains("堡垒遗迹")) + return "下界"; + if (structureName.contains("玄武岩结构") || structureName.contains("下界化石")) + return "下界"; + + // 末地结构 + if (structureName.contains("末地城")) + return "末地"; + if (structureName.contains("末地船")) + return "末地"; + + return ""; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API或债券 + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage("§c无可领取的奖励"); + return; + } + + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) { + player.sendMessage( + "§7[可选奖励] 需完成所有可选条件: " + rt); + continue; + } + } + + // 按 + 分割子项 + String[] parts = rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } + } + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory().addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); + } + + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + public String getProgress( + String player, String rawCond) { + if (player == null || rawCond == null) + return "§7未知"; + String key = stripOptional(rawCond); + + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + + cur + "/1"; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "VILLAGER_TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + + } + + // ===== 击杀泛解析(支持中文列表) ===== +// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等 + if (key.contains("击杀") && key.contains("只")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + String mobPart = key; + java.util.regex.Matcher killM = + java.util.regex.Pattern + .compile("击杀\\d+只(.+)") + .matcher(key); + if (killM.find()) { + mobPart = killM.group(1).trim(); + } + + String[] mobNames = mobPart + .split("[、,,,]+"); + int totalKills = 0; + StringBuilder names = new StringBuilder(); + for (String name : mobNames) { + String trimmed = name.trim(); + if (trimmed.isEmpty()) continue; + String entityType = + chineseMobToEntity(trimmed); + if (!entityType.isEmpty()) { + totalKills += getCounter( + player, + "KILL:" + entityType); + if (names.length() > 0) + names.append("/"); + names.append(trimmed); + } + } + return "§7击杀" + names + ": §e" + + totalKills + "/" + needed; + } + +// 单一KILL:xxx:N 格式(原有逻辑保留) + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + int needed = parseInt(p[2]); + int cur = getCounter(player, + "KILL:" + p[1]); + return "§7击杀" + p[1] + ": §e" + + cur + "/" + needed; + } + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + // ===== 泛解析:击杀类进度 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < mobTypes.size(); i++) { + total += getCounter(player, + "KILL:" + mobTypes.get(i)); + if (i > 0) names.append("/"); + names.append(reverseMobName( + mobTypes.get(i))); + } + return "§7击杀" + names + ": §e" + + total + "/" + needed; + } + } + + // ===== 泛解析:村民交易进度 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + // ===== 泛解析:采集/挖矿进度 ===== + if (key.contains("采集") + || key.contains("挖")) { + int needed = + parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < matTypes.size(); i++) { + total += getCounter( + player, + "MINE:" + matTypes + .get(i)); + if (i > 0) + names.append("/"); + names.append( + translateMaterialName( + matTypes + .get(i))); + } + return "§7采集" + names + + ": §e" + total + + "/" + needed; + } + } + + // ===== 泛解析:合成进度 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "CRAFT"); + return "§7合成: §e" + cur + "/" + needed; + } + + // ===== 泛解析:钓鱼进度 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "FISH"); + return "§7钓鱼: §e" + cur + "/" + needed; + } + + return "§7" + key; + } + private String translateMaterialName( + String name) { + switch (name) { + case "COAL_ORE": + return "煤矿石"; + case "DEEPSLATE_COAL_ORE": + return "深层煤矿石"; + case "IRON_ORE": + return "铁矿石"; + case "DEEPSLATE_IRON_ORE": + return "深层铁矿石"; + case "COPPER_ORE": + return "铜矿石"; + case "DEEPSLATE_COPPER_ORE": + return "深层铜矿石"; + case "GOLD_ORE": + return "金矿石"; + case "DEEPSLATE_GOLD_ORE": + return "深层金矿石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "DEEPSLATE_DIAMOND_ORE": + return "深层钻石矿石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "DEEPSLATE_EMERALD_ORE": + return "深层绿宝石矿石"; + case "LAPIS_ORE": + return "青金石矿石"; + case "DEEPSLATE_LAPIS_ORE": + return "深层青金石矿石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "DEEPSLATE_REDSTONE_ORE": + return "深层红石矿石"; + case "NETHER_GOLD_ORE": + return "下界金矿石"; + case "NETHER_QUARTZ_ORE": + return "下界石英矿石"; + case "ANCIENT_DEBRIS": + return "远古残骸"; + case "COAL": + return "煤炭"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "RAW_COPPER": + return "粗铜"; + case "DIAMOND": + return "钻石"; + case "EMERALD": + return "绿宝石"; + case "LAPIS_LAZULI": + return "青金石"; + case "REDSTONE": + return "红石"; + case "QUARTZ": + return "石英"; + default: + return name; + } + } + + /** + * 中文物种名 → Bukkit EntityType 名 + * 支持击杀泛解析的模糊匹配 + */ + private String chineseMobToEntity(String cn) { + String n = cn.toLowerCase().trim(); + + // 直接别名表 + Map mobMap = new HashMap<>(); + mobMap.put("骷髅", "SKELETON"); + mobMap.put("僵尸", "ZOMBIE"); + mobMap.put("苦力怕", "CREEPER"); + mobMap.put("蜘蛛", "SPIDER"); + mobMap.put("洞穴蜘蛛", "CAVE_SPIDER"); + mobMap.put("小白", "SKELETON"); + mobMap.put("末影人", "ENDERMAN"); + mobMap.put("enderman", "ENDERMAN"); + mobMap.put("猪灵", "PIGLIN"); + mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE"); + mobMap.put("恶魂", "GHAST"); + mobMap.put("凋灵骷髅", "WITHER_SKELETON"); + mobMap.put("烈焰人", "BLAZE"); + mobMap.put("史莱姆", "SLIME"); + mobMap.put("岩浆怪", "MAGMA_CUBE"); + mobMap.put("女巫", "WITCH"); + mobMap.put("蠹虫", "SILVERFISH"); + mobMap.put("潜影贝", "SHULKER"); + mobMap.put("守卫者", "GUARDIAN"); + mobMap.put("远古守卫者", "ELDER_GUARDIAN"); + mobMap.put("溺尸", "DROWNED"); + mobMap.put("僵尸村民", "ZOMBIE_VILLAGER"); + mobMap.put("尸壳", "HUSK"); + mobMap.put("流浪者", "STRAY"); + mobMap.put("幻翼", "PHANTOM"); + mobMap.put("掠夺者", "PILLAGER"); + mobMap.put("卫道士", "VINDICATOR"); + mobMap.put("唤魔者", "EVOKER"); + mobMap.put("恼鬼", "VEX"); + mobMap.put("劫掠兽", "RAVAGER"); + mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY"); + mobMap.put("凋灵", "WITHER"); + mobMap.put("末影龙", "ENDER_DRAGON"); + + // 精确匹配 + if (mobMap.containsKey(n)) { + return mobMap.get(n); + } + + // 模糊匹配(包含关系) + for (Map.Entry en + : mobMap.entrySet()) { + if (n.contains(en.getKey()) + || en.getKey().contains(n)) { + return en.getValue(); + } + } + + // 尝试直接用英文名 + try { + return org.bukkit.entity.EntityType + .valueOf(n.toUpperCase() + .replace(" ", "_")) + .name(); + } catch (Exception e) { + return ""; + } + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + + // ===== 结构检测优先于Y轴"地下"判断 ===== + // 要塞等结构可生成在任何主世界群系下,不应固定为"地下" + // 使用 PaperMC API 检测玩家所在位置的结构 + String structureCat = null; + try { + structureCat = detectStructureAt( + worldName, x, y, z); + } catch (Exception ignored) {} + + if (structureCat != null + && !structureCat.isEmpty() + && !"地下".equals(structureCat)) { + // 结构检测成功且非"地下"类,结构分类优先 + category = structureCat; + plugin.getLogger().info( + "[QuestTracker] 结构覆盖: " + + category + + " (结构: " + + structureCat + ")"); + } else { + // 未检测到有效结构,使用Y轴检测判定地下 + // 地下检测:Y低于表面则归类为地下 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + if (structureCat != null + && !structureCat.isEmpty()) { + plugin.getLogger().info( + "[QuestTracker] 结构" + + structureCat + + " 使用Y轴判定"); + } else { + plugin.getLogger().info( + "[QuestTracker] 未检测到结构: world=" + + worldName + ", pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + } + } + + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡")) + continue; + // 支持群系和结构两种打卡条件 + boolean isBiomeCond = + condKey.contains("群系") + || condKey + .contains("风景点"); + boolean isStructureCond = + condKey.contains("结构") + || condKey + .contains("村庄") + || condKey + .contains("神殿") + || condKey + .contains("神庙") + || condKey + .contains("小屋") + || condKey + .contains("雪屋") + || condKey + .contains("府邸") + || condKey + .contains("前哨站") + || condKey + .contains("矿井") + || condKey + .contains("要塞") + || condKey + .contains("城市") + || condKey + .contains("废墟") + || condKey + .contains("密室") + || condKey + .contains("沉船") + || condKey + .contains("宝箱") + || condKey + .contains("传送门") + || condKey + .contains("化石") + || condKey + .contains("堡垒"); + if (!isBiomeCond + && !isStructureCond) + continue; + String target = + extractBiome(condKey); + plugin.getLogger().info( + "[QuestTracker] 条件匹配: condKey=\"" + + condKey + "\", target=\"" + + target + "\", category=\"" + + category + "\", isBiomeCond=" + + isBiomeCond + ", isStructureCond=" + + isStructureCond); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + /* + * 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录 + */ + public String checkInAtPosition( + String player, String worldName, + double x, double y, double z) { + + List history = + loadCheckInPositions(player); + for (double[] pos : history) { + double dist = Math.sqrt( + Math.pow(x - pos[0], 2) + + Math.pow(y - pos[1], 2) + + Math.pow(z - pos[2], 2)); + if (dist <= 32) { + return "§c距离上次打卡点太近(" + + String.format( + "%.0f", dist) + + "格)"; + } + } + + // ===== 获取群系和分类 ===== + String biome = "minecraft:plains"; + String category = "平原"; + String structureCat = null; + try { + org.bukkit.World w = + Bukkit.getWorld(worldName); + if (w != null) { + org.bukkit.Location loc = + new org.bukkit.Location( + w, + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + biome = loc.getBlock() + .getBiome().getKey() + .toString(); + category = mapBiomeCategory( + biome, worldName); + + // ===== 结构检测优先于Y轴"地下"判断 ===== + structureCat = + detectStructureAt( + worldName, x, y, z); + plugin.getLogger().info( + "[QuestTracker] checkInAtPosition 结构检测: " + + structureCat + " at " + + String.format("%.0f,%.0f,%.0f", x, y, z)); + + if (structureCat != null + && !structureCat.isEmpty() + && !"地下".equals(structureCat)) { + // 结构检测成功且非"地下"类,结构分类优先 + category = structureCat; + } else { + // 未检测到有效结构,使用Y轴检测判定地下 + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + + if ("下界".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.NETHER) { + category = "下界"; + } else if ("末地".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.THE_END) { + category = "末地"; + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 群系读取失败: " + + ex.getMessage()); + } + + plugin.getLogger().info( + "[QuestTracker] 打卡: " + + player + + " world=" + worldName + + " biome=" + biome + + " cat=" + category + + " pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + + saveCheckInPosition( + player, x, y, z); + recordCheckIn( + player, biome, category, + worldName, x, y, z); + List matched = + checkInBiome( + player, biome, + worldName, x, y, z); + + StringBuilder sb = new StringBuilder(); + sb.append("§a打卡成功!群系: §e") + .append(category); + if (structureCat != null && !structureCat.isEmpty()) { + sb.append("\n§a结构: §e").append(structureCat); + } + if (!matched.isEmpty()) { + sb.append("\n§a完成任务条件:"); + for (String m : matched) { + String qn = + findQuestName(m); + sb.append("\n§7• "); + if (qn != null) { + sb.append("§e[") + .append(qn) + .append("§7] §f"); + } + sb.append(m); + } + } + return sb.toString(); + } + + + /** + * 根据条件文本查找所属任务名 + */ + private String findQuestName( + String condition) { + for (String cat : Arrays.asList( + "新人任务", + "主线任务", + "支线任务")) { + for (QuestFile qf : + getQuests(cat)) { + if (qf.conditions.contains( + condition)) { + return qf.displayName; + } + } + } + return null; + } + + + /** + * 加载玩家所有历史打卡坐标 + * 格式: x1,y1,z1;x2,y2,z2;... + */ + private List loadCheckInPositions( + String player) { + List list = new ArrayList<>(); + Object val = storage.getCondition( + player, "checkin_all_pos"); + if (val == null) return list; + String data = val.toString(); + if (data.isEmpty()) return list; + String[] records = data.split(";"); + for (String r : records) { + String[] parts = r.split(","); + if (parts.length >= 3) { + try { + list.add(new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2])}); + } catch (NumberFormatException e) { + // 跳过损坏数据 + } + } + } + return list; + } + + /** + * 保存打卡坐标(追加到已有记录) + */ + private void saveCheckInPosition( + String player, double x, + double y, double z) { + List list = + loadCheckInPositions(player); + list.add(new double[]{x, y, z}); + StringBuilder sb = new StringBuilder(); + for (double[] pos : list) { + if (sb.length() > 0) sb.append(";"); + sb.append(pos[0]).append(",") + .append(pos[1]).append(",") + .append(pos[2]); + } + storage.setConditionRaw( + player, + "checkin_all_pos", + sb.toString()); + } + // ========== 中文生物名 → EntityType 映射 ========== + private static final Map + MOB_NAME_MAP = new HashMap<>(); + static { + MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER"); + MOB_NAME_MAP.put("骷髅", "SKELETON"); + MOB_NAME_MAP.put("僵尸", "ZOMBIE"); + MOB_NAME_MAP.put("苦力怕", "CREEPER"); + MOB_NAME_MAP.put("蜘蛛", "SPIDER"); + MOB_NAME_MAP.put("末影人", "ENDERMAN"); + MOB_NAME_MAP.put("女巫", "WITCH"); + MOB_NAME_MAP.put("烈焰人", "BLAZE"); + MOB_NAME_MAP.put("僵尸猪灵", + "ZOMBIFIED_PIGLIN"); + MOB_NAME_MAP.put("猪灵", "PIGLIN"); + MOB_NAME_MAP.put("恶魂", "GHAST"); + MOB_NAME_MAP.put("凋灵骷髅", + "WITHER_SKELETON"); + MOB_NAME_MAP.put("史莱姆", "SLIME"); + MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE"); + MOB_NAME_MAP.put("溺尸", "DROWNED"); + MOB_NAME_MAP.put("尸壳", "HUSK"); + MOB_NAME_MAP.put("流髑", "STRAY"); + MOB_NAME_MAP.put("幻翼", "PHANTOM"); + MOB_NAME_MAP.put("守卫者", "GUARDIAN"); + MOB_NAME_MAP.put("远古守卫者", + "ELDER_GUARDIAN"); + MOB_NAME_MAP.put("掠夺者", "PILLAGER"); + MOB_NAME_MAP.put("卫道士", "VINDICATOR"); + MOB_NAME_MAP.put("唤魔者", "EVOKER"); + MOB_NAME_MAP.put("恼鬼", "VEX"); + MOB_NAME_MAP.put("劫掠兽", "RAVAGER"); + MOB_NAME_MAP.put("潜影贝", "SHULKER"); + MOB_NAME_MAP.put("末影螨", "ENDERMITE"); + MOB_NAME_MAP.put("凋灵", "WITHER"); + MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON"); + MOB_NAME_MAP.put("监守者", "WARDEN"); + MOB_NAME_MAP.put("嗅探兽", "SNIFFER"); + MOB_NAME_MAP.put("僵尸村民", + "ZOMBIE_VILLAGER"); + } + + /** + * 从条件文本中提取所有匹配的生物类型 + * 按名称长度降序匹配,避免短名误匹配 + */ + private List extractMobTypes( + String text) { + List types = new ArrayList<>(); + List> sorted = + new ArrayList<>( + MOB_NAME_MAP.entrySet()); + sorted.sort((a, b) -> + b.getKey().length() + - a.getKey().length()); + String remaining = text; + for (Map.Entry entry + : sorted) { + if (remaining.contains( + entry.getKey())) { + if (!types.contains( + entry.getValue())) { + types.add(entry.getValue()); + } + remaining = remaining.replace( + entry.getKey(), ""); + } + } + return types; + } + + /** + * EntityType → 中文名 + */ + private String reverseMobName( + String entityType) { + for (Map.Entry entry + : MOB_NAME_MAP.entrySet()) { + if (entry.getValue() + .equals(entityType)) { + return entry.getKey(); + } + } + return entityType; + } + + /** + * 从条件文本中提取矿物名 + * "采集任意矿物(煤炭除外)1组" + * → [COAL_ORE, IRON_ORE, GOLD_ORE, ...] + */ + private List extractMaterialTypes( + String text) { + List types = new ArrayList<>(); + // 排除列表 + List excludes = + new ArrayList<>(); + if (text.contains("煤炭除外")) { + excludes.add("COAL"); + excludes.add("COAL_ORE"); + excludes.add("DEEPSLATE_COAL_ORE"); + } + // 通配符:"矿物" "矿石" + if (text.contains("矿物") + || text.contains("矿石")) { + String[] ores = { + "COAL_ORE", + "DEEPSLATE_COAL_ORE", + "IRON_ORE", + "DEEPSLATE_IRON_ORE", + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE", + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE", + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE", + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE", + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE", + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE", + "NETHER_GOLD_ORE", + "NETHER_QUARTZ_ORE", + "ANCIENT_DEBRIS" + }; + for (String ore : ores) { + if (!excludes.contains(ore)) { + types.add(ore); + } + } + return types; + } + // 单个矿物名 + if (text.contains("煤炭")) + types.add("COAL"); + if (text.contains("铁矿") + || text.contains("铁锭")) + types.add("RAW_IRON"); + if (text.contains("金矿") + || text.contains("金锭")) + types.add("RAW_GOLD"); + if (text.contains("钻石")) + types.add("DIAMOND"); + if (text.contains("绿宝石")) + types.add("EMERALD"); + if (text.contains("青金石")) + types.add("LAPIS_LAZULI"); + if (text.contains("红石")) + types.add("REDSTONE"); + if (text.contains("铜")) + types.add("RAW_COPPER"); + if (text.contains("下界石英") + || text.contains("石英")) + types.add("QUARTZ"); + return types; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + * "打卡村庄" → "村庄" + * "打卡沙漠神殿" → "沙漠" + */ + private String extractBiome(String text) { + if (text == null) return ""; + + // 优先匹配结构名称(更具体) + String structureResult = + mapStructureCategory(text); + if (!structureResult.isEmpty()) { + return structureResult; + } + + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点") + || key.contains("结构") + || key.contains("村庄") + || key.contains("神殿") + || key.contains("神庙") + || key.contains("小屋") + || key.contains("雪屋") + || key.contains("府邸") + || key.contains("前哨站") + || key.contains("矿井") + || key.contains("要塞") + || key.contains("城市") + || key.contains("废墟") + || key.contains("密室") + || key.contains("废墟") + || key.contains("沉船") + || key.contains("宝箱") + || key.contains("传送门") + || key.contains("化石") + || key.contains("堡垒"))) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; + } + return getCounter( + player, "biome_ALL") >= 1; + } + + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "VILLAGER_TRADE") + >= needed; + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "REPORT_VILLAGE") + >= needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return streak >= needed; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + // ===== 泛解析:击杀类中文条件 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + for (String mobType : mobTypes) { + total += getCounter(player, + "KILL:" + mobType); + } + return total >= needed; + } + } + + // ===== 泛解析:村民交易条件 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "TRADE") + >= needed; + } + + // ===== 泛解析:采集/挖矿类条件 ===== + if (key.contains("采集") || key.contains("挖")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + for (String mat : matTypes) { + total += getCounter(player, + "MINE:" + mat); + } + return total >= needed; + } + } + + // ===== 泛解析:合成类条件 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "CRAFT") + >= needed; + } + + // ===== 泛解析:钓鱼类条件 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "FISH") + >= needed; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public void onVillagerTrade(String player) { + String key = "VILLAGER_TRADE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/backup/Main_backup_20260606_235342.java b/backup/Main_backup_20260606_235342.java new file mode 100644 index 0000000..95903d1 --- /dev/null +++ b/backup/Main_backup_20260606_235342.java @@ -0,0 +1,5734 @@ +package Sdf1_login; + + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.io.File; + +import org.bukkit.event.inventory.*; +import org.bukkit.event.player.PlayerInteractEvent; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.LinkedHashMap; +import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import java.util.Date; +import org.bukkit.command.CommandSender; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.scheduler.BukkitTask; + + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; + + + +public class Main extends JavaPlugin + implements CommandExecutor, Listener, + TabCompleter { + + private DatabaseManager db; + private MenuIconManager menuIconMgr; + private ConfigManager config; + private EmailManager email; + private LoginManager loginMgr; + private AFKManager afk; + private GiftManager gift; + private CheckInManager checkIn; + private InviteManager invite; + private PointsManager points; + public GUIManager gui; + private ChatInputManager chatInput; + private Economy economy; + private VerificationManager verification; + private IPGroupManager ipGroup; + private AccountRequestManager accountRequest; + private TicketManager ticket; + private ChatFilterManager chatFilter; + private GarbageManager garbage; + public RadioManager radio; + // 在 lastActivity 字段后面添加: + // [ADDED] 任务追踪系统 + private QuestTracker questTracker; + // [FIX] 垃圾箱防抖锁(记录上次操作时间戳) + private final Map garbageBusy = + new ConcurrentHashMap<>(); + // [ADDED] 群系检测节流 + private final Map lastBiomeCheck = + new ConcurrentHashMap<>(); + // [ADDED] 垃圾站宝箱物品投入次数计数器(事不过三) + private final Map treasureDiscardCount = + new ConcurrentHashMap<>(); + // 在 private WelcomeManager welcome; 字段(若不存在)添加: + private WelcomeManager welcome; + private final Set pendingAdminAuth = + new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + /* + 资源包下载部分控制,不经过RadioDownloadListener的熔断控制 + */ + private RadioDownloadListener radioDL; + private final Set chatInputActive = + ConcurrentHashMap.newKeySet(); + // 补签确认:玩家名 → "日期:总积分" + public final Map + pendingBackCheck = + new HashMap<>(); + public MenuManager menu; + + private final Set loggedIn = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Set needsPasswordChange = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map joinTime = + new ConcurrentHashMap<>(); + private final Map joinLoc = + new HashMap<>(); + private final Map ipAutoLogin = + new HashMap<>(); + private static final long IP_SKIP_MS = 300000L; + private SalesStatsManager salesStats; + + private final Map lastActivity = + new ConcurrentHashMap<>(); + private CommissionManager commission; + // 余额操作待处理 + private String balanceTarget = null; + private boolean balanceGiveMode = false; + private BondManager bondManager; + private CDKManager cdkManager; + private UserGroupManager userGroupManager; + private CypayCommand cypayCommand; + + public BondManager getBonds() { + return bondManager; + } + + public CDKManager getCDK() { + return cdkManager; + } + + public UserGroupManager getUserGroup() { + return userGroupManager; + } + + public CypayCommand getCypay() { + return cypayCommand; + } + + // 在 Main.java 字段声明区域(bondManager 附近)添加: +// 在 bondManager 字段附近添加: + private BondPrinter bondPrinter; + private OrderManager orderManager; + + public OrderManager getOrderManager() { + return orderManager; + } + + public AreaProtection areaProtection; + + + // 钱包流水查看目标(玩家名 → 查看目标) + private final Map walletViewTarget = + new ConcurrentHashMap<>(); + + // 流水格式化 + private static final java.text.SimpleDateFormat + TX_SDF = new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private ShopManager shopManager; //商店 + + + public CommissionManager getCommission() { + return commission; + } + + //pvp + private PVPManager pvpManager; + + private static class PwdRollback { + final String hash; + final String salt; + final long time; + + PwdRollback(String h, String s, long t) { + hash = h; + salt = s; + time = t; + } + } + + // 待删除的玩家名(仅保留最后一个) + private String pendingDeleteName = null; + private BukkitRunnable pendingDeleteTask = null; + + private final Map + pwdRollback = + new ConcurrentHashMap<>(); + private final Map menuChatField = + new java.util.concurrent.ConcurrentHashMap<>(); + private final Map menuChatIdx = + new java.util.concurrent.ConcurrentHashMap<>(); + + + public void recordPasswordChange(String name, + String oldHash, String oldSalt) { + pwdRollback.put(name, + new PwdRollback(oldHash, oldSalt, + System.currentTimeMillis())); + } + + // ===== Getters ===== + public DatabaseManager getDb() { + return db; + } + + + public ConfigManager getConfig2() { + return config; + } + + public EmailManager getEmail() { + return email; + } + + public Economy getEconomy() { + return economy; + } + + public LoginManager getLoginMgr() { + return loginMgr; + } + + public AFKManager getAfk() { + return afk; + } + + public GiftManager getGift() { + return gift; + } + + public CheckInManager getCheckIn() { + return checkIn; + } + + public InviteManager getInvite() { + return invite; + } + + public PointsManager getPoints() { + return points; + } + + public GUIManager getGui() { + return gui; + } + + public ChatInputManager getChatInput() { + return chatInput; + } + + public VerificationManager getVerification() { + return verification; + } + + public IPGroupManager getIPGroup() { + return ipGroup; + } + + public AccountRequestManager getAccountRequest() { + return accountRequest; + } + + public TicketManager getTicket() { + return ticket; + } + + public QuestTracker getQuestTracker() { + return questTracker; + } + + public ChatFilterManager getChatFilter() { + return chatFilter; + } + + public GarbageManager getGarbage() { + return garbage; + } + + public Set getLoggedIn() { + return loggedIn; + } + + public Set getNeedsPasswordChange() { + return needsPasswordChange; + } + + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e) { + if (areaProtection != null) { + Player p = e.getPlayer(); + areaProtection.onPlayerOffline( + p.getUniqueId(), p.getName(), ""); + if (areaProtection != null) { + areaProtection.onPlayerQuit(p.getName()); + } + + } + } + + + // public RadioManager radio; + @Override + public void onEnable() { + getDataFolder().mkdirs(); + // 启动时清理.sdf1临时文件 + cleanupSdf1Files(); + salesStats = new SalesStatsManager(this); + + // ===== 1. 基础配置 ===== + config = new ConfigManager(getDataFolder()); + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + configMgr.loadMessages(); + orderManager = new OrderManager(this); + // ===== 2. 数据库 ===== + db = new DatabaseManager(getDataFolder()); + db.init(); + menuIconMgr = new MenuIconManager(this); + + + // ===== 3. 菜单 ===== + menu = new MenuManager(this); + menu.loadMenu(); + + // ===== 4. GUI(依赖 menu) ===== + gui = new GUIManager(this); + shopManager = new ShopManager(this); + Bukkit.getPluginManager().registerEvents(shopManager, this); + // shopManager.loadCategories(); // 构造器内已调用,此处重复 + + getServer().getPluginManager() + .registerEvents(gui, this); + + // ===== 5. 其他管理器 ===== + email = new EmailManager(config); + loginMgr = new LoginManager(this); + afk = new AFKManager(config, this); + gift = new GiftManager(this); + gift.loadStages(); + checkIn = new CheckInManager(this); + invite = new InviteManager(this); + points = new PointsManager(this); + chatInput = new ChatInputManager(); + verification = new VerificationManager(this); + ipGroup = new IPGroupManager(this, + config.maxAccountsPerIP); + accountRequest = + new AccountRequestManager(this); + accountRequest.setMode(config.approvalMode); + accountRequest.setAutoDelayMinutes( + config.autoApproveDelayMinutes); + ticket = new TicketManager(this); + commission = new CommissionManager(this); + questTracker = new QuestTracker(this); + chatFilter = new ChatFilterManager(this); + chatFilter.loadConfig(); + welcome = new WelcomeManager(this); + + // ===== 6. 垃圾箱 ===== + garbage = new GarbageManager(this); + garbage.init(); + garbage.loadConfig(); + garbage.startAutoCleanup(); + + // ===== 7. Radio ===== + radio = new RadioManager(this); + radio.init(); + radioDL = new RadioDownloadListener(this); + getServer().getPluginManager() + .registerEvents(radioDL, this); + + // ===== 铁块电梯 ===== + getServer().getPluginManager() + .registerEvents(new IronBlockElevator(), this); + + // ===== 8. 经济 ===== + setupEconomy(); + + // ===== 9. 注册命令 ===== + if (getCommand("sdf1_login") != null) { + getCommand("sdf1_login") + .setExecutor(this); + getCommand("sdf1_login") + .setTabCompleter(this); + } + if (getCommand("reg") != null) + getCommand("reg").setExecutor(this); + if (getCommand("login") != null) + getCommand("login").setExecutor(this); + if (getCommand("l") != null) + getCommand("l").setExecutor(this); + if (getCommand("签到") != null) + getCommand("签到") + .setExecutor(this); + if (getCommand("recycle") != null) + getCommand("recycle") + .setExecutor(this); + if (getCommand("垃圾清理") != null) + getCommand("垃圾清理") + .setExecutor(this); + /* if (getCommand("oa") != null) + getCommand("oa").setExecutor(this);*/ + if (getCommand("menu") != null) + getCommand("menu").setExecutor(this); + if (getCommand("printer") != null) { + getCommand("printer").setExecutor(this); + getCommand("shop").setExecutor(this); + getCommand("商店").setExecutor(this); + // getCommand("testorder").setExecutor(this); + if (getCommand("printer") != null) { + getCommand("printer").setExecutor(this); + getCommand("shop").setExecutor(this); + getCommand("商店").setExecutor(this); + } +// protect 命令独立注册 + if (getCommand("protect") != null) { + getCommand("protect").setExecutor(this); + getCommand("protect").setTabCompleter(this); + } + } + + // ===== 10. 注册事件 ===== + getServer().getPluginManager() + .registerEvents(this, this); + + // ===== 11. 定时任务 ===== + afk.startCheck(); + startLoginReminder(); + startTimeoutCheck(); + startPasswordReminder(); + startTicketAutoProcess(); + radio.startMainRadio(); + new BukkitRunnable() { + public void run() { + if (questTracker == null) return; + for (Player p : + Bukkit.getOnlinePlayers()) { + if (!isFrozen(p)) + questTracker.onPlayTime( + p.getName(), 30000L); + } + } + }.runTaskTimer(this, 600L, 600L); +// [新增] Printer 文件清理(每小时检测一次) + new BukkitRunnable() { + public void run() { + if (bondPrinter != null) + bondPrinter.cleanupOldFiles(); + } + }.runTaskTimer(this, 1200L, 72000L); + + // 12 ===PVP=== + pvpManager = new PVPManager(this); + getServer().getPluginManager() + .registerEvents(pvpManager, this); +// PVP区域检测定时器 + getServer().getScheduler() + .runTaskTimer(this, + () -> pvpManager.tickRegions(), + 20L, 20L); + setupPVPTab(); + + // 13 ====cypay债券==== + // 债券系统(独立DB) + bondManager = new BondManager(this); + cdkManager = new CDKManager(this); + userGroupManager = new UserGroupManager(this); + cypayCommand = new CypayCommand(this); +/*加载CDK文件 + cdkManager.loadCDKsFromDir();*/ + // cypay 命令 + if (getCommand("cypay") != null) { + getCommand("cypay").setExecutor(cypayCommand); + getCommand("cypay").setTabCompleter(cypayCommand); + } + bondPrinter = new BondPrinter(this); + // cdkManager.loadCDKsFromDir(); +//14 ====商店==== + // ★ 构造器内已加载完毕,此处重复 + // if (shopManager != null) { + // shopManager.loadCategories(); + // } + + configMgr = new ConfigManager(getDataFolder()); + configMgr.loadSettings(); + orderManager = new OrderManager(this); + +// 15 ======区域保护========= + areaProtection = new AreaProtection(this); + getServer().getPluginManager() + .registerEvents(areaProtection, this); + +// 效果清除定时器(只留一个) + getServer().getScheduler().runTaskTimer(this, () -> { + if (areaProtection != null) { + } + }, 20L, 20L); + +// 和平模式独立检测 + getServer().getScheduler().runTaskTimer(this, () -> { + if (areaProtection == null) return; + for (Player p : Bukkit.getOnlinePlayers()) { + AreaProtection.AreaConfig ac = areaProtection.getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null) { + // 无条件打印,不管 peaceMode 是啥 + /* getLogger().info("[和平定时器] " + p.getName() + + " 在[" + ac.name + + "] peaceMode=" + ac.peaceMode + + " 白名单=" + ac.peaceWhitelist);*/ + if (ac.peaceMode) { + areaProtection.banHostilesWithWhitelist(p, ac); + } + } + } + }, 40L, 40L); + areaProtection.startEnforceTask(); + if (areaProtection != null) { + areaProtection.reload(); + areaProtection.startEnforceTask(); + } + + + getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈"); + getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件\nSdf1_game娱乐游戏插件"); + // 大字画 + getLogger().info("\n" + + " ____ _ __ _ _ _ \n" + + " / ___| __| |/ _/ | | | ___ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | | |/ _ \\ / _` | | '_ \\ \n" + + " ___) | (_| | _| | | | (_) | (_| | | | | | \n" + + " |____/ \\__,_|_| |_|___|_|\\___/ \\__, |_|_| |_| \n" + + " ____ _ __ |_____| _ |___/ _ \n" + + " / ___| __| |/ _/ |_ __ | |_ _ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | '_ \\| | | | |/ _` | | '_ \\ \n" + + " ___) | (_| | _| | |_) | | |_| | (_| | | | | | \n" + + " |____/ \\__,_|_| |_| .__/|_|\\__,_|\\__, |_|_| |_|___ _ __ _ \n" + + " _ __ _____ |_|___ _ __ | |___/_ _ / ___| __| |/ _/ |\n" + + " | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" + + " | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" + + " | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" + + " |_| |___/ " + ); + } + + public BondPrinter getBondPrinter() { + return bondPrinter; + } + + public ShopManager getShopManager() { + return shopManager; + } + + private ConfigManager configMgr; + + public ConfigManager getConfigMgr() { + return configMgr; + } + + + private void setupPVPTab() { + if (getCommand("pvp") != null) { + getCommand("pvp") + .setTabCompleter( + (sender, cmd, + alias, args) -> { + List list = + new ArrayList<>(); + if (args.length == 1) { + String[] sub = { + "create", "stats", + "list", "delete", + "tool", "on", "off", + "tempban"}; + for (String s : sub) { + if (s.startsWith( + args[0] + .toLowerCase())) { + list.add(s); + } + } + } else if (args.length == 2) { + String sub = + args[0].toLowerCase(); + if (sub.equals("stats")) { + for (Player pp : Bukkit + .getOnlinePlayers()) { + if (pp.getName() + .toLowerCase() + .startsWith(args[1] + .toLowerCase())) { + list.add(pp.getName()); + } + } + } else if (sub.equals("delete") + || sub.equals("on") + || sub.equals("off") + || sub.equals("tempban")) { + for (String rn : + pvpManager + .getRegionNames()) { + if (rn.toLowerCase() + .startsWith(args[1] + .toLowerCase())) { + list.add(rn); + } + } + } + } + return list; + }); + } + } + + @Override + public void onDisable() { + // 卸载时清理.sdf1临时文件 + cleanupSdf1Files(); + if (shopManager != null) shopManager.saveAll(); + // 取消未执行的删除任务 + if (pendingDeleteTask != null) { + pendingDeleteTask.cancel(); + pendingDeleteTask = null; + getLogger().info( + "[Sdf1_login] 服务器关闭," + + "取消待删除: " + + pendingDeleteName); + pendingDeleteName = null; + } + if (db != null) db.close(); + if (garbage != null) garbage.close(); + if (questTracker != null) + questTracker.shutdown(); + if (areaProtection != null) { + areaProtection.stopEnforceTask(); + } + for (BukkitTask task : areaProtection.getPendingTasks()) { + task.cancel(); + } + areaProtection.saveWhitelists(); + areaProtection.stopEnforceTask(); + // 取消所有延时清理任务,立即清理 + if (areaProtection != null) { + for (Map.Entry entry + : areaProtection.getPendingClearTasks().entrySet()) { + entry.getValue().cancel(); + // 立即清理效果 + Player p = Bukkit.getPlayer(entry.getKey()); + if (p != null && p.isOnline()) { + List marked = + areaProtection.getPlayerMarkedEffects() + .remove(entry.getKey()); + if (marked != null) { + for (PotionEffectType type : marked) { + p.removePotionEffect(type); + } + } + } + } + areaProtection.stopEnforceTask(); + areaProtection.saveWhitelists(); + areaProtection.getAlreadyForced().clear(); + + } + } + + /** + * 清理插件目录下的 .sdf1 后缀文件 + * 用于防风控任务准备 + */ + private void cleanupSdf1Files() { + File dataFolder = getDataFolder(); + if (dataFolder == null || !dataFolder.exists()) { + return; + } + + File[] sdf1Files = dataFolder.listFiles( + (dir, name) -> name.endsWith(".sdf1") + ); + + if (sdf1Files != null && sdf1Files.length > 0) { + int deletedCount = 0; + for (File file : sdf1Files) { + if (file.delete()) { + deletedCount++; + } + } + getLogger().info("[Sdf1_login] 已清理 " + + deletedCount + " 个 .sdf1 临时文件"); + } + } + + private void setupEconomy() { + if (getServer().getPluginManager() + .getPlugin("Vault") == null) return; + RegisteredServiceProvider rsp = + getServer().getServicesManager() + .getRegistration( + Economy.class); + if (rsp != null) + economy = rsp.getProvider(); + } + + // ===== Timers ===== + private void startTimeoutCheck() { + new BukkitRunnable() { + public void run() { + long now = System.currentTimeMillis(); + Iterator> it = + joinTime.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = + it.next(); + if (now - entry.getValue() + > config.loginTimeout + * 1000L) { + Player p = Bukkit.getPlayer( + entry.getKey()); + if (p != null && p.isOnline() + && !loggedIn.contains( + p.getName())) + p.kickPlayer(config.msg( + "login_timeout")); + it.remove(); + } + } + } + }.runTaskTimer(this, 80L, 80L); + } + + private void startLoginReminder() { + new BukkitRunnable() { + public void run() { + Iterator it = + joinTime.keySet().iterator(); + while (it.hasNext()) { + UUID uuid = it.next(); + Player p = Bukkit.getPlayer(uuid); + if (p == null || !p.isOnline()) { + it.remove(); + continue; + } + if (loggedIn.contains( + p.getName())) { + it.remove(); + continue; + } + // 已注册→提示登录,未注册→提示注册 + if (db.userExists(p.getName())) + p.sendMessage(config.msg( + "not_logged_in")); + else + p.sendMessage(config.msg( + "not_registered")); + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startPasswordReminder() { + new BukkitRunnable() { + public void run() { + for (Player p : + Bukkit.getOnlinePlayers()) { + if (needsPasswordChange.contains( + p.getName())) { + p.sendTitle("§c§l请修改密码", + "§f使用 /sdf1_login pw 修改密码", + 10, 40, 10); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 1.0f); + } + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startTicketAutoProcess() { + new BukkitRunnable() { + public void run() { + if (ticket != null) + ticket.autoProcessCompleted(); + } + }.runTaskTimer(this, 6000L, 6000L); + } + + // ===== Utils ===== + public String getPlayerIP(Player p) { + InetSocketAddress addr = p.getAddress(); + if (addr == null) return null; + return addr.getAddress().getHostAddress(); + } + + + public void resetActivity(Player p) { + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + } + + public boolean canAutoLogin(Player p) { + String ip = getPlayerIP(p); + if (ip == null) return false; + Long last = ipAutoLogin.get(ip); + return last != null + && System.currentTimeMillis() - last + < IP_SKIP_MS; + } + + public void recordIPLogin(Player p) { + String ip = getPlayerIP(p); + if (ip != null) + ipAutoLogin.put(ip, + System.currentTimeMillis()); + } + + public boolean isFrozen(Player p) { + return !loggedIn.contains(p.getName()); + } + private String debugCodePoints(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + if (i > 0) sb.append(" "); + sb.append(String.format("U+%04X", + (int) s.charAt(i))); + } + return sb.toString(); + } + + private boolean isAdmin(CommandSender sender) { + if (sender instanceof Player) + return ((Player) sender) + .getScoreboardTags() + .contains(config.adminTag); + return true; + } + + private void clearPlayer(Player p) { + p.getInventory().clear(); + p.getInventory().setArmorContents( + new ItemStack[4]); + p.getInventory().setExtraContents( + new ItemStack[1]); + p.setLevel(0); + p.setExp(0f); + } + + public void restoreInventory(Player p) { + String name = p.getName(); + try { + String[] backup = + db.loadInventoryBackup(name); + if (backup == null + || !isValidBackup(backup)) { + return; + } + + debugPrintBase64("恢复前", name, + backup[0]); + + ItemStack[] contents = + InventorySerializer + .fromBase64(backup[0]); + if (contents != null) + p.getInventory() + .setContents(contents); + + if (backup[1] != null + && !backup[1].isEmpty()) { + ItemStack[] armor = + InventorySerializer + .fromBase64( + backup[1]); + if (armor != null) + p.getInventory() + .setArmorContents( + armor); + } + + if (backup[2] != null + && !backup[2].isEmpty()) { + ItemStack[] extra = + InventorySerializer + .fromBase64( + backup[2]); + if (extra != null) + p.getInventory() + .setExtraContents( + extra); + } + + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) p.setLevel(lv); + double xp = Double.parseDouble( + backup[4]); + if (xp > 0) + p.setExp((float) xp); + + debugPrintBase64("恢复后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + getLogger().info( + "[Sdf1_login] " + name + + " 背包已恢复"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 还原背包失败: " + + name); + e.printStackTrace(); + } + } + + + public void backupInventory(Player p) { + String name = p.getName(); + int cnt = countItems(p) + + countArmor(p) + + countExtra(p); + if (cnt == 0) { + getLogger().warning( + "[Sdf1_login] " + name + + " 背包为空,跳过备份"); + return; + } + try { + String invB64 = + InventorySerializer.toBase64( + p.getInventory() + .getContents()); + String armorB64 = + InventorySerializer.toBase64( + p.getInventory() + .getArmorContents()); + String extraB64 = + InventorySerializer.toBase64( + p.getInventory() + .getExtraContents()); + db.saveInventoryBackup(name, invB64, + armorB64, extraB64, + p.getLevel(), p.getExp()); + getLogger().info( + "[Sdf1_login] " + name + + " 背包已备份 (物品:" + + cnt + ")"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 背包备份失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventory(Player p) { + String name = p.getName(); + try { + debugPrintBase64("清空前", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + // 清空背包(备份已在上次退出时保存) + p.getInventory().clear(); + p.getInventory() + .setArmorContents(null); + p.getInventory() + .setExtraContents(null); + p.setLevel(0); + p.setExp(0); + + debugPrintBase64("清空后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] hideInventory失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventoryDelayed(Player p, + int attempt) { + String name = p.getName(); + if (!p.isOnline()) return; + if (!isFrozen(p)) return; + + // 从数据库加载已有备份 + String[] existing = + db.loadInventoryBackup(name); + + // 调试打印 + debugPrintBase64("加入-加载DB", name, + existing != null + ? existing[0] : null); + + // 无有效备份 → 跳过清空背包 + if (existing == null + || !isValidBackup(existing)) { + getLogger().info( + "[Sdf1_login] " + name + + " 无有效备份," + + "跳过清空背包"); + return; + } + + // 有有效备份 → 清空当前背包 + hideInventory(p); + } + + + public void autoLogin(Player p, + String registerType) { + String name = p.getName(); + String ip = getPlayerIP(p); + String uuid = + p.getUniqueId().toString(); + String defaultPwd = "123456aA"; + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + if (!"manual".equals( + config.approvalMode)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + String salt = + PasswordUtils.generateSalt(); + String hash = PasswordUtils.hash( + defaultPwd, salt); + db.createUser(name, hash, salt); + db.setField(name, + "premium_uuid", uuid); + db.setField(name, + "register_ip", ip); + db.setField(name, + "register_type", registerType); + db.recordIP(name, ip); + } + + loggedIn.add(name); + p.setAllowFlight(false); + p.setFlying(false); + + + db.setLoggedIn(name, true); + db.setField(name, + "last_login_time", + System.currentTimeMillis()); + db.setField(name, + "last_online_check", + System.currentTimeMillis()); + db.recordIP(name, ip); + joinTime.remove(p.getUniqueId()); + recordIPLogin(p); + + // 只在有有效备份时恢复 + restoreInventory(p); + + giveMenuSnowball(p); + p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); + if (welcome != null) welcome.onLogin(p); + activateBeibao(p); + pushPendingAlerts(p); + + // ★ 玩家上线时恢复区域效果 + AreaProtection areaProt = getAreaProtection(); + if (areaProt != null) { + areaProt.onPlayerJoin(p); + } + + } + + private void activateBeibao(Player p) { + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, p.getName(), + 0, 0); + } catch (Exception ignored) { + } + } + + public void triggerSecurityAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + db.recordSecurityAlert(name, suspiciousIP); + Bukkit.broadcastMessage( + "§c§l[安全警告] §e" + name + + " §f账号疑似遭到 §c" + + suspiciousIP + + " §f的盗号尝试"); + String emailAddr = + (String) db.getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String body = "玩家 " + name + + ":\n您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + + suspiciousIP + + " 的盗号尝试。\n" + + "建议立即修改密码。"; + final String to = emailAddr; + final String finalBody = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, name, + finalBody)); + } + } + + public void pushPendingAlerts(Player p) { + String name = p.getName(); + List> alerts = + db.getUnnotifiedAlerts(name); + if (alerts.isEmpty()) return; + p.sendMessage("§c§l========== 安全报警 =========="); + for (Map alert : alerts) { + p.sendMessage("§e时间: §f" + + alert.get("alert_date")); + p.sendMessage("§e可疑IP: §c" + + alert.get("alert_ip")); + db.markAlertNotified( + ((Number) alert.get("id")) + .intValue()); + } + p.sendMessage( + "§c建议使用 §e/sdf1_login pw §c修改密码"); + p.sendMessage( + "§c§l=================================="); + } + + public void handleReset(Player p) { + loginMgr.handleReset(p); + } + + public boolean dailySign(Player p) { + if (checkIn.isCheckedInToday(p.getName())) + return false; + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(p.getName()); + return true; + } + + public void openTaskPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l任务面板"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + + // 打卡按钮(独立,slot 11) + g.setItem(11, mkItem(Material.COMPASS, + "§b§l群系打卡", + "§7在不同群系打卡完成任务", + "§7打卡范围: 32格去重")); + + // 新人任务(slot 15) + g.setItem(15, mkItem(Material.BOOK, + "§e新人任务", + "§7完成新手阶段获取奖励")); + + // 返回 + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + public void openAccountRequestPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l账号请求"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + g.setItem(10, mkItem(Material.BARRIER, + "§c申请删除账号", + "§7申请删除自己名下的账号")); + g.setItem(13, mkItem(Material.PAPER, + "§6提交工单", + "§7提交bug反馈、求助、举报等")); + g.setItem(16, mkItem(Material.REDSTONE, + "§e找回密码", + "§7通过邮箱找回密码")); + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private void playSuccessSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private int countItems(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countArmor(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getArmorContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countExtra(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getExtraContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + /** + * 检查备份是否有效(包含非雪球的真实物品) + */ + public boolean isValidBackup(String[] backup) { + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) { + return false; + } + try { + ItemStack[] items = + InventorySerializer + .fromBase64(backup[0]); + if (items == null) return false; + for (ItemStack item : items) { + if (item == null + || item.getType() + == Material.AIR) + continue; + if (isMenuSnowball(item)) + continue; + if (isTreasureItem(item)) + continue; // Sdf1_game宝箱物品不计入 + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * 控制台打印 base64 前10位 + */ + private void debugPrintBase64(String label, + String name, String base64) { + if (base64 == null || base64.isEmpty()) { + getLogger().info( + "[BACKUP_DEBUG] " + label + + " " + name + + ": "); + return; + } + String preview = + base64.length() > 10 + ? base64.substring(0, 10) + : base64; + getLogger().info( + "[BACKUP_DEBUG] " + label + " " + + name + ": " + preview); + } + + private ItemStack mkItem(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + private String buildIPKickMessage(Player p, + String ip) { + List> accounts = + db.getAccountDetailsByIP(ip); + StringBuilder sb = new StringBuilder(); + sb.append("§c§l======== IP注册已满 ========\n"); + sb.append("§e您的IP §f").append(ip) + .append(" §e下已注册 §f") + .append(accounts.size()) + .append(" §e个账号:\n"); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int idx = 1; + for (Map acc : accounts) { + long regTime = ((Number) acc.get( + "register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format(new java.util.Date( + regTime)) + : "未知"; + sb.append("§7").append(idx) + .append(". §f") + .append(acc.get("player_name")) + .append(" §7注册于: §e") + .append(dateStr).append("\n"); + idx++; + } + sb.append("§7-----------------------------\n"); + sb.append("§e请使用已有账号登录\n"); + sb.append("§e登录后输入 §f/oa §e提交工单\n"); + sb.append("§c§l============================"); + return sb.toString(); + } + + /** + * 读取备份,返回物品名预览 + */ + private String getBackupItemPreview( + int backupId) { + try { + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) + return "§7空"; + ItemStack[] items = + InventorySerializer + .fromBase64( + backup[0]); + if (items == null) + return "§7空"; + Map counts = + new LinkedHashMap<>(); + for (ItemStack it : items) { + if (it == null + || it.getType() + == Material.AIR) + continue; + String name = translateMaterial( + it.getType().name()); + counts.merge(name, + it.getAmount(), + Integer::sum); + } + if (counts.isEmpty()) + return "§7空"; + StringBuilder sb = + new StringBuilder(); + int shown = 0; + for (Map.Entry en + : counts.entrySet()) { + if (shown >= 5) { + sb.append(" §7..."); + break; + } + if (shown > 0) + sb.append("§7, "); + sb.append("§f").append(en.getKey()) + .append("§7x") + .append(en.getValue()); + shown++; + } + return sb.toString(); + } catch (Exception e) { + return "§7解析失败"; + } + } + + private String translateMaterial( + String name) { + switch (name) { + case "GRASS_BLOCK": + return "草方块"; + case "OAK_LOG": + return "橡木原木"; + case "OAK_PLANKS": + return "橡木木板"; + case "STONE": + return "石头"; + case "COBBLESTONE": + return "圆石"; + case "DIRT": + return "泥土"; + case "SAND": + return "沙子"; + case "GRAVEL": + return "砂砾"; + case "DIAMOND": + return "钻石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "IRON_INGOT": + return "铁锭"; + case "GOLD_INGOT": + return "金锭"; + case "COAL": + return "煤炭"; + case "COAL_ORE": + return "煤矿石"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "OAK_LEAVES": + return "橡树树叶"; + case "OAK_SAPLING": + return "橡树树苗"; + case "STICK": + return "木棍"; + case "CRAFTING_TABLE": + return "工作台"; + case "FURNACE": + return "熔炉"; + case "CHEST": + return "箱子"; + case "TORCH": + return "火把"; + case "BEDROCK": + return "基岩"; + case "TNT": + return "TNT"; + case "REDSTONE": + return "红石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "LAPIS_LAZULI": + return "青金石"; + case "EMERALD": + return "绿宝石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "OBSIDIAN": + return "黑曜石"; + case "GLOWSTONE": + return "萤石"; + case "ARROW": + return "箭"; + case "BOW": + return "弓"; + case "IRON_SWORD": + return "铁剑"; + case "DIAMOND_SWORD": + return "钻石剑"; + case "IRON_PICKAXE": + return "铁镐"; + case "DIAMOND_PICKAXE": + return "钻石镐"; + case "IRON_AXE": + return "铁斧"; + case "DIAMOND_AXE": + return "钻石斧"; + case "IRON_HELMET": + return "铁头盔"; + case "IRON_CHESTPLATE": + return "铁胸甲"; + case "IRON_LEGGINGS": + return "铁护腿"; + case "IRON_BOOTS": + return "铁靴子"; + case "DIAMOND_HELMET": + return "钻石头盔"; + case "DIAMOND_CHESTPLATE": + return "钻石胸甲"; + case "DIAMOND_LEGGINGS": + return "钻石护腿"; + case "DIAMOND_BOOTS": + return "钻石靴子"; + case "BREAD": + return "面包"; + case "COOKED_BEEF": + return "牛排"; + case "COOKED_PORKCHOP": + return "熟猪排"; + case "COOKED_CHICKEN": + return "熟鸡肉"; + case "GOLDEN_APPLE": + return "金苹果"; + case "ENDER_PEARL": + return "末影珍珠"; + case "BLAZE_ROD": + return "烈焰棒"; + case "GHAST_TEAR": + return "恶魂之泪"; + case "NETHER_STAR": + return "下界之星"; + case "SHULKER_SHELL": + return "潜影壳"; + case "ELYTRA": + return "鞘翅"; + case "TOTEM_OF_UNDYING": + return "不死图腾"; + case "TRIDENT": + return "三叉戟"; + case "ENCHANTED_GOLDEN_APPLE": + return "附魔金苹果"; + default: + // 转中文名格式:SANDSTONE -> 沙石 + return name.toLowerCase() + .replace("_", " "); + } + } + + // ===== Events ===== + @EventHandler + public void onJoin(PlayerJoinEvent e) { + + Player p = e.getPlayer(); + String name = p.getName(); + String ip = getPlayerIP(p); + // 强制重发资源包(清掉客户端拒绝记录) + Bukkit.getScheduler() + .runTaskLater(this, () -> { + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + }, 20L); + // 重置聊天输入状态(防止上次退出时残留) + chatInput.reset(p); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); +// 改完了直接打包,要打sdf1一起 + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + + boolean isBedrock = + verification.isBedrockPlayer(p); + boolean isOnlineMode = + verification.isOnlineMode(); + + // ★ 登录阶段始终允许飞行,防止悬空被踢出 + p.setAllowFlight(true); + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + } + if (!"manual".equals(config.approvalMode) + && !db.userExists(name)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + // [ADDED] 资源包(延迟发送,不强制) + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + if (isOnlineMode) { + autoLogin(p, "premium"); + return; + } + if (isBedrock) { + autoLogin(p, "bedrock"); + return; + } + if (canAutoLogin(p)) { + autoLogin(p, "ip_reconnect"); + return; + } + afk.remove(p.getUniqueId()); + afk.recordAction(p.getUniqueId()); + hideInventoryDelayed(p, 0); + // [ADDED] 发送资源包 + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + + // [ADDED] 发送资源包(延迟5秒确保客户端就绪) + // ★ 资源包:只发送一次,延迟3秒确保客户端就绪 + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } + + + + // ===== 登录连续天数更新(基于上线,非签到) ===== + if (db.userExists(name)) { + String today = new java.text.SimpleDateFormat( + "yyyy-MM-dd").format(new java.util.Date()); + Object lastLoginObj = db.getField(name, "last_login_date"); + String lastLoginDate = lastLoginObj != null ? lastLoginObj.toString() : ""; + Object streakObj = db.getField(name, "login_streak"); + int loginStreak = streakObj instanceof Number ? ((Number) streakObj).intValue() : 0; + + if (!lastLoginDate.isEmpty() && !lastLoginDate.equals(today)) { + try { + java.util.Date lastD = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(lastLoginDate); + java.util.Date todayD = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(today); + long diff = todayD.getTime() - lastD.getTime(); + if (diff > 86400000L * 2) { + loginStreak = 1; + } else if (diff > 86400000L) { + loginStreak++; + } + } catch (Exception ignored) {} + } else if (lastLoginDate.isEmpty()) { + loginStreak = 1; + } + db.setField(name, "login_streak", loginStreak); + db.setField(name, "last_login_date", today); + } + + verification.verifyPremiumAsync(p, + isPremium -> { + if (isPremium) { + Bukkit.getScheduler() + .runTask(this, + () -> autoLogin(p, + "premium")); + } else { + Bukkit.getScheduler() + .runTask(this, () -> { + joinTime.put( + p.getUniqueId(), + System.currentTimeMillis()); + if (!db.userExists(name)) + p.sendMessage(config.msg( + "not_registered")); + else + p.sendMessage(config.msg( + "not_logged_in")); + }); + } + }); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player p = e.getPlayer(); + String name = p.getName(); + chatInput.reset(p); + + boolean wasLoggedIn = + loggedIn.contains(name); + + // 判断是否需要保存: + // 1. 已登录 → 保存当前背包 + // 2. 未登录但有真实物品 → 保存 + // (插件安装前的老玩家) + // 3. 未登录且背包被插件清空 → 不保存 + // (避免覆盖有效备份) + boolean hasRealItems = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (it != null + && it.getType() + != Material.AIR + && !isMenuSnowball(it)) { + hasRealItems = true; + break; + } + } + + if (wasLoggedIn || hasRealItems) { + try { + String contents = + InventorySerializer + .toBase64( + p.getInventory() + .getContents()); + String armor = + InventorySerializer + .toBase64( + p.getInventory() + .getArmorContents()); + String extra = + InventorySerializer + .toBase64( + p.getInventory() + .getExtraContents()); + int level = p.getLevel(); + double exp = p.getExp(); + + debugPrintBase64( + "退出保存", name, + contents); + db.saveInventoryBackup(name, + contents, armor, extra, + level, exp); + } catch (Exception ex) { + getLogger().severe( + "[Sdf1_login] " + + "退出保存背包失败: " + + name); + } + } else { + getLogger().info( + "[BACKUP_DEBUG] 退出跳过 " + + name + + ": 背包为空" + + "(插件已清空)"); + } + + if (wasLoggedIn) { + loggedIn.remove(name); + db.setLoggedIn(name, false); + recordIPLogin(p); + } + + lastActivity.remove(p.getUniqueId()); + joinTime.remove(p.getUniqueId()); + joinLoc.remove(p.getUniqueId()); + needsPasswordChange.remove(name); + // [ADDED] 清除 Radio 熔断状态 + // [ADDED] 清除 Radio 熔断状态 + if (radioDL != null) + radioDL.onPlayerQuit( + p.getUniqueId()); + pendingAdminAuth.remove(name); + } + + private static final String MENU_SNOWBALL_TAG = + "sdf1_menu"; + + public void giveMenuSnowball(Player p) { + Object val = db.getField( + p.getName(), "menu_snowball"); + if (val != null) { + int on = val instanceof Number + ? ((Number) val).intValue() : 1; + if (on == 0) return; + } + + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) return; + if (isCustomMenuTrigger(it)) return; + if (isTreasureItem(it)) continue; // 跳过Sdf1_game宝箱物品 + } + + ItemStack customIcon = + menuIconMgr.getIcon(p); + if (customIcon != null) { + tagAsMenuTrigger(customIcon); + // 加外观标识 + ItemMeta cm = customIcon.getItemMeta(); + if (cm != null) { + String originalName = + cm.getDisplayName(); + if (originalName == null + || originalName.isEmpty() + || originalName.equals( + customIcon.getType() + .name())) { + originalName = "自定义物品"; + } + cm.setDisplayName( + "\u00a7e\u00a7l[菜单] \u00a7f" + + originalName); + List cmLore = + cm.hasLore() + ? new ArrayList<>(cm.getLore()) + : new ArrayList<>(); + // 避免重复添加 + if (cmLore.isEmpty() + || !cmLore.get(0).contains( + "\u00a77右键打开主菜单")) { + cmLore.add(0, + "\u00a77右键打开主菜单"); + } + cm.setLore(cmLore); + customIcon.setItemMeta(cm); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, customIcon); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), customIcon); + } + return; + } + + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "\u00a7e\u00a7l[菜单] \u00a7f右键打开主菜单"); + List lore = new ArrayList<>(); + lore.add("\u00a77右键点击打开功能菜单"); + lore.add("\u00a78" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem(slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + + /** + * 手动发放雪球菜单(绕过开关检查) + */ + public void forceGiveMenuSnowball( + Player p) { + // 只检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + private boolean isMenuSnowball(ItemStack item) { + if (item == null + || item.getType() + != Material.SNOWBALL) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null) return false; + if (!im.hasLore()) return false; + List lore = im.getLore(); + if (lore == null) return false; + for (String line : lore) { + if (line.contains(MENU_SNOWBALL_TAG)) + return true; + } + return false; + } + // 运行时初始化版本(避免静态加载问题) + private NamespacedKey getTriggerKey() { + return new NamespacedKey(this, "menu_trigger"); + } + + /** + * 给物品打上菜单触发标记 + */ + private void tagAsMenuTrigger(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + meta.getPersistentDataContainer().set( + getTriggerKey(), + PersistentDataType.STRING, + "true"); + item.setItemMeta(meta); + } + + /** + * 检查物品是否带菜单触发标记 + */ + private boolean isCustomMenuTrigger( + ItemStack item) { + if (item == null) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return meta.getPersistentDataContainer() + .has(getTriggerKey(), + PersistentDataType.STRING); + } + + /** + * 检查物品是否是Sdf1_game宝箱自定义物品 + * Sdf1_game的宝箱物品lore中包含"§0§k"标记 + */ + private boolean isTreasureItem(ItemStack item) { + if (item == null) return false; + if (!item.hasItemMeta()) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + if (!meta.hasLore()) return false; + List lore = meta.getLore(); + if (lore == null) return false; + for (String line : lore) { + // Sdf1_game宝箱物品标记格式: §0§kCUSTOM|玩家|... + if (line.contains("\u00a70\u00a7k")) + return true; + } + return false; + } + + + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + + // 冻结处理(未登录玩家不能移动) + if (isFrozen(p)) { + if (from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + e.setCancelled(true); + p.setAllowFlight(true); + } + } + + private List filterTab(List opts, String prefix) { + List r = new ArrayList<>(); + for (String o : opts) { + if (o.toLowerCase().startsWith(prefix.toLowerCase())) { + r.add(o); + } + } + return r; + } + + + /* + 雪球菜单: + 同时检测左右手 + */ + @EventHandler + public void onInteract(PlayerInteractEvent e) { + // ★ 过滤物理交互(压力板、绊线等),防止GUI被反复打开 + if (e.getAction() == org.bukkit.event.block.Action.PHYSICAL) return; + + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + ItemStack hand = p.getInventory() + .getItemInMainHand(); + ItemStack offhand = p.getInventory() + .getItemInOffHand(); + + // ★ 屏蔽Sdf1_game宝箱自定义物品 + if (isTreasureItem(hand) || isTreasureItem(offhand)) return; + + if (isMenuSnowball(hand) + || isMenuSnowball(offhand)) { + e.setCancelled(true); + gui.openMain(p); + return; + } + + // ★ 自定义菜单物品:只需PDC标记即可触发(不再额外检查DB) + if (isCustomMenuTrigger(hand) + || isCustomMenuTrigger(offhand)) { + e.setCancelled(true); + gui.openMain(p); + } + } + + + @EventHandler + public void onBlockBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + afk.recordAction(p.getUniqueId()); + // [ADDED] 任务计数 + if (questTracker != null) + questTracker.onBlockMined(p.getName(), + e.getBlock().getType().name()); + + } + + @EventHandler + public void onBlockPlace2(BlockPlaceEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) return; + afk.recordAction(p.getUniqueId()); + if (questTracker != null) + questTracker.onBlockPlaced( + p.getName(), + e.getBlock().getType().name()); + } + + @EventHandler + public void onChatTrack(AsyncPlayerChatEvent e) { + if (isFrozen(e.getPlayer())) return; + if (questTracker != null) + questTracker.onPlayerChat( + e.getPlayer().getName()); + } + + + @EventHandler + public void onBlockPlace(BlockPlaceEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + if (isFrozen(e.getPlayer())) { + e.setCancelled(true); + return; + } + ItemStack drop = + e.getItemDrop().getItemStack(); + if (isMenuSnowball(drop)) { + e.setCancelled(true); + } + // ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理 + } + + + @EventHandler + public void onPickup(PlayerPickupItemEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + // ★ 拾取物品后检测违禁品没收 + if (areaProtection != null) { + String areaName = areaProtection.getPlayerArea(p); + if (areaName != null) { + AreaProtection.AreaConfig ac = + areaProtection.getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null + && !areaProtection.isExemptFromConfiscation( + p, areaName)) { + areaProtection.handleConfiscate(p, ac); + } + } + } + } + + @EventHandler + public void onDamage(EntityDamageEvent e) { + if (e.getEntity() instanceof Player) { + if (isFrozen((Player) e.getEntity())) + e.setCancelled(true); + } + } + + @EventHandler + public void onEntityAttack( + EntityDamageByEntityEvent e) { + if (e.getDamager() instanceof Player + && isFrozen( + (Player) e.getDamager())) + e.setCancelled(true); + if (e.getEntity() instanceof Player + && isFrozen( + (Player) e.getEntity())) + e.setCancelled(true); + } + + // [ADDED] 击杀追踪 + @EventHandler + public void onEntityDeath(EntityDeathEvent e) { + if (questTracker == null) return; + Player killer = e.getEntity().getKiller(); + if (killer == null || isFrozen(killer)) return; + if (e.getEntity() instanceof Player) + questTracker.onPlayerKill(killer.getName()); + else + questTracker.onMobKill(killer.getName(), + e.getEntityType().name()); + } + + // [ADDED] 合成追踪 + @EventHandler + public void onCraft(CraftItemEvent e) { + if (e.getWhoClicked() instanceof Player) { + Player p = (Player) e.getWhoClicked(); + if (!isFrozen(p) && questTracker != null) + questTracker.onItemCrafted(p.getName()); + } + } + + // [ADDED] 钓鱼追踪 + @EventHandler + public void onFish(PlayerFishEvent e) { + Player p = e.getPlayer(); + if (p == null || isFrozen(p)) return; + if (questTracker != null + && e.getState() == PlayerFishEvent.State.CAUGHT_FISH) + questTracker.onFishCaught(p.getName()); + } + + // [ADDED] 村民交易追踪 + @EventHandler(priority = EventPriority.MONITOR, + ignoreCancelled = true) + public void onVillagerTrade(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) return; + if (questTracker == null) return; + + Inventory topInv = + e.getView().getTopInventory(); + if (topInv == null + || topInv.getHolder() == null) + return; + if (!(topInv.getHolder() + instanceof org.bukkit.entity.Villager)) + return; + + // 结果槽 = 2 + if (e.getRawSlot() != 2) return; + ItemStack result = topInv.getItem(2); + if (result == null + || result.getType() == Material.AIR) + return; + + final Player fp = p; + Bukkit.getScheduler().runTaskLater(this, + () -> { + if (fp.isOnline()) + questTracker.onVillagerTrade( + fp.getName()); + }, 2L); + } + + + + @EventHandler + public void onChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + String msg = e.getMessage(); + // ★ CDK/经济/债券输入拦截 — 必须在所有聊天处理之前 + if (this.getCDK() != null && this.getCDK().isListening(p.getName())) { + e.setCancelled(true); + this.getCDK().onChat(p, msg); + return; + } + + +// ===== 菜单聊天输入 ===== + if (getMenu().isEditing(p.getName())) { + e.setCancelled(true); + getMenu().onChat(p, msg); + return; + } + + // ===== 冻结检查 ===== + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + // ===== 聊天输入流程(改密码/绑邮箱等) ===== + if (chatInput.isInFlow(p)) { + e.setCancelled(true); + chatInput.handleInput(p, msg, this); + return; + } + + // ===== 管理员密码验证 ===== + if (pendingAdminAuth.contains(p.getName())) { + e.setCancelled(true); + pendingAdminAuth.remove(p.getName()); + if (msg.equals(config.adminPassword)) { + p.sendMessage("§a验证通过"); + p.sendMessage("§7输入的密码已验证"); + } else { + p.sendMessage("§c密码错误"); + } + return; + } + + // ===== 聊天过滤 - 开关检查 ===== + if (!chatFilter.isEnabled()) return; + + // ===== 白名单玩家 ===== + if (chatFilter.isPlayerWhitelisted( + p.getName())) return; + + // ===== 禁言检查 ===== + if (chatFilter.isMuted(p.getName())) { + e.setCancelled(true); + p.sendMessage(chatFilter.msg( + "chat_muted")); + return; + } + + // ===== URL检测 ===== + List urls = + chatFilter.extractUrls(msg); + if (urls.isEmpty()) return; + + // 过滤白名单 + List blocked = new ArrayList<>(); + for (String url : urls) { + if (!chatFilter.isWhitelisted(url)) { + blocked.add(url); + } + } + if (blocked.isEmpty()) return; + + // 取消消息 + e.setCancelled(true); + + for (String url : blocked) { + p.sendMessage(chatFilter.msg( + "chat_url_blocked", + "url", url)); + } + + // 增加违规计数 + chatFilter.incrementViolation(p.getName()); + int count = chatFilter.getViolationCount( + p.getName()); + p.sendMessage(chatFilter.msg( + "chat_url_violation", + "count", String.valueOf(count))); + + // 通知管理员 + if (chatFilter.isNotifyAdmin()) { + String notify = chatFilter.msg( + "chat_url_admin_notify", + "player", p.getName(), + "url", blocked.get(0), + "count", String.valueOf(count)); + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.hasPermission( + "sdf1.admin") + || op.isOp()) { + op.sendMessage(notify); + } + } + } + + // 通报 + if (chatFilter.isNotifyAll()) { + Bukkit.broadcastMessage( + chatFilter.msg( + "chat_url_broadcast", + "player", + p.getName())); + } + + // 处罚 + chatFilter.applyPunishment(p, count); + // ===== 补签确认 ===== + if (pendingBackCheck.containsKey( + p.getName())) { + e.setCancelled(true); + String data = pendingBackCheck + .remove(p.getName()); + String[] parts = data.split(":"); + String dateStr = parts[0]; + int totalPoints = + Integer.parseInt(parts[1]); + + if (msg.trim().equals("1") + || msg.trim() + .equalsIgnoreCase("yes") + || msg.trim().contains("是") + || msg.trim().contains("愿意")) { + int extra = totalPoints - 10; + if (extra > 0 + && !db.deductPoints( + p.getName(), extra)) { + db.addPoints(p.getName(), 10); + p.sendMessage("§c积分也不足," + + "10积分已退还"); + return; + } + checkIn.doBackCheckIn(p, dateStr); + p.sendMessage("§a补签成功!日期: " + + dateStr + " §7(扣除" + + totalPoints + + "积分,免经济)"); + playSuccessSound(p); + } else { + db.addPoints(p.getName(), 10); + p.sendMessage("§c补签已取消," + + "10积分已退还"); + } + return; + } + + } + + // 在 getter 区域添加 + public MenuManager getMenu() { + return menu; + } + public PVPManager getPVPManager() { + return pvpManager; + } + // 在 Main.java 中(与其他 getter 放一起) + public AreaProtection getAreaProtection() { + return areaProtection; + } + +// ==================== 我的钱包 ==================== + + public void openMyWallet(Player p) { + openWalletGUI(p, p.getName()); + } + + private void openWalletGUI(Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l我的钱包"); + + + ItemStack glass = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta gm = glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, glass); + + // 债券余额 (slot 4) + int balance = bondManager.getBonds(target); + g.setItem(4, mkItem(Material.GOLD_INGOT, + "§6§l债券余额: §e" + balance + " §6枚", + "§7当前持有的债券数量")); + + // 积分余额 (slot 5) + int pointsBalance = points.getPoints(target); + g.setItem(5, mkItem(Material.EMERALD, + "§6§l积分余额: §e" + pointsBalance + " §6分", + "§7当前持有的积分数量")); + + // 账户状态 (slot 10) + String status = bondManager + .getAccountStatus(target); + if ("frozen".equals(status)) { + g.setItem(10, mkItem(Material.REDSTONE, + "§c§l账户状态: 冻结中", + "§7无法进行任何资金交易")); + } else { + g.setItem(10, mkItem(Material.EMERALD, + "§a§l账户状态: 正常", + "§7账户运行正常")); + } + + // 同IP玩家 (slot 11) + if (target.equals(p.getName())) { + List sameIP = bondManager + .getSameIPPlayers(target); + List lore = new ArrayList<>(); + lore.add("§7同IP下注册的其他玩家:"); + if (sameIP.isEmpty()) { + lore.add("§a 无"); + } else { + for (String n : sameIP) + lore.add("§c - " + n); + } + lore.add("§7"); + lore.add("§7转账限制:"); + lore.add("§7 不能与同IP玩家转账"); + g.setItem(11, mkItem(Material.PAPER, + "§e§l同IP玩家", lore.toArray( + new String[0]))); + } + + // 近7天流水 (slot 12) + g.setItem(12, mkItem(Material.BOOK, + "§e近7天流水", + "§7点击查看最近7天交易记录")); + + // 近14天流水 (slot 13) + g.setItem(13, mkItem(Material.BOOK, + "§e近14天流水", + "§7点击查看最近14天交易记录", + "§8(系统最大支持14天)")); + + // 转账记录 (slot 14) + g.setItem(14, mkItem(Material.PAPER, + "§e转账记录", + "§7点击查看所有转账明细")); + + // 返回 (slot 22) + g.setItem(22, mkItem(Material.ARROW, + "§7← 返回")); + + walletViewTarget.put(p.getName(), target); + p.openInventory(g); + } + +// ==================== 流水记录GUI ==================== + + public void openWalletTransactions( + Player p, int days) { + String target = walletViewTarget.getOrDefault( + p.getName(), p.getName()); + openWalletTxGUI(p, target, days, false); + } + + public void openWalletTransfers(Player p) { + String target = walletViewTarget.getOrDefault( + p.getName(), p.getName()); + openWalletTxGUI(p, target, 14, true); + } + + private void openWalletTxGUI(Player p, + String target, + int days, + boolean transfersOnly) { + days = Math.min(days, 14); // 普通玩家最大14天 + String title = transfersOnly + ? "§e§l转账记录 - " + target + : "§e§l流水记录 - 近" + days + + "天 - " + target; + + Inventory g = Bukkit.createInventory( + null, 54, title); + + List> txs = transfersOnly + ? bondManager.getTransferTransactions( + target, days) + : bondManager.getTransactions( + target, days); + + int slot = 0; + for (Map tx : txs) { + if (slot >= 45) break; + + String type = (String) tx.get("type"); + int amount = ((Number) tx.get("amount")) + .intValue(); + String tgt = (String) tx.get("target_player"); + String reason = (String) tx.get("reason"); + int bal = ((Number) tx.get("balance_after")) + .intValue(); + long time = ((Number) tx.get("time")) + .longValue(); + + boolean isExpense = "shop_buy".equals(type) + || "shop_packing".equals(type); + boolean pos = isExpense ? false : (amount > 0); + String sign = pos ? "+" : (isExpense ? "-" : ""); + String color = pos ? "§a" : (isExpense ? "§c" : (amount < 0 ? "§c" : "")); +// 金额取绝对值显示 + int displayAmount = Math.abs(amount); + + + // 用 if-else 替代 switch 避免编译问题 + Material mat = Material.PAPER; + if (BondManager.TX_TRANSFER_OUT.equals(type)) { + mat = Material.RED_WOOL; + } else if (BondManager.TX_TRANSFER_IN + .equals(type)) { + mat = Material.LIME_WOOL; + } else if (BondManager.TX_ADMIN_GIVE + .equals(type)) { + mat = Material.GOLD_BLOCK; + } else if (BondManager.TX_ADMIN_DEDUCT + .equals(type)) { + mat = Material.REDSTONE_BLOCK; + } else if (BondManager.TX_REDEEM + .equals(type)) { + mat = Material.NAME_TAG; + } else if (BondManager.TX_SHOP_BUY + .equals(type)) { + mat = Material.CHEST; + } else if (BondManager.TX_FREEZE + .equals(type)) { + mat = Material.BARRIER; + } else if (BondManager.TX_UNFREEZE + .equals(type)) { + mat = Material.EMERALD_BLOCK; + } else if (pos) { + mat = Material.LIME_WOOL; + } else { + mat = Material.RED_WOOL; + } + String typeName = walletFormatType(type); + + List lore = new ArrayList<>(); + lore.add("§7类型: " + typeName); + lore.add("§7金额: " + color + sign + displayAmount + + " §6枚"); + + if (tgt != null && !tgt.isEmpty()) + lore.add("§7对象: §e" + tgt); + if (reason != null && !reason.isEmpty()) + lore.add("§7理由: §7" + reason); + lore.add("§7余额: §f" + bal + " §6枚"); + lore.add("§8" + TX_SDF.format( + new Date(time))); + + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName(color + sign + displayAmount + + " §6枚 - " + typeName); + + im.setLore(lore); + item.setItemMeta(im); + } + g.setItem(slot, item); + slot++; + } + + if (txs.isEmpty()) { + ItemStack empty = new ItemStack( + Material.BARRIER); + ItemMeta em = empty.getItemMeta(); + if (em != null) { + em.setDisplayName("§7暂无记录"); + empty.setItemMeta(em); + } + g.setItem(22, empty); + } else if (txs.size() > 45) { + g.setItem(49, mkItem(Material.PAPER, + "§7还有 " + (txs.size() - 45) + + " 条未显示")); + } + + g.setItem(53, mkItem(Material.ARROW, + "§7← 返回钱包")); + + p.openInventory(g); + } + + + private String walletFormatType(String type) { + if (type == null) return "未知"; + switch (type) { + case BondManager.TX_ADMIN_GIVE: + return "管理员发放"; + case BondManager.TX_ADMIN_DEDUCT: + return "管理员扣除"; + case BondManager.TX_TRANSFER_OUT: + return "转出"; + case BondManager.TX_TRANSFER_IN: + return "转入"; + case BondManager.TX_REDEEM: + return "CDK兑换"; + case BondManager.TX_SHOP_BUY: + return "商城购买"; + case BondManager.TX_DAILY_SIGN: + return "签到奖励"; + case BondManager.TX_FREEZE: + return "冻结"; + case BondManager.TX_UNFREEZE: + return "解冻"; + default: + return type; + } + } + + + // ===== Inventory Click(旧逻辑 + 新主菜单布局 + 新功能) ===== + @EventHandler + public void onInvClick(InventoryClickEvent e) { + if (e.isCancelled()) return; + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + + String title = e.getView().getTitle(); + int slot = e.getRawSlot(); + + // ===== 工单 ===== + if (title.equals(TicketManager.T_TICKET_MAIN) + || title.equals( + TicketManager.T_TICKET_CREATE) + || title.equals( + TicketManager.T_TICKET_MY) + || title.equals( + TicketManager.T_TICKET_ADMIN) + || title.equals( + TicketManager.T_TICKET_DETAIL) + || title.equals("§d§l服务商面板") + || title.equals("§6§l抢单大厅") + || title.equals( + "§b§l我处理中的工单")) { + if (e.getClickedInventory() == null) + return; + if (e.getClickedInventory() + != e.getView().getTopInventory()) + return; + e.setCancelled(true); + ticket.handleClick(p, title, slot); + return; + } + + // ===== 垃圾箱 ===== + // ===== 垃圾回收站 ===== + if (title.equals("§6§l垃圾回收站")) { + if (e.getClickedInventory() == null) + return; + + // 玩家背包普通点击 → 完全不管 + if (e.getClickedInventory() + == p.getInventory() + && !e.isShiftClick()) { + return; + } + + // Shift+Click 从玩家背包存入 + if (e.isShiftClick() + && e.getClickedInventory() + == p.getInventory() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR + && !isMenuSnowball( + e.getCurrentItem())) { + e.setCancelled(true); + long now = System.currentTimeMillis(); + Long last = garbageBusy.get( + p.getUniqueId()); + if (last != null && now - last < 200L) + return; + garbageBusy.put(p.getUniqueId(), now); + ItemStack toSave = + e.getCurrentItem().clone(); + + // ★ 宝箱物品防护(事不过三) + if (isTreasureItem(toSave)) { + UUID uid = p.getUniqueId(); + int count = treasureDiscardCount + .getOrDefault(uid, 0) + 1; + treasureDiscardCount.put(uid, count); + if (count < 3) { + // 前两次:警告 + 踢回背包 + p.sendMessage("§c§l[防护] §f该物品是宝箱自定义物品," + + "禁止投入垃圾站!" + + "§7(第" + count + "/2次警告)"); + // 物品已在toSave中克隆,原物品已被清空,踢回 + p.getInventory().addItem(toSave); + } else { + // 第三次:直接没收 + p.sendMessage("§c§l[防护] §f您多次尝试投入宝箱物品," + + "物品已被没收!"); + treasureDiscardCount.remove(uid); + } + return; + } + + garbage.saveItem(toSave); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage(p, 0); + }, 1L); + return; + } + + // GUI 内点击 → 全部取消,由 garbage.handleClick 处理 + e.setCancelled(true); + + // 功能格 49-53 + if (slot >= 49 && slot <= 53) { + garbage.handleClick(p, title, slot); + return; + } + + // 光标有物品 → 存入 + if (slot < 54 + && e.getCursor() != null + && e.getCursor().getType() + != Material.AIR + && !isMenuSnowball( + e.getCursor())) { + e.setCancelled(true); + ItemStack toSave = + e.getCursor().clone(); + p.getOpenInventory().setCursor(null); + + // ★ 宝箱物品防护(事不过三) + if (isTreasureItem(toSave)) { + UUID uid = p.getUniqueId(); + int count = treasureDiscardCount + .getOrDefault(uid, 0) + 1; + treasureDiscardCount.put(uid, count); + if (count < 3) { + // 前两次:警告 + 踢回背包 + p.sendMessage("§c§l[防护] §f该物品是宝箱自定义物品," + + "禁止投入垃圾站!" + + "§7(第" + count + "/2次警告)"); + p.getInventory().addItem(toSave); + } else { + // 第三次:直接没收 + p.sendMessage("§c§l[防护] §f您多次尝试投入宝箱物品," + + "物品已被没收!"); + treasureDiscardCount.remove(uid); + } + return; + } + + garbage.saveItem(toSave); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage(p, 0); + }, 2L); + return; + } + + // 点击有 ID 的物品 → 取出 + if (slot < 54) { + ItemStack inSlot = + e.getInventory().getItem(slot); + if (inSlot != null + && inSlot.hasItemMeta() + && inSlot.getItemMeta() + .hasLore()) { + List lore = + inSlot.getItemMeta().getLore(); + if (lore != null && !lore.isEmpty() + && lore.get(0).startsWith( + "§7ID: #")) { + try { + String numStr = lore.get(0) + .replace( + "§7ID: #", "") + .trim(); + int dbId = + Integer.parseInt( + numStr); + if (garbage.removeItem( + dbId)) { + ItemStack give = + inSlot.clone(); + ItemMeta gm = + give.getItemMeta(); + if (gm == null) + gm = Bukkit + .getItemFactory() + .getItemMeta( + give + .getType()); + gm.setLore(null); + give.setItemMeta(gm); + p.getInventory() + .addItem(give); + e.getInventory() + .setItem( + slot, null); + } + } catch (Exception ex) { + p.sendMessage( + "§c取出失败"); + } + } + } + } + return; + } + + + // ===== 二级菜单(.txt)—— 移到前面 ===== + if (title.startsWith("§6§l") + && !title.equals(GUIManager.T_MAIN) + && !title.equals(GUIManager.T_ADMIN) + && !title.equals(GUIManager.T_MY_INFO) + && !title.equals(GUIManager.T_INVITE) + && !title.equals( + GUIManager.T_TASK_CENTER) + && !title.equals( + GUIManager.T_GIFT_STAGES) + && !title.equals("§6§l任务面板") + && !title.equals( + "§6§l垃圾回收站")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + // ★ bounds检查 ★ + if (slot < 45 + && slot < e.getInventory() + .getSize() + && e.getCurrentItem() != null) { + ItemMeta subLm = + e.getCurrentItem().getItemMeta(); + if (subLm != null + && subLm.getLore() != null) { + for (String subLn + : subLm.getLore()) { + if (subLn.contains( + "§7指令: §f")) { + String cmd = + subLn.replace( + "§7指令: §f", + "").trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals( + "null")) { + p.sendMessage( + "§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) + cmd = cmd.substring(1); + Bukkit.dispatchCommand( + p, cmd); + return; + } + } + } + } + return; + } + + // ===== [DEBUG] 标题追踪 ===== + { + String dt = title; + if (dt.contains("钱包") + || dt.contains("余额") + || dt.contains("流水") + || dt.contains("转账")) { + getLogger().info( + "[DEBUG-CLICK] " + + "player=" + p.getName() + + " title=[" + dt + "]" + + " slot=" + slot + + " codePoints=" + + debugCodePoints(dt)); + } + } + + // ===== 二级菜单(.txt)—— 排除所有固定GUI ===== + if (title.startsWith("§6§l") + && !title.equals(GUIManager.T_MAIN) + && !title.equals(GUIManager.T_ADMIN) + && !title.equals(GUIManager.T_MY_INFO) + && !title.equals(GUIManager.T_INVITE) + && !title.equals( + GUIManager.T_TASK_CENTER) + && !title.equals( + GUIManager.T_GIFT_STAGES) + && !title.equals("§6§l任务面板") + && !title.equals("§6§l垃圾回收站") + && !title.equals("§6§l我的钱包") + && !title.equals("§6§l余额操作")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot < 45 + && slot < e.getInventory() + .getSize() + && e.getCurrentItem() != null) { + ItemMeta subLm = e.getCurrentItem() + .getItemMeta(); + if (subLm != null + && subLm.getLore() != null) { + for (String subLn + : subLm.getLore()) { + if (subLn.contains( + "§7指令: §f")) { + String cmd = subLn + .replace("§7指令: §f", "") + .trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals("null")) { + p.sendMessage("§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) { + cmd = cmd.substring(1); + } + Bukkit.dispatchCommand(p, cmd); + return; + } + } + } + } + return; + } + + // ===== 我的钱包(唯一handler) ===== + if (title.equals("§6§l我的钱包")) { + e.setCancelled(true); + if (slot == 22) { + gui.openMain(p); + } else if (slot == 12) { + openWalletTransactions(p, 7); + } else if (slot == 13) { + openWalletTransactions(p, 14); + } else if (slot == 14) { + openWalletTransfers(p); + } + return; + } + + // ===== 流水/转账记录(唯一handler) ===== + if (title.startsWith("§e§l流水记录") + || title.startsWith("§e§l转账记录")) { + e.setCancelled(true); + if (slot == 53) { + openMyWallet(p); + } + return; + } + + // ===== 余额操作(唯一handler) ===== + if (title.equals("§d§l余额操作")) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot >= 0 + && slot < e.getInventory().getSize()) { + ItemStack item = e.getInventory() + .getItem(slot); + if (item == null + || !item.hasItemMeta() + || !item.getItemMeta() + .hasDisplayName()) { + return; + } + // 在 slot == 49 的 return 之后、读取 item 之前 + // ItemStack item = e.getInventory().getItem(slot); + if (item == null) return; + Material mat = item.getType(); +// 以下是常见的装饰性玻璃板材质,按需扩展 + if (mat.name().contains("STAINED_GLASS") + || mat.name().contains("GLASS_PANE") + || mat == Material.LIGHT_BLUE_STAINED_GLASS_PANE + || mat == Material.GRAY_STAINED_GLASS_PANE + // ... 其他装饰物 + || mat == Material.BLACK_STAINED_GLASS_PANE) { + e.setCancelled(true); + return; // 装饰物品,不处理 + } + + String targetName = item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", ""); + if (targetName.equals(p.getName())) { + p.sendMessage("§c不能操作自己"); + return; + } + if (e.isLeftClick()) { + balanceTarget = targetName; + balanceGiveMode = true; + p.closeInventory(); + p.sendMessage( + "§e请输入金额和理由"); + p.sendMessage( + "§7例: +100 给款 / -100 扣款"); + } else if (e.isRightClick()) { + if (this.getCDK() != null) { + this.getCDK().requestInput( + p, "bond", + targetName); + } + p.closeInventory(); + p.sendMessage( + "§e【债券操作】对 §a" + + targetName + + " §e进行债券操作"); + p.sendMessage( + "§e输入 +金额 给债券," + + " -金额 扣债券"); + p.sendMessage( + "§7可附加理由: §a+100 " + + "活动奖励 §7不填默认:" + + "管理员手动调整"); + } + } + return; + } + + + + +// ===== 菜单管理 ===== + if (title.equals("§c§l菜单管理")) { + e.setCancelled(true); + if (slot == 26) { + gui.openMain(p); + } else if (slot == 22) { + gui.openEditor(p, -1); + } else if (slot >= 0 && slot < 9) { + List items = + getMenu().getItems(); + if (slot < items.size()) { + if (e.isLeftClick()) { + gui.openEditor(p, slot); + } else if (e.isRightClick()) { + MenuManager.MenuItem mi = + items.get(slot); + if ("OP".equals(mi.permType)) { + mi.permType = + "玩家"; + } else { + mi.permType = "OP"; + } + gui.openMenuManager(p); + p.sendMessage( + "§a" + mi.title + + " §7权限切换为: §e" + + mi.permType); + } + } + } + return; + } + + // ===== 全局 slot 检查(放后面) ===== + if (e.getClickedInventory() == null) return; + if (e.getClickedInventory() + != e.getView().getTopInventory()) + return; + if (slot < 0 || slot >= e.getInventory() + .getSize()) return; + if (e.getCurrentItem() == null) return; + + + // ==================== 群系传送(新) ==================== + if (title.equals(GUIManager.T_BIOME)) { + e.setCancelled(true); + if (slot == 49) { + gui.openMain(p); + return; + } + if (slot < 12) { + String[] cats = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沼泽", "河流", + "地下", "蘑菇岛", "下界", "末地" + }; + p.closeInventory(); + gui.teleportToBiome(p, cats[slot]); + } + return; + } + + // ===== 编辑器 ===== + if (title.equals(GUIManager.T_EDITOR)) { + e.setCancelled(true); + if (slot >= 27) return; + List el = + getMenu().getItems(); + int ei = el.size() - 1; + if (ei < 0) return; + MenuManager.MenuItem em = el.get(ei); + switch (slot) { + case 10: + getMenu().editTitle(p, ei); + break; + case 11: + getMenu().editCommand(p, ei); + break; + case 20: + em.permType = + "玩家".equals(em.permType) + ? "OP" : "玩家"; + gui.openEditor(p, ei); + break; + // 编辑器 case 22(保存) + case 22: + if (getMenu() != null) { + MenuManager.MenuItem last = + getMenu().getItems().get(ei); + if (last.command == null + || last.command.trim().isEmpty() + || last.command.trim() + .equals("/")) { + p.sendMessage( + "§c指令不能为空,请先填写指令"); + gui.openEditor(p, ei); + return; + } + getMenu().saveMenu(); + p.closeInventory(); + p.sendMessage("§a已保存!"); + gui.openMain(p); + } + break; + + case 24: + getMenu().loadMenu(); + // ★ 如果最后一项是空指令的新项,删掉 ★ + List cancelItems = + getMenu().getItems(); + if (!cancelItems.isEmpty()) { + MenuManager.MenuItem last = + cancelItems + .get(cancelItems.size() - 1); + if ((last.command == null + || last.command.trim().isEmpty() + || last.command.trim().equals("/")) + && "新菜单项" + .equals(last.title)) { + cancelItems.remove( + cancelItems.size() - 1); + } + } + p.closeInventory(); + p.sendMessage("§c已取消"); + gui.openMain(p); + break; + } + } + // ==================== 主菜单(新布局) ==================== + // ==================== 主菜单(新布局) ==================== + if (title.equals(GUIManager.T_MAIN)) { + e.setCancelled(true); + + // ★ 统一:所有自定义菜单项通过 lore 识别 ★ + if (e.getCurrentItem() != null) { + ItemMeta meta = e.getCurrentItem() + .getItemMeta(); + if (meta != null && meta.getLore() != null) { + for (String ln : meta.getLore()) { + if (ln.contains("§7指令: §f")) { + String cmd = ln.replace( + "§7指令: §f", "").trim(); + if (cmd.isEmpty() + || cmd.equals("/") + || cmd.equals("null")) { + p.sendMessage("§c指令为空"); + return; + } + p.closeInventory(); + if (cmd.startsWith("/")) { + cmd = cmd.substring(1); + } + if (cmd.endsWith(".txt")) { + gui.openSubMenu(p, cmd); + } else { + Bukkit.dispatchCommand(p, cmd); + } + return; + } + } + } + } + + // 固定按钮 + if (slot == 10) { + gui.openMyInfo(p); + } else if (slot == 12) { + gui.openInvite(p); + } else if (slot == 14) { + p.openInventory(points.createShopGUI(p)); + } else if (slot == 16) { + gui.openBiomeMenu(p); + } else if (slot == 19) { + ticket.openMain(p); + } else if (slot == 20) { + openMyWallet(p); + } else if (slot == 21) { + dailySignWithReward(p); + gui.openMain(p); + } else if (slot == 23) { + gui.openTaskCenter(p); + } else if (slot == 25) { + if (p.isOp() || isAdmin(p)) { + garbage.openRecycle(p); + } + } else if (slot == 29) { + if (isAdmin(p)) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType.ADMIN_AUTH; + p.sendMessage("请输入管理密码:"); + } + } else if (slot == 31) { + if (p.isOp()) { + gui.openMenuManager(p); + } + } else if (slot == 34) { + if (p.isOp() || isAdmin(p)) { + gui.openBalanceOps(p); + } + } + + + return; + } + + // ==================== 新人礼包 ==================== + if (title.equals(GUIManager.T_GIFT_STAGES)) { + e.setCancelled(true); + if (slot == 26) { + gui.openTaskCenter(p); + } else if (slot >= 10 && slot <= 18) { + QuestTracker qt = getQuestTracker(); + if (qt != null) { + List quests = + qt.getQuests("新人任务"); + int idx = slot - 10; + if (idx >= 0 && idx < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(idx); + String pn = p.getName(); + if (qt.hasClaimed(pn, qf)) { + p.sendMessage( + "§c该阶段奖励已领取"); + } else if (!qt.isStageCompleted( + pn, qf)) { + p.sendMessage("§c条件未满足"); + } else { + qt.claimRewards(p, qf); + qt.markClaimed(pn, qf); + gui.openGiftStages(p); + playSuccessSound(p); + } + } + } else { + int stage = slot - 9; + if (gift.canClaim(p, stage)) { + gift.claimReward(p, stage); + gui.openGiftStages(p); + } else { + p.sendMessage(config.msg( + "gift_not_ready")); + } + } + } + return; + } + + // ==================== 主线/支线任务 ==================== + if (title.equals("§d§l主线任务") + || title.equals("§d§l支线任务")) { + e.setCancelled(true); + if (slot == e.getInventory().getSize() - 1) { + gui.openTaskCenter(p); + } else if (questTracker != null) { + String cat = title.contains("主线") + ? "主线任务" : "支线任务"; + List quests = + questTracker.getQuests(cat); + if (slot < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(slot); + if (!questTracker.isStageCompleted( + p.getName(), qf)) { + p.sendMessage("§c条件未满足"); + } else if (questTracker.hasClaimed( + p.getName(), qf)) { + p.sendMessage("§c已领取过"); + } else { + questTracker.claimRewards(p, qf); + questTracker.markClaimed( + p.getName(), qf); + gui.openTaskList(p, cat); + playSuccessSound(p); + } + } + } + return; + } + + // ==================== 我的信息 ==================== + if (title.equals(GUIManager.T_MY_INFO)) { + e.setCancelled(true); + // ★ 只在图标槽(slot 4)调用,不干扰按钮 + if (slot == 4 + && gui.handleMyInfoIconClick(p, e)) { + return; + } + if (slot == 26) gui.openMain(p); + + else if (slot == 10) { + if (checkIn.isCheckedInToday( + p.getName())) + p.sendMessage( + config.msg("checkin_already")); + else { + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn( + p.getName()); + playSuccessSound(p); + gui.openMyInfo(p); + } + } else if (slot == 11) { + chatInput.getState(p).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p.closeInventory(); + p.sendMessage("§e请输入当前密码:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .BACK_CHECKIN; + p.closeInventory(); + p.sendMessage("§e请输入补签日期:"); + } else if (slot == 13) { + String code = (String) db.getField( + p.getName(), "invite_code"); + if (code == null || code.isEmpty()) + code = invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + } else if (slot == 14) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SET_EMAIL; + p.closeInventory(); + p.sendMessage("§e请输入邮箱:"); + } else if (slot == 15) { + p.openInventory( + points.createShopGUI(p)); + } + return; + } + + + // ==================== 管理面板 ==================== + if (title.equals(GUIManager.T_ADMIN)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SMTP_CONFIG; + p.closeInventory(); + p.sendMessage("§e当前SMTP: " + + config.getSmtp("smtp地址")); + p.sendMessage( + "§e输入新地址(0跳过):"); + } else if (slot == 11) + gui.openUserManagement(p); + else if (slot == 12) gui.openMyInfo(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) { + if (e.getClick().isLeftClick()) { + config.afkEnabled = + !config.afkEnabled; + p.sendMessage(config.msg( + config.afkEnabled + ? "afk_set_enabled" + : "afk_set_disabled")); + config.saveSettings(); + gui.openAdmin(p); + } else if (e.getClick() + .isRightClick()) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_AFK_TIME; + p.sendMessage( + "§e输入挂机时长(分钟):"); + } + } + return; + } + + // ==================== 任务中心 ==================== + if (title.equals(GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) { + if (questTracker != null) { + Location loc = p.getLocation(); + String result = + questTracker + .checkInAtPosition( + p.getName(), + p.getWorld() + .getName(), + loc.getX(), + loc.getY(), + loc.getZ()); + p.sendMessage(result); + playSuccessSound(p); + } + } else if (slot == 14) + gui.openTaskList(p, "主线任务"); + else if (slot == 16) + gui.openTaskList(p, "支线任务"); + else if (slot == 18) + ticket.openMain(p); + else if (slot == 20) { + dailySignWithReward(p); + gui.openMain(p); + } + return; + } + + // ==================== 邀请 ==================== + if (title.equals(GUIManager.T_INVITE)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + String code = + invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + gui.openInvite(p); + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .INVITE_INPUT_CODE; + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } + return; + } + + // ==================== 用户管理 ==================== + if (title.equals(GUIManager.T_USER_MGMT)) { + e.setCancelled(true); + if (slot == 53) gui.openAdmin(p); + else if (slot >= 0 && slot < 45) { + ItemStack item = + e.getInventory().getItem(slot); + if (item != null && item.hasItemMeta() + && item.getItemMeta() + .hasDisplayName()) + gui.openUserDetail(p, + item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", "")); + } + return; + } + + // ==================== 用户详情 ==================== + if (title.startsWith("§e§l管理: ")) { + e.setCancelled(true); + String tgt = title.substring( + "§e§l管理: ".length()); + if (slot == 22) gui.openUserManagement(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_POINTS; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage( + "§e输入" + tgt + "的新积分:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_PWD; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage( + "§e输入" + tgt + "的临时密码:"); + } else if (slot == 14) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) { + forceGiveMenuSnowball(tp); + p.sendMessage( + "§a已发放雪球菜单给 " + + tgt); + } else { + p.sendMessage("§c玩家不在线"); + } + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_DELETE_CONFIRM; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage(config.msg( + "admin_delete_confirm")); + } + return; + } + + // ==================== 积分商城 ==================== + if (title.startsWith("§d§l积分商城")) { + e.setCancelled(true); + if (slot == 49) gui.openMain(p); + else if (slot == 50 && isAdmin(p)) + gui.openShopAdmin(p); + else points.handleClick(p, slot); + return; + } + + // ==================== 商城管理 ==================== + if (title.equals("§c§l商城管理")) { + e.setCancelled(true); + if (slot == 49) + p.openInventory( + points.createShopGUI(p)); + return; + } + + // ==================== CY背包商城 ==================== + if (title.startsWith("§d§lCY背包商城")) { + e.setCancelled(true); + int size = e.getInventory().getSize(); + if (slot == size - 1) + p.openInventory( + points.createShopGUI(p)); + else points.handleCYClick(p, slot); + return; + } + + // ==================== 任务面板 ==================== + if (title.equals("§6§l任务面板")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 11) { + Location loc = p.getLocation(); + String result = + questTracker.checkInAtPosition( + p.getName(), + p.getWorld().getName(), + loc.getX(), loc.getY(), + loc.getZ()); + p.sendMessage(result); + playSuccessSound(p); + openTaskPanel(p); + } else if (slot == 15) { + gui.openGiftStages(p); + } + return; + } + + // ==================== 账号请求面板 ==================== + if (title.equals("§e§l账号请求")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + p.closeInventory(); + p.sendMessage("§c§l[确认] §f请输入:"); + p.sendMessage("§e/sdf1_login del " + + p.getName()); + } else if (slot == 13) { + ticket.openCreate(p); + } else if (slot == 16) { + p.closeInventory(); + loginMgr.handleReset(p); + } + } + } + + /* + * 签到 + 盲盒经济奖励 + */ + public void dailySignWithReward(Player p) { + String name = p.getName(); + + if (checkIn.isCheckedInToday(name)) { + p.sendMessage( + config.msg("checkin_already")); + return; + } + + // ===== 1. 执行签到(内部已加积分) ===== + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(name); + + // ===== 2. 获取连续天数 ===== + int streak = checkIn.getStreak(name); + double multiplier = 1.0; + if (streak >= 21) { + multiplier = 1.3; + } else if (streak >= 14) { + multiplier = 1.2; + } else if (streak >= 7) { + multiplier = 1.1; + } + + // ===== 3. 经济奖励(不再重复加积分) ===== + if (economy == null) { + playSuccessSound(p); + return; + } + + // 固定金额模式 + if ("fixed".equals( + config.checkinRewardType) + && config.checkinRewardFixed > 0) { + final double reward = + config.checkinRewardFixed; + final double m = multiplier; + p.sendMessage("§e§l[签到] §f" + + "正在生成今日奖励..."); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + double rewardFinal = + Math.round( + reward * m + * 100.0) + / 100.0; + double before = + economy + .getBalance(p); + economy.depositPlayer( + p, rewardFinal); + double after = + economy + .getBalance(p); + p.sendMessage( + "§6§l[签到] §f" + + "获得奖励: §a§l$" + + String.format( + "%.2f", + rewardFinal)); + p.sendMessage("§7余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", after)); + playSuccessSound(p); + }, 30L); + return; + } + + // 盲盒范围模式 + if ("range".equals( + config.checkinRewardType) + && config.checkinRewardMin > 0 + && config.checkinRewardMax + > config.checkinRewardMin) { + final double min = + config.checkinRewardMin; + final double max = + config.checkinRewardMax; + final double m = multiplier; + getLogger().info( + "[签到] 盲盒: min=" + + min + " max=" + max + + " multi=" + m); + p.sendMessage("§e§l[签到盲盒] " + + "§f正在生成今日惊喜..."); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + double rand = + Math.random(); + double reward = min + + rand + * (max - min); + reward = Math.round( + reward * m * 100.0) + / 100.0; + getLogger().info( + "[签到] 盲盒结果: rand=" + + String.format( + "%.4f", rand) + + " reward=" + + reward); + double before = + economy + .getBalance(p); + economy.depositPlayer( + p, reward); + double after = + economy + .getBalance(p); + p.sendMessage( + "§6§l====== 签到盲盒 ======"); + p.sendMessage( + "§7开出: §a§l$" + + String.format( + "%.2f", + reward)); + p.sendMessage("§7余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", after)); + p.sendMessage( + "§6§l========================"); + playSuccessSound(p); + }, 40L); + return; + } + + // 无经济奖励 + playSuccessSound(p); + } + + public BondManager getBondManager() { + return bondManager; + } + + + @EventHandler + public void onVillagerTrade( + org.bukkit.event.inventory.TradeSelectEvent e) { + // TradeSelectEvent 在玩家点击交易时触发 + if (!(e.getWhoClicked() instanceof Player)) return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p) || questTracker == null) return; + questTracker.onVillagerTrade(p.getName()); + } + + @EventHandler + public void onInvDrag(InventoryDragEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + String t = e.getView().getTitle(); +// 禁止T_MY_INFO拖拽,只允许点击操作 + // 我的信息:允许拖入图标槽(31) + if (GUIManager.T_MY_INFO.equals( + e.getView().getTitle())) { + boolean toIconSlot = false; + for (int raw : e.getRawSlots()) { + if (raw == 31) { + toIconSlot = true; + break; + } + } + if (toIconSlot) { + ItemStack dragged = + e.getOldCursor().clone(); + String b64 = gui.serializeItem(dragged); + if (b64 != null) { + db.saveMenuIcon(p.getName(), b64, + dragged.getType().name()); + } + p.sendMessage("§a已保存菜单图标: " + + dragged.getType().name()); + // 刷新GUI显示 + Bukkit.getScheduler() + .runTaskLater(this, () -> + gui.openMyInfo(p), 2L); + } + e.setCancelled(true); + return; + } + + + // 回收站拖拽 + if (t.equals("§6§l垃圾回收站")) { + boolean toGUI = false; + for (int rawSlot : e.getRawSlots()) { + if (rawSlot < 54) { + toGUI = true; + break; + } + } + if (toGUI) { + ItemStack cur = e.getCursor(); + if (cur != null + && cur.getType() != Material.AIR + && !isMenuSnowball(cur)) { + e.setCancelled(true); + p.getOpenInventory().setCursor(null); + garbage.saveItem(cur.clone()); + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage( + p, + garbage + .getRecyclePage( + p + .getName())); + }, 2L); + return; + } + } + return; + } + + + + //服务商工单系统 + if (t.equals(TicketManager.T_TICKET_MAIN) + || t.equals( + TicketManager.T_TICKET_CREATE) + || t.equals( + TicketManager.T_TICKET_MY) + || t.equals( + TicketManager.T_TICKET_ADMIN) + || t.equals( + TicketManager.T_TICKET_DETAIL) + || t.equals("§d§l服务商面板") + || t.equals("§6§l抢单大厅") + || t.equals( + "§b§l我处理中的工单") + || t.equals("§6§l我的历史工单") + || t.equals("§c§l商城管理")) { + e.setCancelled(true); + } + } + /** 验证玩家是否在主库中真实存在 */ + public boolean isRealPlayer(String name) { + if (name == null || name.isEmpty()) return false; + try { + Map user = + getDb().getUser(name); + return user != null && !user.isEmpty() + && user.containsKey("player_name"); + } catch (Exception e) { + return false; + } + } + + // ===== Commands ===== + @Override + public boolean onCommand(CommandSender sender, + Command cmd, String label, + String[] args) { + + String cmdName = cmd.getName().toLowerCase(); + // 区域防护(必须在最前面) + if (cmd.getName().equalsIgnoreCase("protect") + || cmd.getName().equals("区域保护")) { + if (areaProtection == null) { + sender.sendMessage("§c区域防护未初始化"); + return true; + } + if (args.length == 0) { + sender.sendMessage("§a§l===== 区域防护 ====="); + sender.sendMessage("§e/protect 工具 §7获取选区工具"); + sender.sendMessage("§e/protect 创建 <区域名> §7创建区域"); + sender.sendMessage("§e/protect 列表 §7查看所有区域"); + sender.sendMessage("§e/protect 重载 §7重载配置"); + sender.sendMessage("§e/protect add <玩家> §7全局加白"); + sender.sendMessage("§e/protect add <区域> <玩家> §7区域加白"); + sender.sendMessage("§e/protect remove <玩家> §7全局删白"); + sender.sendMessage("§e/protect remove <区域> <玩家> §7区域删白"); + sender.sendMessage("§e/protect additem <物品ID> §7全局加黑"); + sender.sendMessage("§e/protect additem <区域> <物品ID> §7区域加黑"); + sender.sendMessage("§e/protect removeitem <物品ID> §7全局删黑"); + sender.sendMessage("§e/protect removeitem <区域> <物品ID> §7区域删黑"); + sender.sendMessage("§e/protect list §7全局白名单"); + sender.sendMessage("§e/protect list <区域> §7区域白名单"); + sender.sendMessage("§e/protect listitem §7全局物品黑名单"); + sender.sendMessage("§e/protect listitem <区域> §7区域物品黑名单"); + sender.sendMessage("§e/protect expand [格数] §7扩建选区"); + sender.sendMessage("§e/protect contraction [格数] §7收缩选区"); + sender.sendMessage("§e/protect addname <区域> <名字> §7添加和平白名单生物"); + sender.sendMessage("§e/protect removename <区域> <名字> §7移除和平白名单生物"); + sender.sendMessage("§e/protect addwhite <玩家> §7添加模式排除玩家"); + sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家"); + sender.sendMessage("§e/protect listname <区域> §7查看和平白名单"); + sender.sendMessage("§e/protect listwhite §7查看模式排除名单"); + + return true; + } + // 其他子命令交给 areaProtection + try { + return areaProtection.handleCommand(sender, args); + } catch (Exception e) { + sender.sendMessage("§c执行出错: " + e.getMessage()); + e.printStackTrace(); + return true; + } + } + + + if (cmd.getName().equalsIgnoreCase("shop") + || cmd.getName().equals("商店")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可使用"); + return true; + } + if (shopManager != null) { + shopManager.openShopMain((Player) sender); + } else { + sender.sendMessage("§c商店系统未加载"); + } + return true; + } + + String sub = (args.length > 0) ? args[0].toLowerCase() : ""; + +// ===== 在 onCommand 方法体内,最开头加: ===== + if (cmd.getName().equalsIgnoreCase("sdf1_login") + && args.length > 0 + && args[0].equalsIgnoreCase("shop")) { + if (shopManager == null) { + sender.sendMessage("§c商店未加载"); + return true; + } + return shopManager.handleCommand(sender, args); + } + // set + if (sub.equals("set")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage("§e用法: /sdf1_login set <玩家> <临时密码>"); + return true; + } + String tgt = args[1]; + String tempPwd = args[2]; + if (!db.userExists(tgt)) { + sender.sendMessage("§c玩家不存在"); + return true; + } + String salt = (String) db.getField(tgt, "password_salt"); + String hash = PasswordUtils.hash(tempPwd, salt); + db.setField(tgt, "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + 300000L); + db.setField(tgt, "temp_pw_used", 0); + + // ★ 立即发邮件(踢人之前) + String emailAddr = (String) db.getField(tgt, "email"); + if (emailAddr != null && !emailAddr.isEmpty()) { + // 获取最近登录信息 + Object lastLoginObj = db.getField(tgt, "last_login_time"); + long lastLogin = lastLoginObj != null + ? ((Number) lastLoginObj).longValue() : 0; + String lastIP = (String) db.getField(tgt, "last_ip"); + if (lastIP == null || lastIP.isEmpty()) + lastIP = "未知"; + + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String loginTimeStr = lastLogin > 0 + ? sdf.format(new java.util.Date(lastLogin)) + : "未知时间"; + + final String to = emailAddr; + final String fName = tgt; + final String pwd = tempPwd; + final String ipInfo = lastIP; + final String timeInfo = loginTimeStr; + final Main self = this; + + Bukkit.getScheduler().runTaskAsynchronously(this, + () -> { + // ★ 发送临时密码邮件(带登录信息) + boolean sent = email.sendTempPassword( + to, fName, pwd); + // ★ 额外发送安全通知邮件 + if (sent) { + String body = "玩家 " + fName + + ":\n\n" + + "管理员已为您的账号设置了临时密码。\n\n" + + "临时密码: " + pwd + "\n" + + "过期时间: 5分钟内\n\n" + + "最近登录记录:\n" + + " 时间: " + timeInfo + "\n" + + " IP地址: " + ipInfo + "\n\n" + + "如果您本人未操作,请忽略此邮件。\n" + + "如果您怀疑账号被盗,请立即修改密码。"; + email.sendBody(to, + "【安全通知】账号临时密码已设置", body); + } + Bukkit.getScheduler().runTask(self, () -> { + if (sent) { + sender.sendMessage("§a临时密码已发送至 " + + tgt + " 的邮箱"); + } + }); + }); + } else { + sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知"); + } + + // 踢人 + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c管理员已设置了临时密码,请用新密码登录"); + sender.sendMessage("§a已为 " + tgt + " 设置临时密码"); + return true; + } + + + if (label.equalsIgnoreCase("pvp")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + return pvpManager.onCommand(p, args); + } + + if (cmdName.equals("cypay")) { + if (args.length > 0) { + String target = args[0]; + if (!getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + } + return cypayCommand.onCommand( + sender, cmd, label, args); + } + + + + // ===== /printer [玩家名] ===== + if (label.equalsIgnoreCase("printer")) { + if (bondPrinter == null) { + sender.sendMessage("§cPrinter未初始化"); + return true; + } + if (!sender.isOp() + && !isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length == 0) { + // 打印全服 + sender.sendMessage( + "§e正在导出全服流水..."); + bondPrinter.printAll(sender); + return true; + } + if (args.length == 1) { + // 打印指定玩家 + String target = args[0]; + sender.sendMessage("§e正在导出 " + + target + " 的流水..."); + bondPrinter.printPlayer( + target, sender); + return true; + } + sender.sendMessage( + "§7用法: /printer [玩家名]"); + return true; + } + // ===== /chat 独立命令 ===== + if (cmdName.equals("chat") + || cmdName.equals("聊天")) { + if (sender instanceof Player) { + Player chatP = (Player) sender; + if (!chatP.getScoreboardTags() + .contains(config.adminTag)) { + sender.sendMessage("§c权限不足"); + return true; + } + } + if (args.length < 1) { + sender.sendMessage( + "§e/chat "); + return true; + } + String cs = args[0].toLowerCase(); + if (cs.equals("reload")) { + chatFilter.loadConfig(); + sender.sendMessage("§a聊天配置已重载"); + return true; + } + if (cs.equals("开") || cs.equals("on")) { + chatFilter.setEnabled(true); + chatFilter.saveConfig(); + sender.sendMessage("§aURL过滤已开启"); + return true; + } + if (cs.equals("关") || cs.equals("off")) { + chatFilter.setEnabled(false); + chatFilter.saveConfig(); + sender.sendMessage("§cURL过滤已关闭"); + return true; + } + if (cs.equals("add")) { + if (args.length < 2) { + sender.sendMessage("§e/chat add "); + return true; + } + chatFilter.addUrl(args[1]); + sender.sendMessage("§a已添加白名单: " + args[1]); + return true; + } + if (cs.equals("remove")) { + if (args.length < 2) { + sender.sendMessage("§e/chat remove "); + return true; + } + if (chatFilter.removeUrl(args[1])) + sender.sendMessage("§c已移除: " + args[1]); + else + sender.sendMessage("§c未找到: " + args[1]); + return true; + } + if (cs.equals("addplayer")) { + if (args.length < 2) { + sender.sendMessage("§e/chat addplayer <玩家>"); + return true; + } + chatFilter.addWhitelistPlayer(args[1]); + sender.sendMessage("§a白名单玩家: " + args[1]); + return true; + } + if (cs.equals("takeplayer")) { + if (args.length < 2) { + sender.sendMessage("§e/chat takeplayer <玩家>"); + return true; + } + if (chatFilter.removeWhitelistPlayer(args[1])) + sender.sendMessage("§c已移除白名单: " + args[1]); + else + sender.sendMessage("§c未找到: " + args[1]); + return true; + } + if (cs.equals("unmute")) { + if (args.length < 2) { + sender.sendMessage("§e/chat unmute <玩家>"); + return true; + } + chatFilter.unmutePlayer(args[1]); + sender.sendMessage("§a已解禁: " + args[1]); + return true; + } + if (cs.equals("reset")) { + if (args.length < 2) { + sender.sendMessage("§e/chat reset <玩家>"); + return true; + } + chatFilter.resetPlayer(args[1]); + sender.sendMessage("§a已重置: " + args[1]); + return true; + } + sender.sendMessage("§c未知子命令"); + return true; + } + if (label.equalsIgnoreCase("sdf1_login") + && args.length >= 2 + && args[0].equalsIgnoreCase("give")) { + if (!sender.isOp()) { + sender.sendMessage("§c仅OP可用"); + return true; + } + Player target = + Bukkit.getPlayer(args[1]); + if (target == null || !target.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + args[1]); + return true; + } + giveMenuSnowball(target); + sender.sendMessage("§a已发放菜单雪球给 " + + target.getName()); + target.sendMessage( + "§a管理员已发放菜单雪球给你"); + return true; + } + + if (cmdName.equals("recycle")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player pr = (Player) sender; + if (!pr.isOp() + && !pr.getScoreboardTags() + .contains(config.adminTag)) { + pr.sendMessage("§c权限不足"); + return true; + } + garbage.openRecycle(pr); + return true; + } + + + // /垃圾清理 + if (cmdName.equals("垃圾清理")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + int count = garbage.collectItems(); + sender.sendMessage("§a已清理 " + + count + " 个掉落物"); + return true; + } + // /menu 开关雪球菜单 + if (cmdName.equals("menu")) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length == 0) { + p2.sendMessage( + "§e用法: /menu <开/关>"); + return true; + } + String arg = args[0]; + if (arg.equals("开")) { + db.setField(p2.getName(), + "menu_snowball", 1); + p2.sendMessage( + "§a雪球菜单已开启," + + "下次进入将发放"); + } else if (arg.equals("关")) { + db.setField(p2.getName(), + "menu_snowball", 0); + p2.sendMessage( + "§c雪球菜单已关闭," + + "下次进入不再发放"); + } else { + p2.sendMessage( + "§e用法: /menu <开/关>"); + } + return true; + } + + if (cmdName.equals("reg") + || cmdName.equals("注册")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (db.userExists(p.getName())) { + p.sendMessage( + "§c您已注册过,请使用 /login"); + return true; + } + try { + loginMgr.handleRegister(p, args); + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onRegister(p); + } + + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] reg命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c注册出错,请联系管理员"); + } + return true; + } + + + if (cmdName.equals("login") + || cmdName.equals("l") + || cmdName.equals("登录")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (loggedIn.contains(p.getName())) { + p.sendMessage("§c您已登录"); + return true; + } + try { + loginMgr.handleLogin(p, args); + // [FIX] 手动登录后关闭飞行(仅一次) + if (loggedIn.contains(p.getName()) + && p.getAllowFlight()) { + p.setAllowFlight(false); + p.setFlying(false); + } + + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onLogin(p); + } + + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] login命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c登录出错,请联系管理员"); + } + return true; + } + + // /找回密码 + if (cmdName.equals("找回密码")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + handleReset((Player) sender); + return true; + } + + // /签到 + if (cmdName.equals("签到")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // 绑定邮箱 + if (cmdName.equals("绑定邮箱")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 1) { + String emailAddr = args[0]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // /oa + if (cmdName.equals("oa")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + openAccountRequestPanel( + (Player) sender); + return true; + } + + // /玩家信息 + if (cmdName.equals("玩家信息")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + // ===== /sdf1_login ===== + if (!cmdName.equals("sdf1_login")) + return false; + + if (args.length == 0) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§e用法: /sdf1_login <子命令>"); + return true; + } + gui.openMain((Player) sender); + return true; + } + +// ===== del / confirm / cancel(最先处理) ===== + if (sub.equals("del")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login del <玩家>"); + return true; + } + String target = args[1]; + if (!db.userExists(target)) { + sender.sendMessage( + "§c玩家 " + target + " 不存在"); + return true; + } + if (pendingDeleteTask != null) { + pendingDeleteTask.cancel(); + String old = pendingDeleteName; + pendingDeleteTask = null; + pendingDeleteName = null; + getLogger().info( + "[Sdf1_login] 取消旧待删除: " + old); + } + pendingDeleteName = target; + pendingDeleteTask = new BukkitRunnable() { + @Override + public void run() { + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + if (name == null) return; + db.deleteUser(name); + getLogger().info( + "[Sdf1_login] 已删除: " + name); + Player tp = Bukkit.getPlayer(name); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c账号已被管理员删除"); + } + }; + pendingDeleteTask.runTaskLater(this, 1200L); + sender.sendMessage("§e§l[删除确认] 目标: §c" + + target); + sender.sendMessage("§760秒后执行," + + "§a/sdf1_login confirm §7确认," + + "§c/sdf1_login cancel §7取消"); + getLogger().warning("[Sdf1_login] 待删除: " + + target + " by " + sender.getName()); + return true; + } + + if (sub.equals("confirm")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (pendingDeleteName == null + || pendingDeleteTask == null) { + sender.sendMessage( + "§c当前没有待删除的账号"); + return true; + } + pendingDeleteTask.cancel(); + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + db.deleteUser(name); + sender.sendMessage("§a已立即删除: " + name); + Player tp = Bukkit.getPlayer(name); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c账号已被管理员删除"); + return true; + } + + if (sub.equals("cancel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (pendingDeleteName == null + || pendingDeleteTask == null) { + sender.sendMessage( + "§c当前没有待删除的账号"); + return true; + } + pendingDeleteTask.cancel(); + String name = pendingDeleteName; + pendingDeleteName = null; + pendingDeleteTask = null; + sender.sendMessage("§a已取消删除: " + name); + return true; + } + +// ===== stop ===== + if (sub.equals("stop")) { + if (!(sender instanceof Player)) { + if (args.length >= 2) { + db.clearTempPassword(args[1]); + sender.sendMessage("§a已报废 " + + args[1] + " 的临时密码"); + } else { + db.clearAllTempPasswords(); + sender.sendMessage("§a已报废所有临时密码"); + } + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String tgt = args[1]; + String myIP = getPlayerIP(p2); + String targetIP = + db.getPlayerIPByName(tgt); + if (targetIP == null) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + targetIP = getPlayerIP(tp); + } + if (targetIP == null + || !targetIP.equals(myIP)) { + p2.sendMessage("§c只能操作同IP下的玩家"); + return true; + } + db.clearTempPassword(tgt); + p2.sendMessage("§a已报废 " + tgt + " 的临时密码"); + } else { + db.clearTempPassword(p2.getName()); + p2.sendMessage("§a已报废您的临时密码"); + } + return true; + } + +// ===== reload ===== + if (sub.equals("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + // 重载时清理.sdf1临时文件 + cleanupSdf1Files(); + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + chatFilter.loadConfig(); + if (radio != null) radio.reload(); + if (getMenu() != null) { + getMenu().loadMenu(); + } + if (shopManager != null) { + shopManager.loadCategories(); + } + if (shopManager != null) shopManager.loadCategories(); + if (areaProtection != null) { + areaProtection.clearAllPlayerAreas(); + } + if (questTracker != null) { + questTracker.reload(); + } + sender.sendMessage("§a§l配置已重载!"); + return true; + } + +// ===== back ===== + if (sub.equals("back")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + // ... 保持原有back代码不变 ... + if (args.length < 2) { + sender.sendMessage("§e用法: /sdf1_login back <玩家> [#编号]"); + return true; + } + // 原有back逻辑 + String backTarget = args[1]; + if (args.length >= 3 + && args[2].startsWith("#")) { + int backupId; + try { + backupId = Integer.parseInt( + args[2].replace("#", "")); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效编号"); + return true; + } + String[] backup = + db.getInventoryBackupById(backupId); + if (backup == null) { + sender.sendMessage("§c备份 #" + backupId + " 不存在"); + return true; + } + Player tp = Bukkit.getPlayer(backTarget); + if (tp != null && tp.isOnline()) { + try { + tp.getInventory().setContents( + InventorySerializer.fromBase64(backup[0])); + if (backup[1] != null && !backup[1].isEmpty()) + tp.getInventory().setArmorContents( + InventorySerializer.fromBase64(backup[1])); + if (backup[2] != null && !backup[2].isEmpty()) + tp.getInventory().setExtraContents( + InventorySerializer.fromBase64(backup[2])); + int lv = Integer.parseInt(backup[3]); + if (lv > 0) tp.setLevel(lv); + tp.sendMessage("§a管理员已恢复您的背包"); + sender.sendMessage("§a已恢复 " + backTarget + " (备份#" + backupId + ")"); + } catch (Exception e) { + sender.sendMessage("§c恢复失败: " + e.getMessage()); + } + } else { + db.saveInventoryBackup(backTarget, + backup[0], backup[1], backup[2], + Integer.parseInt(backup[3]), + Double.parseDouble(backup[4])); + sender.sendMessage("§a" + backTarget + " 离线,备份已写入"); + } + } else { + List> backups = + db.getInventoryBackups(backTarget, 5); + if (backups.isEmpty()) { + sender.sendMessage("§c" + backTarget + " 没有背包备份"); + return true; + } + sender.sendMessage("§e§l=== " + backTarget + " 背包备份 ==="); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat("MM-dd HH:mm:ss"); + for (Map b : backups) { + int bid = ((Number) b.get("id")).intValue(); + Object stObj = b.get("save_time"); + long st = stObj != null + ? ((Number) stObj).longValue() : 0; + String time = st > 0 + ? sdf.format(new java.util.Date(st)) + : "未知时间"; + String itemNames = getBackupItemPreview(bid); + sender.sendMessage("§7#" + bid + " §f" + time + " §7: " + itemNames); + } + sender.sendMessage("§7使用 §e/sdf1_login back " + backTarget + " #编号 §7恢复"); + } + return true; + } + +// ===== pw / password ===== + if (sub.equals("pw") || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate(newPwd)) { + p2.sendMessage(config.msg("password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), "password_salt"); + String hash = PasswordUtils.hash(oldPwd, salt); + boolean mainOk = db.checkPassword( + p2.getName(), hash); + boolean tempOk = db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg("password_wrong")); + return true; + } + String oldHash = (String) db.getField( + p2.getName(), "password_hash"); + String newSalt = PasswordUtils.generateSalt(); + String newHash = PasswordUtils.hash(newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg("password_same")); + return true; + } + recordPasswordChange(p2.getName(), oldHash, salt); + db.setField(p2.getName(), "password_hash", newHash); + db.setField(p2.getName(), "password_salt", newSalt); + if (!mainOk && tempOk) + db.clearTempPassword(p2.getName()); + // pw 命令改密码成功后 + needsPasswordChange.remove(p2.getName()); +// 清除临时密码 + if (!mainOk && tempOk) + db.clearTempPassword(p2.getName()); + + return true; + } + + if (args.length == 2) { + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), "password_salt"); + String hash = PasswordUtils.hash(oldPwd, salt); + boolean mainOk = db.checkPassword( + p2.getName(), hash); + boolean tempOk = db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg("password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType.CHANGE_PWD_STEP2; + chatInput.getState(p2).ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage("§e请输入新密码:"); + return true; + } + + chatInput.getState(p2).type = + ChatInputManager.InputType.CHANGE_PWD_STEP1; + p2.sendMessage("§e请输入当前密码:"); + return true; + } + +// ===== email ===== + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL_VERIFY; + chatInput.getState(p2).targetPlayer = emailAddr; + chatInput.getState(p2).ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + oldEmail); + p2.sendMessage("§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL_VERIFY; + chatInput.getState(p2).targetPlayer = emailAddr; + chatInput.getState(p2).ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = + email.sendVerifyCode(to, c); + Bukkit.getScheduler() + .runTask(this, () -> { + if (sent) { + fp.sendMessage("§a验证码已发送到 " + to); + fp.sendMessage("§e输入6位验证码(0取消):"); + } else { + fp.sendMessage("§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType.SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + +// ===== sign ===== + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + dailySignWithReward((Player) sender); + return true; + } + +// ===== reset ===== + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset((Player) sender); + return true; + } + +// ===== undo(密码撤销) ===== + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), "password_hash", rb.hash); + db.setField(p2.getName(), "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // radio子命令(控制台+玩家) + // sdf1_login radio → 停止全服广播 + // sdf1_login radio reload → 重载 + // sdf1_login radio <歌名> → 全服播放 + // sdf1_login radio <歌名> <玩家名> → 指定玩家播放 + // sdf1_login radio <歌名> login → 推送给登录中玩家 + if (sub.equals("radio")) { + if (args.length < 2) { + // 无参数 = 停止 + radio.stopMainRadio(); + sender.sendMessage("§c全服广播已停止"); + return true; + } + String arg1 = args[1]; + + // reload + if (arg1.equalsIgnoreCase("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + radio.reload(); + sender.sendMessage("§aRadio已重载"); + return true; + } + + // radio <歌名> → 全服播放 + // radio <歌名> <玩家名> → 指定玩家 + // radio <歌名> login → 登录阶段播放 + String songName = arg1; + + if (args.length >= 3) { + String target = args[2]; + + if (target.equalsIgnoreCase("login")) { + if (radio.hasSong(songName)) { + radio.playToLoginPlayers(songName); + sender.sendMessage( + "§a已推送 §e" + songName + + " §a给登录中玩家"); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + return true; + } + } else { + Player targetP = + Bukkit.getPlayer(target); + if (targetP != null + && targetP.isOnline()) { + if (radio.hasSong(songName)) { + radio.playToPlayer( + targetP, songName); + sender.sendMessage( + "§a已向 §e" + target + + " §a播放 §e" + + songName); + targetP.sendMessage( + "§6§l[广播] §f正在播放: §e" + + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + + songName); + } + } else { + sender.sendMessage( + "§c玩家不在线: " + + target); + } + return true; + } + } else { + if (radio.hasSong(songName)) { + radio.playToAll(songName); + sender.sendMessage( + "§a全服播放: §e" + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + } + return true; + } + + + // pw / password + if (sub.equals("pw") + || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + // /sdf1_login pw 旧密码 新密码 + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate( + newPwd)) { + p2.sendMessage(config.msg( + "password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + String oldHash = + (String) db.getField( + p2.getName(), + "password_hash"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg( + "password_same")); + return true; + } + recordPasswordChange( + p2.getName(), + oldHash, salt); + db.setField(p2.getName(), + "password_hash", newHash); + db.setField(p2.getName(), + "password_salt", newSalt); + if (!mainOk && tempOk) { + db.clearTempPassword( + p2.getName()); + } + needsPasswordChange.remove( + p2.getName()); + p2.sendMessage(config.msg( + "password_changed")); + return true; + } + + if (args.length == 2) { + // /sdf1_login pw 旧密码 + // 验证旧密码,然后聊天输入新密码 + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP2; + chatInput.getState(p2) + .ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage( + "§e请输入新密码:"); + return true; + } + + // /sdf1_login pw (无参数) + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p2.sendMessage( + "§e请输入当前密码:"); + return true; + } + + // email + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + // 已绑定:先验证密码 + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + // 未绑定:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // sign + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // reset + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset( + (Player) sender); + return true; + } + + // undo + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = + pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time + > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), + "password_hash", rb.hash); + db.setField(p2.getName(), + "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // take + if (sub.equals("take")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login take <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + if (!db.deductPoints(args[1], amount)) { + sender.sendMessage("§c积分不足"); + return true; + } + sender.sendMessage("§a已扣除 " + args[1] + + " " + amount + " 积分"); + return true; + } + + + // give - 手动发放雪球菜单 + if (sub.equals("give")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login give <玩家名>"); + return true; + } + Player tp = Bukkit.getPlayer( + args[1]); + if (tp == null || !tp.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + + args[1]); + return true; + } + forceGiveMenuSnowball(tp); + tp.sendMessage( + "§a管理员已为您发放菜单雪球"); + sender.sendMessage( + "§a已发放雪球菜单给 " + + args[1]); + return true; + } + + // shopadd + if (sub.equals("shopadd")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 4) { + sender.sendMessage( + "§e用法: /sdf1_login shopadd <名称> <价格> <命令>"); + return true; + } + String shopName = args[1]; + int price; + try { + price = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + String shopCmd = args[3]; + points.addItem(shopName, price, shopCmd); + sender.sendMessage("§a已添加商品: " + + shopName + " (" + price + + "积分)"); + return true; + } + + // shopdel + if (sub.equals("shopdel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login shopdel <序号>"); + return true; + } + try { + int idx = Integer.parseInt(args[1]); + points.removeItem(idx); + sender.sendMessage("§a已删除商品 #" + idx); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + } + return true; + } + + // get + if (sub.equals("get")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login get <玩家>"); + return true; + } + Map user = + db.getUser(args[1]); + if (user.isEmpty()) { + sender.sendMessage("§c玩家不存在"); + return true; + } + sender.sendMessage( + "§e=== " + args[1] + " ==="); + sender.sendMessage("§7积分: §e" + + user.getOrDefault("points", 0)); + sender.sendMessage("§7邮箱: §e" + + user.getOrDefault("email", "无")); + sender.sendMessage("§7签到天数: §e" + + user.getOrDefault( + "total_checkin_days", 0)); + sender.sendMessage("§7邀请码: §e" + + user.getOrDefault( + "invite_code", "无")); + return true; + } + + + // kick (挂机) + if (sub.equals("kick")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login kick <开关> [分钟]"); + return true; + } + String mode = args[1].toLowerCase(); + if (mode.equals("on") + || mode.equals("开") + || mode.equals("启用")) { + config.afkEnabled = true; + if (args.length >= 3) { + try { + config.afkTimeout = + Integer.parseInt( + args[2]) * 60; + } catch (NumberFormatException ignored) { + } + } + config.saveSettings(); + int minutes = + config.afkTimeout / 60; + sender.sendMessage( + "§a挂机踢出已开启,超时: " + + minutes + " 分钟"); + } else if (mode.equals("off") + || mode.equals("关") + || mode.equals("停用")) { + config.afkEnabled = false; + config.saveSettings(); + sender.sendMessage( + "§c挂机踢出已关闭"); + } else { + sender.sendMessage( + "§c无效参数 on/off"); + } + return true; + } + + // add + if (sub.equals("add")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login add <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + db.addPoints(args[1], amount); + sender.sendMessage("§a已为 " + args[1] + + " 增加 " + amount + " 积分"); + return true; + } + + // ticket + if (sub.equals("ticket") + || sub.equals("工单")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + ticket.openMain((Player) sender); + return true; + } + + sender.sendMessage("§c未知参数"); + return true; + } + + return true; + } + /** + * 供SDF1反射调用的CDK兑换 + * 返回: "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeemBondForExternal( + String playerName, String code) { + if (cdkManager == null) + return "fail:CDK未初始化"; + return cdkManager.redeem(code, playerName); + } + // 旧的(可能缺失或拼写错误) + public SalesStatsManager getSalesStats() { + return salesStats; + } + + /** + * 是否是垃圾站展示物品(带ID lore) + */ + private boolean isTrashDisplayItem( + ItemStack item) { + if (item == null + || item.getType() + == Material.AIR) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null || !im.hasLore()) + return false; + List lore = im.getLore(); + if (lore == null || lore.isEmpty()) + return false; + return lore.get(0) + .startsWith("§7ID: #"); + } + + private void sendChatHelp(Player p) { + p.sendMessage( + "§e§l======== 聊天过滤帮助 =========="); + p.sendMessage("§e/chat reload §7- 重载配置"); + p.sendMessage("§e/chat 开/关 §7- 开启/关闭聊天过滤"); + p.sendMessage("§e/chat add §7- 添加白名单URL"); + p.sendMessage("§e/chat remove §7- 移除白名单URL"); + p.sendMessage("§e/chat addplayer <玩家> §7- 添加白名单玩家"); + p.sendMessage("§e/chat takeplayer <玩家> §7- 移除白名单玩家"); + p.sendMessage("§e/chat unmute <玩家> §7- 解禁玩家"); + p.sendMessage("§e/chat reset <玩家> §7- 重置违规记录"); + p.sendMessage( + "§e§l=================================="); + } + + private int findEmptySlot(Inventory inv) { + for (int i = 0; i < 49; i++) { + ItemStack item = inv.getItem(i); + if (item == null + || item.getType() + == Material.AIR) + return i; + } + return -1; + } + + // ===== TabComplete ===== + @Override + public List onTabComplete( + CommandSender sender, Command cmd, + String label, String[] args) { + List list = new ArrayList<>(); + if (cmd.getName().equalsIgnoreCase( + "sdf1_login") + && args.length == 1) { + if (isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "pw", "email", + "sign", "reset", "undo", + "get", "del", "set", + "take", "add", "kick", + "ticket", "oa", "stop", + "shopadd", "shopdel", "back", "radio", "back", "shop")); + } else { + list.addAll(Arrays.asList( + "pw", "email", "sign", + "reset", "undo", + "ticket", "oa", "stop")); + } + } + if (cmd.getName().equalsIgnoreCase("protect") + || cmd.getName().equals("区域保护")) { + + String sub = args.length > 0 ? args[0].toLowerCase() : ""; + + // ===== 第一层:子命令 ===== + if (args.length == 1) { + List opts = Arrays.asList( + "工具", "创建", "列表", "重载", "删除", + "add", "remove", + "additem", "removeitem", + "on", "off", "tempon", + "expand", "contraction", + "list", "listitem", + "addname", "removename", "listname", + "addwhite", "removewhite", "listwhite"); + return filterTab(opts, args[0]); + } + + // ===== 第二层 ===== + if (args.length == 2) { + // add/remove: 区域名 + global + 在线玩家 + if (sub.equals("add") || sub.equals("remove")) { + List all = new ArrayList<>(); + all.add("global"); + all.addAll(areaProtection.getAreaNames()); + for (Player pl : Bukkit.getOnlinePlayers()) + all.add(pl.getName()); + return filterTab(all, args[1]); + } + // additem/removeitem: 区域名 + global + if (sub.equals("additem") || sub.equals("removeitem")) { + List all = new ArrayList<>(); + all.add("global"); + all.addAll(areaProtection.getAreaNames()); + return filterTab(all, args[1]); + } + // list/listitem: 区域名 + if (sub.equals("list") || sub.equals("listitem")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // expand/contraction: 数字 + if (sub.equals("expand") || sub.equals("contraction")) { + return Arrays.asList("1", "3", "5", "10", "20", "50"); + } + // 创建/删除: 区域名 + if (sub.equals("创建") || sub.equals("删除")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // addname/removename/listname: 区域名 + if (sub.equals("addname") || sub.equals("removename") + || sub.equals("listname")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + // addwhite/removewhite/listwhite: 区域名 + if (sub.equals("addwhite") || sub.equals("removewhite") + || sub.equals("listwhite")) { + return filterTab( + new ArrayList<>(areaProtection.getAreaNames()), + args[1]); + } + } + + // ===== 第三层 ===== + if (args.length == 3) { + // add/remove: 第三层是玩家名 + if (sub.equals("add") || sub.equals("remove")) { + List players = new ArrayList<>(); + for (Player pl : Bukkit.getOnlinePlayers()) + players.add(pl.getName()); + return filterTab(players, args[2]); + } + // additem/removeitem: 第三层是物品ID + if (sub.equals("additem") || sub.equals("removeitem")) { + List items = Arrays.asList( + "TNT", "BROWN_MUSHROOM", "RED_MUSHROOM", + "MUSHROOM_STEW", "POISONOUS_POTATO", + "SPIDER_EYE", "POTION", "SPLASH_POTION", + "OMINOUS_BOTTLE", "ENDER_PEARL", + "BOW", "CROSSBOW", "ARROW", + "FLINT_AND_STEEL"); + return filterTab(items, args[2]); + } + // addname/removename: 第三层是生物名字(自由输入) + if (sub.equals("addname") || sub.equals("removename")) { + return null; + } + // addwhite/removewhite: 第三层是玩家名 + if (sub.equals("addwhite") || sub.equals("removewhite")) { + List players = new ArrayList<>(); + for (Player pl : Bukkit.getOnlinePlayers()) + players.add(pl.getName()); + return filterTab(players, args[2]); + } + } + + return null; + } + + + if (cmd.getName().equalsIgnoreCase("chat") + && args.length == 1 + && isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "add", "remove", + "addplayer", "takeplayer", + "unmute", "reset")); + } + if (cmd.getName().equalsIgnoreCase("cypay")) { + return cypayCommand.onTabComplete(sender, cmd, label, args); + } + + return list; + } +} \ No newline at end of file diff --git a/backup/QuestTracker.java.backup b/backup/QuestTracker.java.backup new file mode 100644 index 0000000..f2eda67 --- /dev/null +++ b/backup/QuestTracker.java.backup @@ -0,0 +1,3662 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final QuestStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + private static boolean loggedStructureRegistry + = false; + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new QuestStorage( + plugin.getDataFolder()); + + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 岩岸/沙滩 + BIOME_CATEGORY.put("minecraft:stony_shore", + "石岸"); +// 海岸 + BIOME_CATEGORY.put("minecraft:mushroom_fields", + "蘑菇岛"); +// 苍白之园 + BIOME_CATEGORY.put( + "minecraft:pale_garden", "森林"); +// 末地小型岛屿 + BIOME_CATEGORY.put( + "minecraft:small_end_islands", "末地"); +// 主世界结构归类 + BIOME_CATEGORY.put("minecraft:village", "平原"); + BIOME_CATEGORY.put( + "minecraft:desert_pyramid", "沙漠"); + BIOME_CATEGORY.put( + "minecraft:jungle_pyramid", "森林"); + BIOME_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + + + } + + // 所有群系大类列表 + private static final String[] + ALL_CATEGORIES = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "石岸", "蘑菇岛", "地下", + "下界", "末地" + }; + + // ===== 结构映射(PaperMC 1.21.4 实际结构 key)===== + private static final Map + STRUCTURE_CATEGORY = + new HashMap<>(); + static { + // 主世界结构 + STRUCTURE_CATEGORY.put( + "minecraft:village_plains", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_desert", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_savanna", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_snowy", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_taiga", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:desert_pyramid", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:jungle_pyramid", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + STRUCTURE_CATEGORY.put( + "minecraft:igloo", "雪原"); + STRUCTURE_CATEGORY.put( + "minecraft:mansion", "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:pillager_outpost", + "掠夺者前哨站"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft_mesa", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:trail_ruins", + "远古遗迹"); + STRUCTURE_CATEGORY.put( + "minecraft:trial_chambers", + "试炼密室"); + STRUCTURE_CATEGORY.put( + "minecraft:monument", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_cold", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_warm", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck_beached", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:buried_treasure", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_desert", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_jungle", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_mountain", + "山地"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_ocean", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_swamp", + "沼泽"); + // 下界结构 + STRUCTURE_CATEGORY.put( + "minecraft:fortress", "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:bastion_remnant", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:nether_fossil", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_nether", + "下界"); + // 末地结构 + STRUCTURE_CATEGORY.put( + "minecraft:end_city", "末地"); + } + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + CATEGORY_KEYWORDS.put("石岸", "石岸"); + CATEGORY_KEYWORDS.put("沙滩", "石岸"); + CATEGORY_KEYWORDS.put("海岸", "石岸"); + + // ===== 结构名称关键词映射 ===== + // 主世界结构 + CATEGORY_KEYWORDS.put("村庄", "村庄"); + CATEGORY_KEYWORDS.put("沙漠神殿", "沙漠"); + CATEGORY_KEYWORDS.put("丛林神庙", "森林"); + CATEGORY_KEYWORDS.put("沼泽小屋", "沼泽"); + CATEGORY_KEYWORDS.put("雪屋", "雪原"); + CATEGORY_KEYWORDS.put("林地府邸", "森林"); + CATEGORY_KEYWORDS.put("掠夺者前哨站", "平原"); + CATEGORY_KEYWORDS.put("废弃矿井", "地下"); + CATEGORY_KEYWORDS.put("要塞", "地下"); + CATEGORY_KEYWORDS.put("远古城市", "地下"); + CATEGORY_KEYWORDS.put("古迹废墟", "地下"); + CATEGORY_KEYWORDS.put("试炼密室", "地下"); + CATEGORY_KEYWORDS.put("海底废墟", "海洋"); + CATEGORY_KEYWORDS.put("沉船", "海洋"); + CATEGORY_KEYWORDS.put("埋藏的宝箱", "海洋"); + CATEGORY_KEYWORDS.put("废弃传送门", "主世界"); + CATEGORY_KEYWORDS.put("化石", "主世界"); + + // 下界结构 + CATEGORY_KEYWORDS.put("下界要塞", "下界"); + CATEGORY_KEYWORDS.put("猪灵堡垒", "下界"); + CATEGORY_KEYWORDS.put("玄武岩结构", "下界"); + + // 末地结构 + CATEGORY_KEYWORDS.put("末地城", "末地"); + CATEGORY_KEYWORDS.put("末地船", "末地"); + + // 下界相关别名 + CATEGORY_KEYWORDS.put("地狱要塞", "下界"); + CATEGORY_KEYWORDS.put("下界堡垒", "下界"); + + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 优先用世界名判断维度 + if (worldName != null) { + String w = + worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + + // 用群系映射表 + if (rawKey != null) { + String lower = + rawKey.toLowerCase(); + // 带 minecraft: 前缀的直接查 + String cat = + BIOME_CATEGORY.get(lower); + if (cat != null) + return cat; + + // 不带前缀的尝试补全 + String prefixed = + "minecraft:" + lower; + cat = BIOME_CATEGORY.get(prefixed); + if (cat != null) + return cat; + + // 模糊匹配群系名 + for (Map.Entry entry + : BIOME_CATEGORY.entrySet()) { + if (entry.getKey() + .contains(lower) + || lower.contains( + entry.getKey() + .replace( + "minecraft:", + ""))) { + return entry.getValue(); + } + } + } + + return "主世界"; + } + + // ========== 结构检测 ========== + + /** + * 检测指定位置是否存在自然生成的结构 + * 返回结构分类(如"村庄"、"沙漠"等),未找到返回null + * 使用 PaperMC World.hasStructureAt() + Registry.STRUCTURE API + */ + private String detectStructureAt( + String worldName, double x, + double y, double z) { + try { + World w = Bukkit.getWorld(worldName); + if (w == null) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测: 世界 '" + + worldName + "' 不存在"); + return null; + } + + int bx = (int) Math.floor(x); + int by = (int) Math.floor(y); + int bz = (int) Math.floor(z); + + // 首次调用时打印所有注册表结构键 + if (!loggedStructureRegistry) { + plugin.getLogger().info( + "[QuestTracker] === 结构注册表诊断 ==="); + int count = 0; + for (Structure s : Registry.STRUCTURE) { + NamespacedKey key = + Registry.STRUCTURE.getKey(s); + plugin.getLogger().info( + "[QuestTracker] 注册表结构 #" + + count + ": " + key); + count++; + } + plugin.getLogger().info( + "[QuestTracker] 注册表共 " + + count + " 个结构"); + plugin.getLogger().info( + "[QuestTracker] === 诊断结束 ==="); + loggedStructureRegistry = true; + } + + // 第1步: 精确位置检查 + io.papermc.paper.math.Position exactPos = + io.papermc.paper.math.Position.block( + bx, by, bz); + for (Structure structure : Registry.STRUCTURE) { + boolean hasAt = + w.hasStructureAt(exactPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY.get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(精确): " + + keyStr + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + + // 第2步: Y轴偏移检查 (-5 到 +10) + for (int dy = -5; dy <= 10; dy++) { + if (dy == 0) continue; + int testY = by + dy; + io.papermc.paper.math.Position testPos = + io.papermc.paper.math.Position.block( + bx, testY, bz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + testPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(Y偏移" + + dy + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + } + + // 第3步: 水平偏移检查 (±1格) + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; + io.papermc.paper.math.Position offsetPos = + io.papermc.paper.math.Position + .block(bx + dx, by, + bz + dz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + offsetPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey( + structure); + String keyStr = (key != null) + ? key.toString() + : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(偏移" + + dx + "," + + dz + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat + : "未映射")); + if (cat != null) return cat; + } + } + } + } + + plugin.getLogger().info( + "[QuestTracker] 所有位置检查完毕, " + + "未匹配任何结构 (pos=" + + bx + "," + by + "," + bz + + ")"); + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测异常: " + + ex.getClass().getSimpleName() + + ": " + ex.getMessage()); + ex.printStackTrace(); + } + return null; + } + + /** + * 将结构中文名映射到对应的大类 + * 用于匹配任务条件中的结构名 + */ + private String mapStructureCategory( + String structureName) { + if (structureName == null) return ""; + + // 主世界结构 + if (structureName.contains("村庄")) + return "村庄"; + if (structureName.contains("沙漠神殿")) + return "沙漠"; + if (structureName.contains("丛林神庙") || structureName.contains("丛林神殿")) + return "森林"; + if (structureName.contains("沼泽小屋")) + return "沼泽"; + if (structureName.contains("雪屋") || structureName.contains("冰屋")) + return "雪原"; + if (structureName.contains("林地府邸")) + return "森林"; + if (structureName.contains("掠夺者前哨站")) + return "掠夺者前哨站"; + if (structureName.contains("废弃矿井") || structureName.contains("恶地废弃矿井")) + return "地下"; + if (structureName.contains("要塞")) + return "地下"; + if (structureName.contains("远古城市")) + return "地下"; + if (structureName.contains("远古遗迹") || structureName.contains("古迹废墟")) + return "远古遗迹"; + if (structureName.contains("试炼密室")) + return "试炼密室"; + if (structureName.contains("海底神殿") || structureName.contains("海洋神殿")) + return "海洋"; + if (structureName.contains("海底废墟") || structureName.contains("冻洋废墟") || structureName.contains("暖洋废墟")) + return "海洋"; + if (structureName.contains("沉船")) + return "海洋"; + if (structureName.contains("埋藏的宝箱") || structureName.contains("埋藏的宝藏")) + return "海洋"; + if (structureName.contains("废弃传送门")) + return "平原"; + if (structureName.contains("化石")) + return "主世界"; + + // 下界结构 + if (structureName.contains("下界要塞")) + return "下界"; + if (structureName.contains("猪灵堡垒") || structureName.contains("堡垒遗迹")) + return "下界"; + if (structureName.contains("玄武岩结构") || structureName.contains("下界化石")) + return "下界"; + + // 末地结构 + if (structureName.contains("末地城")) + return "末地"; + if (structureName.contains("末地船")) + return "末地"; + + return ""; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API或债券 + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage("§c无可领取的奖励"); + return; + } + + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) { + player.sendMessage( + "§7[可选奖励] 需完成所有可选条件: " + rt); + continue; + } + } + + // 按 + 分割子项 + String[] parts = rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } + } + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory().addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); + } + + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + public String getProgress( + String player, String rawCond) { + if (player == null || rawCond == null) + return "§7未知"; + String key = stripOptional(rawCond); + + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + + cur + "/1"; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "VILLAGER_TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + + } + + // ===== 击杀泛解析(支持中文列表) ===== +// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等 + if (key.contains("击杀") && key.contains("只")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + String mobPart = key; + java.util.regex.Matcher killM = + java.util.regex.Pattern + .compile("击杀\\d+只(.+)") + .matcher(key); + if (killM.find()) { + mobPart = killM.group(1).trim(); + } + + String[] mobNames = mobPart + .split("[、,,,]+"); + int totalKills = 0; + StringBuilder names = new StringBuilder(); + for (String name : mobNames) { + String trimmed = name.trim(); + if (trimmed.isEmpty()) continue; + String entityType = + chineseMobToEntity(trimmed); + if (!entityType.isEmpty()) { + totalKills += getCounter( + player, + "KILL:" + entityType); + if (names.length() > 0) + names.append("/"); + names.append(trimmed); + } + } + return "§7击杀" + names + ": §e" + + totalKills + "/" + needed; + } + +// 单一KILL:xxx:N 格式(原有逻辑保留) + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + int needed = parseInt(p[2]); + int cur = getCounter(player, + "KILL:" + p[1]); + return "§7击杀" + p[1] + ": §e" + + cur + "/" + needed; + } + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + // ===== 泛解析:击杀类进度 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < mobTypes.size(); i++) { + total += getCounter(player, + "KILL:" + mobTypes.get(i)); + if (i > 0) names.append("/"); + names.append(reverseMobName( + mobTypes.get(i))); + } + return "§7击杀" + names + ": §e" + + total + "/" + needed; + } + } + + // ===== 泛解析:村民交易进度 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + // ===== 泛解析:采集/挖矿进度 ===== + if (key.contains("采集") + || key.contains("挖")) { + int needed = + parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < matTypes.size(); i++) { + total += getCounter( + player, + "MINE:" + matTypes + .get(i)); + if (i > 0) + names.append("/"); + names.append( + translateMaterialName( + matTypes + .get(i))); + } + return "§7采集" + names + + ": §e" + total + + "/" + needed; + } + } + + // ===== 泛解析:合成进度 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "CRAFT"); + return "§7合成: §e" + cur + "/" + needed; + } + + // ===== 泛解析:钓鱼进度 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "FISH"); + return "§7钓鱼: §e" + cur + "/" + needed; + } + + return "§7" + key; + } + private String translateMaterialName( + String name) { + switch (name) { + case "COAL_ORE": + return "煤矿石"; + case "DEEPSLATE_COAL_ORE": + return "深层煤矿石"; + case "IRON_ORE": + return "铁矿石"; + case "DEEPSLATE_IRON_ORE": + return "深层铁矿石"; + case "COPPER_ORE": + return "铜矿石"; + case "DEEPSLATE_COPPER_ORE": + return "深层铜矿石"; + case "GOLD_ORE": + return "金矿石"; + case "DEEPSLATE_GOLD_ORE": + return "深层金矿石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "DEEPSLATE_DIAMOND_ORE": + return "深层钻石矿石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "DEEPSLATE_EMERALD_ORE": + return "深层绿宝石矿石"; + case "LAPIS_ORE": + return "青金石矿石"; + case "DEEPSLATE_LAPIS_ORE": + return "深层青金石矿石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "DEEPSLATE_REDSTONE_ORE": + return "深层红石矿石"; + case "NETHER_GOLD_ORE": + return "下界金矿石"; + case "NETHER_QUARTZ_ORE": + return "下界石英矿石"; + case "ANCIENT_DEBRIS": + return "远古残骸"; + case "COAL": + return "煤炭"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "RAW_COPPER": + return "粗铜"; + case "DIAMOND": + return "钻石"; + case "EMERALD": + return "绿宝石"; + case "LAPIS_LAZULI": + return "青金石"; + case "REDSTONE": + return "红石"; + case "QUARTZ": + return "石英"; + default: + return name; + } + } + + /** + * 中文物种名 → Bukkit EntityType 名 + * 支持击杀泛解析的模糊匹配 + */ + private String chineseMobToEntity(String cn) { + String n = cn.toLowerCase().trim(); + + // 直接别名表 + Map mobMap = new HashMap<>(); + mobMap.put("骷髅", "SKELETON"); + mobMap.put("僵尸", "ZOMBIE"); + mobMap.put("苦力怕", "CREEPER"); + mobMap.put("蜘蛛", "SPIDER"); + mobMap.put("洞穴蜘蛛", "CAVE_SPIDER"); + mobMap.put("小白", "SKELETON"); + mobMap.put("末影人", "ENDERMAN"); + mobMap.put("enderman", "ENDERMAN"); + mobMap.put("猪灵", "PIGLIN"); + mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE"); + mobMap.put("恶魂", "GHAST"); + mobMap.put("凋灵骷髅", "WITHER_SKELETON"); + mobMap.put("烈焰人", "BLAZE"); + mobMap.put("史莱姆", "SLIME"); + mobMap.put("岩浆怪", "MAGMA_CUBE"); + mobMap.put("女巫", "WITCH"); + mobMap.put("蠹虫", "SILVERFISH"); + mobMap.put("潜影贝", "SHULKER"); + mobMap.put("守卫者", "GUARDIAN"); + mobMap.put("远古守卫者", "ELDER_GUARDIAN"); + mobMap.put("溺尸", "DROWNED"); + mobMap.put("僵尸村民", "ZOMBIE_VILLAGER"); + mobMap.put("尸壳", "HUSK"); + mobMap.put("流浪者", "STRAY"); + mobMap.put("幻翼", "PHANTOM"); + mobMap.put("掠夺者", "PILLAGER"); + mobMap.put("卫道士", "VINDICATOR"); + mobMap.put("唤魔者", "EVOKER"); + mobMap.put("恼鬼", "VEX"); + mobMap.put("劫掠兽", "RAVAGER"); + mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY"); + mobMap.put("凋灵", "WITHER"); + mobMap.put("末影龙", "ENDER_DRAGON"); + + // 精确匹配 + if (mobMap.containsKey(n)) { + return mobMap.get(n); + } + + // 模糊匹配(包含关系) + for (Map.Entry en + : mobMap.entrySet()) { + if (n.contains(en.getKey()) + || en.getKey().contains(n)) { + return en.getValue(); + } + } + + // 尝试直接用英文名 + try { + return org.bukkit.entity.EntityType + .valueOf(n.toUpperCase() + .replace(" ", "_")) + .name(); + } catch (Exception e) { + return ""; + } + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + // 结构检测(覆盖纯生物群系分类) + // 地下检测:Y低于表面则归类为地下 +// 覆盖废弃矿井/试炼密室/要塞/远古城市等 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + + // ===== 结构检测(覆盖纯生物群系分类)===== + // 使用 PaperMC API 检测玩家所在位置的结构 + String structureCat = + detectStructureAt( + worldName, x, y, z); + if (structureCat != null + && !structureCat.isEmpty()) { + // 结构检测成功,结构分类优先 + // (站在结构上就不算"地下") + category = structureCat; + plugin.getLogger().info( + "[QuestTracker] 结构覆盖: " + + category + + " (结构: " + + structureCat + ")"); + } else { + plugin.getLogger().info( + "[QuestTracker] 未检测到结构: world=" + + worldName + ", pos=" + + String.format("%.0f,%.0f,%.0f", x, y, z)); + } + + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡")) + continue; + // 支持群系和结构两种打卡条件 + boolean isBiomeCond = + condKey.contains("群系") + || condKey + .contains("风景点"); + boolean isStructureCond = + condKey.contains("结构") + || condKey + .contains("村庄") + || condKey + .contains("神殿") + || condKey + .contains("神庙") + || condKey + .contains("小屋") + || condKey + .contains("雪屋") + || condKey + .contains("府邸") + || condKey + .contains("前哨站") + || condKey + .contains("矿井") + || condKey + .contains("要塞") + || condKey + .contains("城市") + || condKey + .contains("废墟") + || condKey + .contains("密室") + || condKey + .contains("沉船") + || condKey + .contains("宝箱") + || condKey + .contains("传送门") + || condKey + .contains("化石") + || condKey + .contains("堡垒"); + if (!isBiomeCond + && !isStructureCond) + continue; + String target = + extractBiome(condKey); + plugin.getLogger().info( + "[QuestTracker] 条件匹配: condKey=\"" + + condKey + "\", target=\"" + + target + "\", category=\"" + + category + "\", isBiomeCond=" + + isBiomeCond + ", isStructureCond=" + + isStructureCond); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + /* + * 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录 + */ + public String checkInAtPosition( + String player, String worldName, + double x, double y, double z) { + + List history = + loadCheckInPositions(player); + for (double[] pos : history) { + double dist = Math.sqrt( + Math.pow(x - pos[0], 2) + + Math.pow(y - pos[1], 2) + + Math.pow(z - pos[2], 2)); + if (dist <= 32) { + return "§c距离上次打卡点太近(" + + String.format( + "%.0f", dist) + + "格)"; + } + } + + // ===== 获取群系和分类 ===== + String biome = "minecraft:plains"; + String category = "平原"; + String structureCat = null; + try { + org.bukkit.World w = + Bukkit.getWorld(worldName); + if (w != null) { + org.bukkit.Location loc = + new org.bukkit.Location( + w, + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + biome = loc.getBlock() + .getBiome().getKey() + .toString(); + category = mapBiomeCategory( + biome, worldName); + + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + + // ===== 结构检测(覆盖纯生物群系分类)===== + structureCat = + detectStructureAt( + worldName, x, y, z); + plugin.getLogger().info( + "[QuestTracker] checkInAtPosition 结构检测: " + + structureCat + " at " + + String.format("%.0f,%.0f,%.0f", x, y, z)); + if (structureCat != null + && !structureCat.isEmpty()) { + // 结构分类优先于"地下" + category = structureCat; + } + + if ("下界".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.NETHER) { + category = "下界"; + } else if ("末地".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.THE_END) { + category = "末地"; + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 群系读取失败: " + + ex.getMessage()); + } + + plugin.getLogger().info( + "[QuestTracker] 打卡: " + + player + + " world=" + worldName + + " biome=" + biome + + " cat=" + category + + " pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + + saveCheckInPosition( + player, x, y, z); + recordCheckIn( + player, biome, category, + worldName, x, y, z); + List matched = + checkInBiome( + player, biome, + worldName, x, y, z); + + StringBuilder sb = new StringBuilder(); + sb.append("§a打卡成功!群系: §e") + .append(category); + if (structureCat != null && !structureCat.isEmpty()) { + sb.append("\n§a结构: §e").append(structureCat); + } + if (!matched.isEmpty()) { + sb.append("\n§a完成任务条件:"); + for (String m : matched) { + String qn = + findQuestName(m); + sb.append("\n§7• "); + if (qn != null) { + sb.append("§e[") + .append(qn) + .append("§7] §f"); + } + sb.append(m); + } + } + return sb.toString(); + } + + + /** + * 根据条件文本查找所属任务名 + */ + private String findQuestName( + String condition) { + for (String cat : Arrays.asList( + "新人任务", + "主线任务", + "支线任务")) { + for (QuestFile qf : + getQuests(cat)) { + if (qf.conditions.contains( + condition)) { + return qf.displayName; + } + } + } + return null; + } + + + /** + * 加载玩家所有历史打卡坐标 + * 格式: x1,y1,z1;x2,y2,z2;... + */ + private List loadCheckInPositions( + String player) { + List list = new ArrayList<>(); + Object val = storage.getCondition( + player, "checkin_all_pos"); + if (val == null) return list; + String data = val.toString(); + if (data.isEmpty()) return list; + String[] records = data.split(";"); + for (String r : records) { + String[] parts = r.split(","); + if (parts.length >= 3) { + try { + list.add(new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2])}); + } catch (NumberFormatException e) { + // 跳过损坏数据 + } + } + } + return list; + } + + /** + * 保存打卡坐标(追加到已有记录) + */ + private void saveCheckInPosition( + String player, double x, + double y, double z) { + List list = + loadCheckInPositions(player); + list.add(new double[]{x, y, z}); + StringBuilder sb = new StringBuilder(); + for (double[] pos : list) { + if (sb.length() > 0) sb.append(";"); + sb.append(pos[0]).append(",") + .append(pos[1]).append(",") + .append(pos[2]); + } + storage.setConditionRaw( + player, + "checkin_all_pos", + sb.toString()); + } + // ========== 中文生物名 → EntityType 映射 ========== + private static final Map + MOB_NAME_MAP = new HashMap<>(); + static { + MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER"); + MOB_NAME_MAP.put("骷髅", "SKELETON"); + MOB_NAME_MAP.put("僵尸", "ZOMBIE"); + MOB_NAME_MAP.put("苦力怕", "CREEPER"); + MOB_NAME_MAP.put("蜘蛛", "SPIDER"); + MOB_NAME_MAP.put("末影人", "ENDERMAN"); + MOB_NAME_MAP.put("女巫", "WITCH"); + MOB_NAME_MAP.put("烈焰人", "BLAZE"); + MOB_NAME_MAP.put("僵尸猪灵", + "ZOMBIFIED_PIGLIN"); + MOB_NAME_MAP.put("猪灵", "PIGLIN"); + MOB_NAME_MAP.put("恶魂", "GHAST"); + MOB_NAME_MAP.put("凋灵骷髅", + "WITHER_SKELETON"); + MOB_NAME_MAP.put("史莱姆", "SLIME"); + MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE"); + MOB_NAME_MAP.put("溺尸", "DROWNED"); + MOB_NAME_MAP.put("尸壳", "HUSK"); + MOB_NAME_MAP.put("流髑", "STRAY"); + MOB_NAME_MAP.put("幻翼", "PHANTOM"); + MOB_NAME_MAP.put("守卫者", "GUARDIAN"); + MOB_NAME_MAP.put("远古守卫者", + "ELDER_GUARDIAN"); + MOB_NAME_MAP.put("掠夺者", "PILLAGER"); + MOB_NAME_MAP.put("卫道士", "VINDICATOR"); + MOB_NAME_MAP.put("唤魔者", "EVOKER"); + MOB_NAME_MAP.put("恼鬼", "VEX"); + MOB_NAME_MAP.put("劫掠兽", "RAVAGER"); + MOB_NAME_MAP.put("潜影贝", "SHULKER"); + MOB_NAME_MAP.put("末影螨", "ENDERMITE"); + MOB_NAME_MAP.put("凋灵", "WITHER"); + MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON"); + MOB_NAME_MAP.put("监守者", "WARDEN"); + MOB_NAME_MAP.put("嗅探兽", "SNIFFER"); + MOB_NAME_MAP.put("僵尸村民", + "ZOMBIE_VILLAGER"); + } + + /** + * 从条件文本中提取所有匹配的生物类型 + * 按名称长度降序匹配,避免短名误匹配 + */ + private List extractMobTypes( + String text) { + List types = new ArrayList<>(); + List> sorted = + new ArrayList<>( + MOB_NAME_MAP.entrySet()); + sorted.sort((a, b) -> + b.getKey().length() + - a.getKey().length()); + String remaining = text; + for (Map.Entry entry + : sorted) { + if (remaining.contains( + entry.getKey())) { + if (!types.contains( + entry.getValue())) { + types.add(entry.getValue()); + } + remaining = remaining.replace( + entry.getKey(), ""); + } + } + return types; + } + + /** + * EntityType → 中文名 + */ + private String reverseMobName( + String entityType) { + for (Map.Entry entry + : MOB_NAME_MAP.entrySet()) { + if (entry.getValue() + .equals(entityType)) { + return entry.getKey(); + } + } + return entityType; + } + + /** + * 从条件文本中提取矿物名 + * "采集任意矿物(煤炭除外)1组" + * → [COAL_ORE, IRON_ORE, GOLD_ORE, ...] + */ + private List extractMaterialTypes( + String text) { + List types = new ArrayList<>(); + // 排除列表 + List excludes = + new ArrayList<>(); + if (text.contains("煤炭除外")) { + excludes.add("COAL"); + excludes.add("COAL_ORE"); + excludes.add("DEEPSLATE_COAL_ORE"); + } + // 通配符:"矿物" "矿石" + if (text.contains("矿物") + || text.contains("矿石")) { + String[] ores = { + "COAL_ORE", + "DEEPSLATE_COAL_ORE", + "IRON_ORE", + "DEEPSLATE_IRON_ORE", + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE", + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE", + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE", + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE", + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE", + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE", + "NETHER_GOLD_ORE", + "NETHER_QUARTZ_ORE", + "ANCIENT_DEBRIS" + }; + for (String ore : ores) { + if (!excludes.contains(ore)) { + types.add(ore); + } + } + return types; + } + // 单个矿物名 + if (text.contains("煤炭")) + types.add("COAL"); + if (text.contains("铁矿") + || text.contains("铁锭")) + types.add("RAW_IRON"); + if (text.contains("金矿") + || text.contains("金锭")) + types.add("RAW_GOLD"); + if (text.contains("钻石")) + types.add("DIAMOND"); + if (text.contains("绿宝石")) + types.add("EMERALD"); + if (text.contains("青金石")) + types.add("LAPIS_LAZULI"); + if (text.contains("红石")) + types.add("REDSTONE"); + if (text.contains("铜")) + types.add("RAW_COPPER"); + if (text.contains("下界石英") + || text.contains("石英")) + types.add("QUARTZ"); + return types; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + * "打卡村庄" → "村庄" + * "打卡沙漠神殿" → "沙漠" + */ + private String extractBiome(String text) { + if (text == null) return ""; + + // 优先匹配结构名称(更具体) + String structureResult = + mapStructureCategory(text); + if (!structureResult.isEmpty()) { + return structureResult; + } + + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点") + || key.contains("结构") + || key.contains("村庄") + || key.contains("神殿") + || key.contains("神庙") + || key.contains("小屋") + || key.contains("雪屋") + || key.contains("府邸") + || key.contains("前哨站") + || key.contains("矿井") + || key.contains("要塞") + || key.contains("城市") + || key.contains("废墟") + || key.contains("密室") + || key.contains("废墟") + || key.contains("沉船") + || key.contains("宝箱") + || key.contains("传送门") + || key.contains("化石") + || key.contains("堡垒"))) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; + } + return getCounter( + player, "biome_ALL") >= 1; + } + + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "VILLAGER_TRADE") + >= needed; + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "REPORT_VILLAGE") + >= needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return streak >= needed; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + // ===== 泛解析:击杀类中文条件 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + for (String mobType : mobTypes) { + total += getCounter(player, + "KILL:" + mobType); + } + return total >= needed; + } + } + + // ===== 泛解析:村民交易条件 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "TRADE") + >= needed; + } + + // ===== 泛解析:采集/挖矿类条件 ===== + if (key.contains("采集") || key.contains("挖")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + for (String mat : matTypes) { + total += getCounter(player, + "MINE:" + mat); + } + return total >= needed; + } + } + + // ===== 泛解析:合成类条件 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "CRAFT") + >= needed; + } + + // ===== 泛解析:钓鱼类条件 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "FISH") + >= needed; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public void onVillagerTrade(String player) { + String key = "VILLAGER_TRADE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/backup/README.md b/backup/README.md new file mode 100644 index 0000000..e74ef6a --- /dev/null +++ b/backup/README.md @@ -0,0 +1,569 @@ +# Sdf1系列登录插件使用说明 + +# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改! + +## 1. 功能介绍 + +| 名字 | 描述/作用 | 施工状态 | +| :--- | :--- | :--- | +| 登录 | 用户登录/注册 | 已完成 | +| 世界垃圾箱 | 清理服务器凋落物 | 已完成 | +| 工单系统 | 用户提交工单 | 已完成 | +| 签到系统 | 签到积分换奖励 | 已完成 | +| 新人礼包 | 新玩家7日礼包 | 已完成 | +| 挂机检测 | 挂机检测踢出 | 已完成 | +| 自定义菜单 | 可以自己diy指令 | 已完成 | +| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 | +| 商城系统| 内置游戏商人可供销售商品 | 已完工 | +| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 | + + +## 2.安装说明: + +| 前置插件 | 选装类型 | 功能作用 | +| :--- | :--- | :--- | +| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 +| EssentialsX | 可选 | 主流经济系统 +| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统 + + +### 2.1 下载 +您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。 + +### 2.2. 配置文件 + +#### 2.2.1 消息.txt +此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)
+同时兼容` `标签,对段落加粗,`\n 和
`进行段落换行 + + +```txt +# ========================================== +# Sdf1_login 消息配置文件 +# ========================================== +# 颜色代码使用 § 符号 +# §0黑色 §1深蓝 §2深绿 §3深青 +# §4深红 §5深紫 §6金色 §7灰色 +# §8深灰 §9蓝色 §a绿色 §b浅蓝 +# §c红色 §d粉色 §e黄色 §f白色 +# §l粗体 §n下划线 §o斜体 §r重置 +# +# 占位符用 {xxx} 表示,运行时自动替换 +# 支持 粗体 标签 +# 支持 \n 或
换行 +# 修改后执行 /sdf1_login reload 重载 +# ========================================== + +# ---- 登录注册相关 ---- +# login_timeout: 登录超时被踢出时的提示 +login_timeout=§c登录超时,请重新登录 +# not_registered: 未注册玩家收到的提示 +not_registered=§e您尚未注册,请使用 /reg <密码> +# not_logged_in: 已注册但未登录的提示 +not_logged_in=§e您尚未登录,请使用 /login <密码> +# already_registered: 已注册玩家再次注册时的提示 +already_registered=§c您已注册过账号 +# already_logged_in: 已登录玩家再次登录时的提示 +already_logged_in=§c您已登录 +# reg_success: 注册成功提示 +reg_success=§a注册成功!欢迎来到服务器 +# login_success: 登录成功提示 +login_success=§a登录成功! +# login_failed: 密码错误提示 +login_failed=§c密码错误,请重试 + +# ---- 密码相关 ---- +# password_wrong: 旧密码验证失败 +password_wrong=§c密码错误 +# password_changed: 密码修改成功 +password_changed=§a密码修改成功! +# password_format_error: 密码格式不符合要求 +password_format_error=§c密码需6位以上,含大小写字母和数字 +# password_same: 新密码与旧密码相同 +password_same=§c新密码不能与旧密码相同 +# reset_no_email: 找回密码时未绑定邮箱 +reset_no_email=§c您未绑定邮箱,无法找回密码 +# reset_sent: 临时密码已发送到邮箱 +reset_sent=§a临时密码已发送到您的邮箱,请查收 + +# ---- 签到积分相关 ---- +# checkin_already: 今日已签到 +checkin_already=§c您今日已签到 +# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率 +checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi} +# points_insufficient: 积分不足 +points_insufficient=§c积分不足,当前: {points} +# points_purchase_success: 积分购买成功 +points_purchase_success=§a购买成功!消耗 {count} 积分 + +# ---- 挂机检测相关 ---- +# afk_set_enabled: 挂机检测开启 +afk_set_enabled=§a挂机检测已开启 +# afk_set_disabled: 挂机检测关闭 +afk_set_disabled=§c挂机检测已关闭 +# afk_kick: 因挂机被踢出 +afk_kick=§c因挂机过久被踢出 + +# ---- 邮箱相关 ---- +# email_set: 邮箱设置成功 +email_set=§a邮箱设置成功:{email} +# email_format_error: 邮箱格式错误 +email_format_error=§c邮箱格式不正确 + +# ---- 邀请相关 ---- +# invite_code_generated: 邀请码生成 +invite_code_generated=§a邀请码:{code} +# invite_used: 使用邀请码成功 +invite_used=§a邀请成功,邀请人:{player} +# invite_self: 使用自己的邀请码 +invite_self=§c不能使用自己的邀请码 +# invite_not_found: 邀请码不存在 +invite_not_found=§c邀请码不存在 +# invite_referral_bonus: 被邀请人签到后邀请人获得奖励 +invite_referral_bonus=§a邀请奖励!获得 {points} 积分 + +# ---- 新人礼包相关 ---- +# gift_not_ready: 礼包条件未满足 +gift_not_ready=§c礼包条件未满足 +# gift_claimed: 礼包领取成功 +gift_claimed=§a礼包领取成功! + +# ---- 审批与IP限制 ---- +# need_approval: 注册需审批 +need_approval=§e注册需审批 (工单#{id}) +# ip_max_accounts: IP注册已达上限 +ip_max_accounts=§cIP注册已达上限 + +# ---- 管理员操作 ---- +# admin_delete_confirm: 管理员删除账号确认提示 +admin_delete_confirm=§c请在聊天中输入玩家名确认删除 + +# ---- 聊天过滤 ---- +# chat_muted: 禁言玩家尝试聊天 +chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天 +# chat_url_blocked: 发送违规链接被拦截 +# {url}=检测到的链接地址 +chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url} +# chat_url_violation: 当前违规次数 +# {count}=当前违规次数 +chat_url_violation=§7当前违规次数: §e{count} +# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数 +chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次) +# chat_url_broadcast: 全服通报,{player}=玩家名 +chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚 + +# ---- 通用 ---- +# 未知参数: 未知命令或权限不足时的提示 +未知参数=§c未知参数或权限不足 +# only_player: 仅玩家可用的命令 +# only_player=§c此命令仅玩家可用 + +``` + +### 2.2.2 挂机白名单.txt +当挂机踢出开启时,该配置下的玩家不受挂机提出影响 + +### 2.2.3 SMTP设置.txt + +此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。
可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效 + +```txt +# ===== SMTP 邮件服务器配置 ===== +smtp地址=smtp.example.com +smtp端口=465 +smtp账号=mail@example.com +smtp密码=example.com +发件人名称=Sdf1_login +smtp加密=true +验证码接收邮箱=example@example.com + +``` + +### 2.2.4 插件设置.txt + +此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等 + +```txt +# ===== Sdf1_login 插件设置 ===== +管理标签=admin +管理密码=qweasd +每IP最大账号数=3 +审批模式=manual +自动审批延迟分钟=30 +登录超时秒数=60 +挂机踢出=false +挂机超时秒数=300 +服务商积分倍率=2.0 +基础经济奖励=100 +经济奖励每评分=50.0 +报单人积分奖励=10 +# ---- 签到盲盒奖励 ---- +签到固定金额=0 +签到最小金额=10 +签到最大金额=100 +签到给积分=true +签到积分数量=10 + + +``` + +### 2.2.5 chat.txt +该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。
+可用的处罚规则: +- warn: 警告 +- mute: 禁言 +- kick: 踢出 +- ban: 封禁 +- banip:封禁IP + +```txt +# Sdf1_chat 聊天过滤配置 +通知管理员: true +全服通报: true +禁言时长: 300 + +白名单: +*.minecraft.net +baidu.com +github.com + +处罚规则: +1:warn:0 +2:warn:0 +3:mute:300 +5:mute:600 +8:kick:0 +10:ban:0 + +白名单玩家: + +``` +### 2.2.6 radio_config.txt +```txt +# ============ Sdf1 Radio Config ============ +# +# resource-pack-url +# 填外部地址(HTTP或HTTPS均可) +# 例: https://example.com/radiopack.zip +# 留空 = 使用内置HTTP服务 +# +# http-port +# 内置HTTP端口(仅resource-pack-url为空时生效) +# +# ============ Settings =============== + +resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc +http-port=443 +``` +- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了 + +### 2.2.7 菜单.txt +- 自定义菜单配置文件 + +```json +# 服务器菜单 +# 使用类json格式 +// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持的块注释 + +{ + 标题:主城传送 + 指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录 + 权限类型:玩家 + 图标:COMPASS +} + +{ + 标题:随机传送 + 指令:/rtp + 权限类型:玩家 + 图标:ENDER_PEARL +} + +{ + 标题:擂台比武 + 指令:/dom tp PVP + 权限类型:玩家 + 图标:netherite_spear +} + +{ + 标题:领地操作 + 指令:/dom + 权限类型:玩家 + 图标:paper +} +{ + 标题:逛商店 + 指令:/dom tp shop + 权限类型:玩家 + 图标:honeycomb +} + +{ + 标题:传送开关 + 指令:/tptoggle + 权限类型:玩家 + 图标:redstone_torch +} + +{ + 标题:返回上一个位置 + 指令:/back + 权限类型:玩家 + 图标:red_bed +} +{ + 标题:前往小黑塔 + 指令:/dom tp 小黑塔 + 权限类型:玩家 + 图标:dragon_head +} +{ + 标题:云背包 + 指令:/cy + 权限类型:玩家 + 图标:potato +} +{ + 标题:自杀 + 指令:/suicide + 权限类型:玩家 + 图标:wooden_axe +} +{ + 标题:游戏商人 + 指令:/shop + 权限类型:玩家 + 图标:shield + 格子:28 +} +``` +### 2.2.8 商品配置 +- 配置此处的文件,负责展示游戏内所销售的商品。
+- 一个文件名,对应一个商品分类,使用md文件格式 +``` +# 建筑材料 + +| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 | +| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 | +| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 | +| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 | +| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 | +| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 | +| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 | +| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 | + +``` + +## 2.3 商城部分介绍 +1. 自助退款,买多买错,不用慌,一件退单很安心
+2. 购物车,买的多怕买错,先加购物车,后续统一结算
+3. 害怕订单找不到?一键查看历史订单
+ ![](img/shop4.png) +4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖 +![](img/shop7.png)
+![](img/shop6.png) +5. 想查看玩家消费记录?小手一点,明明白白
+![](img/shop5.png) + + + + +## 2.4 打印流水部分介绍 +1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天
+2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删 + + + +## 3. 指令参数及介绍 + +- Sdf1_login 主指令
+- Sdf1_login reload 重载配置文件
+- reg 注册账号
+- login 登录账号
+- l 登录账号
+- sdf1_login del 删除玩家账号
+- sdf1_login radio 停止播放广播
+- sdf1_login radio reload 重载广播并重新打包资源包
+- menu 开/关 是否开启菜单
+- printer 打印流水数据,支持不带参数&带玩家名为参数
+- sdf1_login printer 作用同printer
+- sdf1_login shop 商城 +- protect 区域防护 + + + +## 4. 自定义菜单物品 +- 打开主界面,选择"我的"进入
+![](img/menu1.png) +- 将你的自定义物品与雪球交换
+![](img/menu2.png) +- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了 + +## 5. pvp战神榜 +- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域
+![](img/pvp1.png) +- 并且有着网游同款的击杀通报
+![](img/pvp3.png) +- 播报逻辑是:
+- 3杀
+- 5杀
+- 5杀以上每杀一次都是一条播报
+- 终结者通报,触发连杀的玩家被终结也会有全服通报 + +## 6. 区域防护 +- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。
+- 还能够保护指定区域的玩家不受任何伤害。
+ +| 指令 | 参数1| 参数2 | 参数3 | 结果 | +| :---: | :---: | :---: | :---: | :---: | +| protect | 无 | 无 | 无 | 帮助信息 | +| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 | +| protect | add | 玩家名 | 无| 加白指定玩家为全局 | +| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 | +| protect | additem | 物品名 | 无 | 加黑指定物品到全局 | +| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 | +| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 | +| protect | remove | 玩家名字 | 无 | 移除玩家为全白 | +| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 | +| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑 +| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑 +| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 | +| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 | +| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 | + +示范: +- `/protect add 末地保护区 players` 添加players玩家到末地保护区
+- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品
+- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。
+- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写 + + +| 参数 | 效果 | +| :---: | :---: | +| 和平模式 | 所有敌对生物全部进虚空 | +| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 | +| 清除XX效果 | 立即清除玩家身上的效果 +| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果 + +## 生物属性对照表 +| 中文名称 | 类型 | 附加属性 | +| :---: | :----: | :----: | +| 溺尸 | 近战/远程 | 亡灵、水生 | +| 骷髅 | 远程 | 亡灵 | +| 流浪者 | 远程 | 亡灵、寒冷群系变种 | +| 沼骸 | 远程 | 亡灵、沼泽变种 | +| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 | +| 掠夺者 | 远程 | 灾厄村民、袭击成员 | +| 卫道士 | 近战 | 灾厄村民、袭击成员 | +| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 | +| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 | +| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 | +| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 | +| 守卫者 | 近战/激光远程 | 海底神殿、水下 | + + +配置文件解析:
+- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的注释`和`Java/* */块注释` +```txt +# 区域: 主城区 +起点: world,-130,-713 +终点: world,-427,-560 +高度范围: 0-255 + +没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个) + // 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块 +// 禁止破坏方块 # 同上 +禁止PVP # 玩家之间不能PVP +禁止摔伤 # 摔落无法造成任何伤害 +禁止饥饿 # 不会消耗饱食度 +禁止一切伤害 # 任何怪物都无法造成伤害 +# 禁止丢弃物品 +# 禁止末影珍珠 +# 禁止使用弓箭 +# 禁止骑乘 +// 和平模式 // 所有的敌对生物全部传送进虚空 +禁止袭击 +禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外 +// 清除所有效果 # 清理所有负面效果 +效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间 +效果 生命提升 255 999999 +效果 抗性提升 255 999999 +效果 抗火 255 999999 +# 禁止药水 +禁止爆炸 +禁止使用物品: TNT +# 效果: 夜视 1 999 +进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示 +离开提示: 已离开保护区 + 没收提示: 你的违禁品已被没收 + 通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域 +# 惩罚命令: ban {player} 60s 违规 + + +``` + +## 运行截图 + +主界面 +![](img/login.png) + +邀请数据 +![](img/login2.png) + +积分商店 +![](img/login3.png) + +任务中心 +![](img/login4.png) + +垃圾箱 +![](img/login5.png) + +管理员面板 +![](img/login6.png) + +管理面板-用户管理 +![](img/login7.png) + + +工单中心 +![](img/login8.png) + +工单管理 +![](img/login9.png) + +我的工单 +![](img/login10.png) + + +提交工单 +![](img/login11.png) + +服务商列表 +![](img/login12.png) + +服务商抢单大厅 +![](img/login13.png) + +服务商-接单界面 +![](img/login14.png) + +管理员-工单详情页面 +![](img/login15.png) + +商城主界面 +![](img/shop1.png) + +商城购物车 +![](img/cart1.png) + +商城退单 +![](img/shop2.png) + + +## 4. 下载地址 +- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login) +- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login) diff --git a/backup/img/cart1.png b/backup/img/cart1.png new file mode 100644 index 0000000..ff9a1c6 Binary files /dev/null and b/backup/img/cart1.png differ diff --git a/backup/img/login.png b/backup/img/login.png new file mode 100644 index 0000000..b9433b3 Binary files /dev/null and b/backup/img/login.png differ diff --git a/backup/img/login1.png b/backup/img/login1.png new file mode 100644 index 0000000..4f54df9 Binary files /dev/null and b/backup/img/login1.png differ diff --git a/backup/img/login10.png b/backup/img/login10.png new file mode 100644 index 0000000..6b083c2 Binary files /dev/null and b/backup/img/login10.png differ diff --git a/backup/img/login11.png b/backup/img/login11.png new file mode 100644 index 0000000..c1220c1 Binary files /dev/null and b/backup/img/login11.png differ diff --git a/backup/img/login12.png b/backup/img/login12.png new file mode 100644 index 0000000..b561550 Binary files /dev/null and b/backup/img/login12.png differ diff --git a/backup/img/login13.png b/backup/img/login13.png new file mode 100644 index 0000000..1255d6b Binary files /dev/null and b/backup/img/login13.png differ diff --git a/backup/img/login14.png b/backup/img/login14.png new file mode 100644 index 0000000..7b4f091 Binary files /dev/null and b/backup/img/login14.png differ diff --git a/backup/img/login15.png b/backup/img/login15.png new file mode 100644 index 0000000..a3d00a4 Binary files /dev/null and b/backup/img/login15.png differ diff --git a/backup/img/login2.png b/backup/img/login2.png new file mode 100644 index 0000000..03dc32c Binary files /dev/null and b/backup/img/login2.png differ diff --git a/backup/img/login3.png b/backup/img/login3.png new file mode 100644 index 0000000..b12a88b Binary files /dev/null and b/backup/img/login3.png differ diff --git a/backup/img/login4.png b/backup/img/login4.png new file mode 100644 index 0000000..ed32c55 Binary files /dev/null and b/backup/img/login4.png differ diff --git a/backup/img/login5.png b/backup/img/login5.png new file mode 100644 index 0000000..fa8fd67 Binary files /dev/null and b/backup/img/login5.png differ diff --git a/backup/img/login6.png b/backup/img/login6.png new file mode 100644 index 0000000..956d0bb Binary files /dev/null and b/backup/img/login6.png differ diff --git a/backup/img/login7.png b/backup/img/login7.png new file mode 100644 index 0000000..15fc70e Binary files /dev/null and b/backup/img/login7.png differ diff --git a/backup/img/login8.png b/backup/img/login8.png new file mode 100644 index 0000000..57189c2 Binary files /dev/null and b/backup/img/login8.png differ diff --git a/backup/img/login9.png b/backup/img/login9.png new file mode 100644 index 0000000..fbd43f7 Binary files /dev/null and b/backup/img/login9.png differ diff --git a/backup/img/menu1.png b/backup/img/menu1.png new file mode 100644 index 0000000..b056be9 Binary files /dev/null and b/backup/img/menu1.png differ diff --git a/backup/img/menu2.png b/backup/img/menu2.png new file mode 100644 index 0000000..888cbf2 Binary files /dev/null and b/backup/img/menu2.png differ diff --git a/backup/img/pvp1.png b/backup/img/pvp1.png new file mode 100644 index 0000000..9efe88b Binary files /dev/null and b/backup/img/pvp1.png differ diff --git a/backup/img/pvp3.png b/backup/img/pvp3.png new file mode 100644 index 0000000..3b56cc9 Binary files /dev/null and b/backup/img/pvp3.png differ diff --git a/backup/img/shop1.png b/backup/img/shop1.png new file mode 100644 index 0000000..eb0ea8e Binary files /dev/null and b/backup/img/shop1.png differ diff --git a/backup/img/shop2.png b/backup/img/shop2.png new file mode 100644 index 0000000..a083df0 Binary files /dev/null and b/backup/img/shop2.png differ diff --git a/backup/img/shop4.png b/backup/img/shop4.png new file mode 100644 index 0000000..fe59548 Binary files /dev/null and b/backup/img/shop4.png differ diff --git a/backup/img/shop5.png b/backup/img/shop5.png new file mode 100644 index 0000000..9511132 Binary files /dev/null and b/backup/img/shop5.png differ diff --git a/backup/img/shop6.png b/backup/img/shop6.png new file mode 100644 index 0000000..e69fedd Binary files /dev/null and b/backup/img/shop6.png differ diff --git a/backup/img/shop7.png b/backup/img/shop7.png new file mode 100644 index 0000000..30a9e8d Binary files /dev/null and b/backup/img/shop7.png differ diff --git a/backup/pom.xml b/backup/pom.xml new file mode 100644 index 0000000..9d5c185 --- /dev/null +++ b/backup/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + sdf1 + Sdf1_login + 1.0 + jar + + + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21.4-R0.1-SNAPSHOT + provided + + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + org.xerial + sqlite-jdbc + 3.46.0.0 + provided + + + + + + Sdf1_login + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + + diff --git a/backup/src/main/java/Sdf1_login/AFKManager.java b/backup/src/main/java/Sdf1_login/AFKManager.java new file mode 100644 index 0000000..f714c1b --- /dev/null +++ b/backup/src/main/java/Sdf1_login/AFKManager.java @@ -0,0 +1,197 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; + +public class AFKManager { + + // 玩家最后活动时间(空闲才开始计时) + private final Map lastAction = + new HashMap<>(); + // 每个玩家当前的警告阶段 + private final Map lastStage = + new HashMap<>(); + // 是否已进入挂机检测(false=还在活动,不计时) + private final Map afkTracking = + new HashMap<>(); + + private final ConfigManager config; + private final JavaPlugin plugin; + + public AFKManager(ConfigManager config, + JavaPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public void startCheck() { + new BukkitRunnable() { + public void run() { + tick(); + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void tick() { + if (!config.afkEnabled) return; + long now = System.currentTimeMillis(); + + for (Player p : Bukkit.getOnlinePlayers()) { + String name = p.getName(); + UUID uuid = p.getUniqueId(); + + if (!isLoggedIn(name)) continue; + if (config.getAfkWhitelist().contains(name)) + continue; + + // 确保有记录 + if (!lastAction.containsKey(uuid)) { + lastAction.put(uuid, now); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + long last = lastAction.get(uuid); + long idle = (now - last) / 1000; + int timeout = config.afkTimeout; + + // 还在活动范围内:不进入挂机检测 + if (idle < 60) { + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + // 超过60秒无活动:开始挂机检测 + afkTracking.put(uuid, true); + long remain = timeout - idle; + + // 已超时:踢出 + if (remain <= 0) { + p.kickPlayer("§c§l你已挂机 " + + fmtSec(idle) + ",被踢出"); + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + continue; + } + + int stage = lastStage.getOrDefault(uuid, 0); + + // 阶段3:最后10秒 + if (remain <= 10) { + if (stage < 3) { + lastStage.put(uuid, 3); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + remain + " 秒踢出", + 0, 22, 3); + playSound(p); + continue; + } + + // 阶段2:1分钟~10秒 + if (remain <= 60) { + if (stage < 2) { + lastStage.put(uuid, 2); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + fmtSec(remain) + " 踢出", + 5, 25, 5); + playSound(p); + continue; + } + + // 阶段1:3分钟提醒 + if (remain <= 180 && stage < 1) { + lastStage.put(uuid, 1); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§f还剩 " + fmtSec(remain) + " 踢出", + 10, 60, 20); + for (int i = 0; i < 3; i++) { + final int delay = i * 10; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + if (p.isOnline()) playSound(p); + }, delay); + } + } + + if (remain > 180) { + lastStage.remove(uuid); + } + } + } + + /** + * 玩家有活动时调用(移动/交互/放置/挖掘等) + * 重置计时,退出挂机检测 + */ + public void recordAction(UUID uuid) { + lastAction.put(uuid, System.currentTimeMillis()); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + } + + /** + * 玩家下线时调用 + * 彻底停止计时,清理所有数据 + */ + public void remove(UUID uuid) { + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + } + + /** + * 获取玩家当前是否在挂机检测中 + */ + public boolean isAfkTracking(UUID uuid) { + return afkTracking.getOrDefault(uuid, false); + } + + private boolean isLoggedIn(String name) { + return ((Main) plugin).getLoggedIn() + .contains(name); + } + + private void playSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private String fmtSec(long sec) { + if (sec < 60) return sec + "秒"; + long m = sec / 60; + long s = sec % 60; + return m + "分" + (s > 0 ? s + "秒" : ""); + } +} diff --git a/backup/src/main/java/Sdf1_login/AccountRequestManager.java b/backup/src/main/java/Sdf1_login/AccountRequestManager.java new file mode 100644 index 0000000..ae5eaad --- /dev/null +++ b/backup/src/main/java/Sdf1_login/AccountRequestManager.java @@ -0,0 +1,146 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AccountRequestManager { + + public enum ApprovalMode { AUTO, MANUAL, HYBRID } + + public static class Request { + public final int id; + public final String requester; + public final String target; + public final String ip; + public final long time; + public String status; + public String denyReason; + + public Request(int id, String requester, String target, String ip) { + this.id = id; + this.requester = requester; + this.target = target; + this.ip = ip; + this.time = System.currentTimeMillis(); + this.status = "PENDING"; + this.denyReason = ""; + } + } + + private final Main plugin; + private final Map requests = new ConcurrentHashMap<>(); + private final Map pendingByPlayer = new ConcurrentHashMap<>(); + private int nextId = 1; + private ApprovalMode mode = ApprovalMode.MANUAL; + private int autoDelayMinutes = 60; + + public AccountRequestManager(Main plugin) { + this.plugin = plugin; + } + + public ApprovalMode getMode() { return mode; } + + public void setMode(String m) { + try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } + catch (Exception e) { this.mode = ApprovalMode.MANUAL; } + } + + public void setAutoDelayMinutes(int min) { + this.autoDelayMinutes = min; + } + + public Request createRequest(String requester, String target, String ip) { + int id = nextId++; + Request req = new Request(id, requester, target, ip); + requests.put(id, req); + pendingByPlayer.put(requester, id); + + if (mode == ApprovalMode.AUTO) { + scheduleAutoApprove(id); + } else if (mode == ApprovalMode.HYBRID) { + String targetIp = (String) plugin.getDb().getField(target, "ip_address"); + if (ip != null && ip.equals(targetIp)) { + scheduleAutoApprove(id); + } + } + return req; + } + + private void scheduleAutoApprove(int id) { + long delayTicks = (long) autoDelayMinutes * 60L * 20L; + Request req = requests.get(id); + if (req == null) return; + String requesterName = req.requester; + String targetName = req.target; + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Request r = requests.get(id); + if (r != null && "PENDING".equals(r.status)) { + approveRequest(id); + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); + Player requester = Bukkit.getPlayer(requesterName); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); + } + } + }, delayTicks); + } + + public boolean approveRequest(int id) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "APPROVED"; + plugin.getDb().deleteUser(req.target); + pendingByPlayer.remove(req.requester); + // 清背包+传送出生点+踢出 + Player target = Bukkit.getPlayer(req.target); + if (target != null && target.isOnline()) { + target.getInventory().clear(); + target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); + target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); + target.teleport(target.getWorld().getSpawnLocation()); + target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); + } + return true; + } + + + public boolean denyRequest(int id, String reason) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "DENIED"; + req.denyReason = reason; + pendingByPlayer.remove(req.requester); + return true; + } + + public List getPendingRequests() { + List list = new ArrayList<>(); + for (Request r : requests.values()) { + if ("PENDING".equals(r.status)) list.add(r); + } + return list; + } + + public Request getRequest(int id) { + return requests.get(id); + } + + public boolean hasPending(String player) { + return pendingByPlayer.containsKey(player); + } + + public void cancelPending(String player) { + Integer id = pendingByPlayer.remove(player); + if (id != null) { + Request r = requests.get(id); + if (r != null) r.status = "CANCELLED"; + } + } +} diff --git a/backup/src/main/java/Sdf1_login/AreaProtection.java b/backup/src/main/java/Sdf1_login/AreaProtection.java new file mode 100644 index 0000000..bc53d53 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/AreaProtection.java @@ -0,0 +1,4907 @@ +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.entity.Entity; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import java.sql.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +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 java.util.Set; +import java.util.HashSet; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.entity.Item; + + + + +public class AreaProtection implements Listener { + + 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 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<>(); + + // 选地 + private static final Material WAND = Material.BREEZE_ROD; + + private final Map pos1 = + new ConcurrentHashMap<>(); + private final Map pos2 = + new ConcurrentHashMap<>(); + private final Set selecting = + ConcurrentHashMap.newKeySet(); + 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<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + + + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + 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 (p.isOp()) { + plugin.getLogger().info( + "[防护-调试] 豁免原因: OP, 玩家=" + name); + return true; + }*/ + if (globalPlayerWhitelist.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 全局白名单, 玩家=" + + name);*/ + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (aw != null && aw.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 区域白名单, 玩家=" + + name + " 区域=" + areaName);*/ + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 模式排除名单, 玩家=" + + name);*/ + return true; + } + /* plugin.getLogger().info( + "[防护-调试] 无豁免, 玩家=" + name);*/ + 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(); + loadAllAreas(); + loadWhitelists(); + recoverPendingEffects(); + } + 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); + } + + +// ==================== 数据库迁移 ==================== + + /** + * 如果旧位置有 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("========================================"); + } + +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // & → § + 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", "\n"); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + int i = 0; + + while (i < msg.length()) { + 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(); + if (!stack.isEmpty()) { + // 重置后重新应用外层格式 + sb.append("§r"); + for (char c : stack) { + sb.append("§").append(c); + } + } else { + sb.append("§r"); + } + } + 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": return 'l'; + case "i": return 'o'; + case "u": return 'n'; + case "s": return 'm'; + default: return 0; + } + } + +// =============== 加载 ==================== + + public void loadAllAreas() { + areas.clear(); + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files == null) return; + for (File f : files) { + try { + String name = f.getName() + .replace(".txt", ""); + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载失败: " + f.getName()); + } + } + } + + + 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)"); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 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 == '\u200B' // 零宽空格 + || c == '\u200C' // 零宽非连接 + || c == '\u200D' // 零宽连接 + || c == '\uFEFF' // BOM + || c == '\u00A0' // 不间断空格 + || c == '\u2028' // 行分隔符 + || c == '\u2029' // 段分隔符 + || c == '\u200E' // LTR标记 + || c == '\u200F' // RTL标记 + || c == '\u2060' // 字连接符 + || (c >= '\u2000' && c <= '\u200A')) { // 各种空格 + continue; + } + + // 全角→半角映射 + if (c >= '\uFF01' && c <= '\uFF5E') { + c = (char) (c - 0xFEE0); + } + + // 全角空格→半角空格 + if (c == '\u3000') { + 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.denyDrop = 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("和平模式") + && !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() + 5000); + + /* 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); + /* plugin.getLogger().info("[和平] " + p.getName() + + " 附近实体总数=" + nearby.size());*/ + for (Entity ent : nearby) { + String typeName = ent.getType().name(); + /* plugin.getLogger().info("[和平] 实体: " + + typeName + " isHostile=" + isHostile(typeName));*/ + } + int scanned = 0; + int waiting = 0; + int saved = 0; + int banished = 0; + + for (Entity ent : p.getNearbyEntities(48, 48, 48)) { + if (!isHostile(ent.getType().name())) continue; + scanned++; + + UUID entUid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(entUid); + String customName = ent.getCustomName(); + + // 有保护期记录 + if (protectUntil != null) { + long remain = protectUntil - now; + + if (remain > 0) { + // 保护期内,精确剩余秒数 + int secondsLeft = (int) Math.ceil(remain / 1000.0); + waiting++; + + // 保护期内也检查名字(玩家可能刚贴上) + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + protectedEntities.remove(entUid); + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 白名单匹配,永久保留");*/ + continue; + } + + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 保护期剩余约" + secondsLeft + "秒");*/ + continue; + } + + // 保护期到了,最终检查 + protectedEntities.remove(entUid); + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + " 5秒到期");*/ + + // 到期时再查一次白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 到期但白名单匹配,保留");*/ + continue; + } + + // 没名字或不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名=[" + customName + "] 传送虚空");*/ + continue; + } + + // 没有保护期记录(非刷怪蛋生成的或记录被清了) + // 也检查一下白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 无保护期但白名单匹配,保留");*/ + continue; + } + + // 没有保护期且不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 无保护期,命名=[" + customName + "] 传送虚空");*/ + } + + /* plugin.getLogger().info("[和平] " + + " 扫描=" + scanned + + " 等待=" + waiting + + " 保留=" + saved + + " 传送=" + banished + + " 白名单=" + ac.peaceWhitelist);*/ + } + + 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" + )); + + 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; + } + + // ==================== 白名单检查 ==================== + + private boolean isPlayerWhitelisted(String player, + AreaConfig ac) { + if (globalPlayerWhitelist.contains( + player.toLowerCase())) + return true; + Set areaList = + areaPlayerWhitelist.get(ac.name); + return areaList != null + && areaList.contains(player.toLowerCase()); + } + + 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 taskId = Bukkit.getScheduler() + .runTaskTimer(plugin, new Runnable() { + @Override + public void run() { + if (!p.isOnline()) return; + Location loc = p.getLocation(); + int px = loc.getBlockX(); + int py = loc.getBlockY(); + int pz = loc.getBlockZ(); + 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); + for (int dy = 5; dy <= 30; dy += 5) { + for (int x = minX; x <= maxX; x += 4) { + for (int z = minZ; z <= maxZ; z += 4) { + if (x == minX || x == maxX + || z == minZ || z == maxZ) { + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + } + } + } + } + } + }, 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; + + Location from = event.getFrom(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 防抖 + 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) { + // ★ 每次移动都清除指定效果 + clearBadEffects(p, 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); + + // 离开提示 + if (oldAc != null && oldAc.leaveMsg != null + && !oldAc.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(oldAc.leaveMsg)); + } + + // 收标 + 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 (marked != null && !marked.isEmpty()) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★创建延时任务: " + + toClear.size() + "个");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-移动-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + plugin.getLogger().info( + "[防护-移动-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size()); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + /* plugin.getLogger().info( + "[防护-移动-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] + cleared" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + 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) { + if (newAc.enterMsg != null + && !newAc.enterMsg.isEmpty()) { + p.sendMessage( + formatAreaMsg(newAc.enterMsg)); + } + 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()); + } + } + } + +// ===== 清除所有效果 ===== + if (newAc.clearAllBadEffects) { + for (PotionEffect eff : p.getActivePotionEffects()) { + p.removePotionEffect(eff.getType()); + } + plugin.getLogger().info( + "[防护] ★清除所有效果, 玩家=" + p.getName()); + } + + 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); + } + } +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + 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 || ac.giveEffects.isEmpty()) return; + if (ac.giveEffects.isEmpty()) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + for (String[] eff : ac.giveEffects) { + 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); + } + + // 给效果 + 贴标 + List applied = new ArrayList<>(); + for (String[] eff : ac.giveEffects) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + int lv = Integer.parseInt(eff[1]) - 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) { + } + } + + // 贴标 + // 新代码(合并,不覆盖): + 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());*/ + } + } + + /** + * 离开区域时,延时15秒清理效果 + * 流程:收标 → 15秒后清效果 → 确认清空 → 清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); + + // 启动15秒延时 + 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); + } + } + + // 确认清空后清DB + removePlayerEffects(uid, areaName); + plugin.getLogger().info( + "[防护] 15秒已到, 清除 " + + (marked != null ? marked.size() : 0) + + " 个效果 + DB, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + }.runTaskLater(plugin, 300L); // 15秒 = 300 tick + + pendingClearTask.put(uid, task); + } + public Set getAllAreaNames() { + return areas.keySet(); + } + + /** + * 15秒内进了新区域: + * 清除旧区域有、新区域没有的效果 + */ + 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) { + for (String[] eff : newAc.giveEffects) { + 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 都跳过 + */ + private boolean isExemptFromConfiscation( + Player p, String areaName) { + String name = p.getName(); + + if (globalPlayerWhitelist.contains(name)) + return true; + + Set areaWl = + areaPlayerWhitelist.get(areaName); + if (areaWl != null && areaWl.contains(name)) + return true; + + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) + return true; + + return false; + } + + /** + * 没收玩家背包中的违禁品 + */ + private void confiscateItems(Player p, + AreaConfig ac) { + boolean confiscated = false; + + for (int i = 0; + i < p.getInventory().getSize(); i++) { + ItemStack item = p.getInventory().getItem(i); + if (item == null) continue; + + String typeName = item.getType().name(); + if (ac.confiscateItems + .contains(typeName)) { + p.getInventory().setItem(i, null); + confiscated = true; + } + } + + if (confiscated) { + String msg = ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty() + ? ac.confiscateMsg + : "§c你的违禁品已被没收"; + p.sendMessage(msg); + + // 通报批评 + if (ac.enableAnnounce + && ac.announceTemplate != null) { + String text = ac.announceTemplate + .replace("{player}", + p.getName()); + Bukkit.broadcastMessage(text); + } + } + } + + + + private String getChineseGameMode(GameMode mode) { + switch (mode) { + case SURVIVAL: return "生存模式"; + case CREATIVE: return "创造模式"; + case ADVENTURE: return "冒险模式"; + case SPECTATOR: return "旁观模式"; + default: return mode.name(); + } + } + + private void handleEnter(Player p, AreaConfig ac) { + UUID uid = p.getUniqueId(); + + // 清除负面效果 + clearBadEffects(p, ac); + + // 给予效果 + List given = new ArrayList<>(); + for (String[] parts : ac.giveEffects) { + 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) {} + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + given.add(type); + } + } + + removePlayerEffects(uid, ac.name); + if (!given.isEmpty()) { + // savePlayerEffects(uid, p.getName(), ac.name, given); + } + + // 白名单检查(最先执行) + if (isPlayerWhitelisted(p.getName(), ac)) 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; + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return true; + } + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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; + + // 去掉"效果"后缀 + String clean = name; + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + + // 硬编码中文名→英文ID映射(不依赖任何外部Map) + switch (clean) { + // 负面 + case "缓慢": + return resolveEffectType("slowness"); + case "挖掘疲劳": + return resolveEffectType("mining_fatigue"); + case "瞬间伤害": + return resolveEffectType("instant_damage"); + case "反胃": + return resolveEffectType("nausea"); + case "失明": + return resolveEffectType("blindness"); + case "饥饿": + return resolveEffectType("hunger"); + case "虚弱": + return resolveEffectType("weakness"); + case "中毒": + return resolveEffectType("poison"); + case "凋零": + return resolveEffectType("wither"); + case "飘浮": + return resolveEffectType("levitation"); + case "霉运": + return resolveEffectType("unluck"); + case "黑暗": + return resolveEffectType("darkness"); + case "蓄风": + return resolveEffectType("wind_charged"); + case "盘丝": + return resolveEffectType("weaving"); + case "渗浆": + return resolveEffectType("oozing"); + case "寄生": + return resolveEffectType("infested"); + // 中性 + case "不祥之兆": + case "不祥征兆": + return resolveEffectType("bad_omen"); + case "袭击之兆": + case "袭击征兆": + return resolveEffectType("raid_omen"); + case "试炼之兆": + case "试炼征兆": + return resolveEffectType("trial_omen"); + // 正面 + case "迅捷": + return resolveEffectType("speed"); + case "急迫": + return resolveEffectType("haste"); + case "力量": + return resolveEffectType("strength"); + case "瞬间治疗": + return resolveEffectType("instant_health"); + case "跳跃提升": + return resolveEffectType("jump_boost"); + case "生命恢复": + return resolveEffectType("regeneration"); + case "抗性提升": + return resolveEffectType("resistance"); + case "抗火": + return resolveEffectType("fire_resistance"); + case "水下呼吸": + return resolveEffectType("water_breathing"); + case "隐身": + return resolveEffectType("invisibility"); + case "夜视": + return resolveEffectType("night_vision"); + case "发光": + return resolveEffectType("glowing"); + case "生命提升": + return resolveEffectType("health_boost"); + case "伤害吸收": + return resolveEffectType("absorption"); + case "饱和": + return resolveEffectType("saturation"); + case "幸运": + return resolveEffectType("luck"); + case "村庄英雄": + return resolveEffectType("hero_of_the_village"); + case "缓降": + return resolveEffectType("slow_falling"); + case "潮涌能量": + return resolveEffectType("conduit_power"); + case "海豚的恩惠": + return resolveEffectType("dolphins_grace"); + case "致命中毒": + return resolveEffectType("poison"); + default: + break; + } + + +// 兜底:直接用原版英文ID查(★ 注意是 PotionEffectType.getByName,不是 resolveEffectType) + PotionEffectType fallback = PotionEffectType.getByName(clean); + if (fallback != null) return fallback; + fallback = PotionEffectType.getByName(clean.toLowerCase()); + if (fallback != null) return fallback; + + plugin.getLogger().warning("[防护] 无法识别效果: " + name); + 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); + } + + // 创建15秒延时清理 + if (marked != null && !marked.isEmpty()) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★创建延时任务: " + + toClear.size() + "个效果");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-传送-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); +/* + plugin.getLogger().info( + "[防护-传送-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size() + + " 区域=" + clearArea);*/ + + 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); + /* plugin.getLogger().info( + "[防护-传送-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] ★★★完成: 清除" + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + pendingClearTask.put(uid, task); + + } + + // 离开提示 + if (fromArea != null && fromArea.leaveMsg != null + && !fromArea.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(fromArea.leaveMsg)); + } + } + + // ===== 进入新区域 ===== + 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); + } + } + + // 进入提示 + if (toArea != null && toArea.enterMsg != null + && !toArea.enterMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(toArea.enterMsg)); + } + + // 发放效果 + if (toArea != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] 调用applyRegionEffects");*/ + applyRegionEffects(p, uid, toName, toArea); +// 清除效果 + clearBadEffects(p, toArea); + + if (toArea.confiscateItems != null + && !toArea.confiscateItems.isEmpty() + && !isExemptFromConfiscation( + p, toName)) { + confiscateItems(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 (isPlayerWhitelisted(p.getName(), ac)) 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(); + } + + 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) { + for (String[] parts : ac.giveEffects) { + 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) {} + + 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 && ac.denyBlockPlace + && !isPlayerWhitelisted(p.getName(), ac)) { + 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 && ac.denyBlockBreak + && !isPlayerWhitelisted(p.getName(), ac)) { + 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; + // 保护性规则:对所有人无条件生效 + if (ac.denyAllDamage) { e.setCancelled(true); return; } + if (ac.denyFallDamage + && e.getCause() == EntityDamageEvent.DamageCause.FALL) + e.setCancelled(true); + } + + + @EventHandler + public void onPVP(EntityDamageByEntityEvent e) { + if (!(e.getDamager() instanceof Player)) return; + 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 && ac.denyPVP + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + + @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 && 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 && ac.denyDrop + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); + } + } + + // 替换整个 onInteract 方法 + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand == null) return; + + // ===== 选区工具 ===== + if (hand.getType() == Material.BREEZE_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); + 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 { + pos2.put(uid, block.getLocation()); + p.sendMessage("§a§l[防护] §f位置2: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + } + } + return; + } + } + + // ===== 放置实体类物品时不检查(展示框、矿车、船、地图等)===== + Material mat = hand.getType(); + if (mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME + || 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.MAP + || mat == Material.ARMOR_STAND) { + return; + } + + // ===== 区域规则检查 ===== + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + + // 末影珍珠禁止 + if (ac.denyEnderPearl + && hand.getType() == Material.ENDER_PEARL) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); + return; + } + + // 弓箭禁止 + if (ac.denyBow + && (hand.getType() == Material.BOW + || hand.getType() == Material.CROSSBOW)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); + return; + } + + // 白名单跳过 + if (isPlayerWhitelisted(p.getName(), ac)) return; + + // 禁止使用物品 + String typeId = hand.getType().name(); + if (ac.denyUseItems.contains(typeId)) { + 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(); + } + } + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + + 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("#")) { + 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("#")) { + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + 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 && ac.denyPotion + && !isPlayerWhitelisted(shooter.getName(), ac)) { + e.setCancelled(true); + shooter.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; + } + + /** + * 判断字符串是否是区域名(精准或模糊) + */ + 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", "创建", "删除", "重载", + "工具", "expand", "contraction", + "on", "off", "tempon", "modeexempt" + )); + + 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) { + showHelp(sender); + return true; + } + String sub = args[0].toLowerCase(); +// ===== 诊断命令 ===== + 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; + ItemStack tool = new ItemStack(WAND); + ItemMeta meta = tool.getItemMeta(); + meta.setDisplayName("§a§l区域选择工具"); + meta.setLore(Arrays.asList( + "§7左键: 位置1", "§7右键: 位置2")); + tool.setItemMeta(meta); + p.getInventory().addItem(tool); + p.sendMessage("§a§l[防护] §f已获取选择工具"); + return true; + } + + // ===== 创建 ===== + if (sub.equals("创建") || sub.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 创建 <名>"); + return true; + } + Player p = (Player) sender; + UUID u = p.getUniqueId(); + if (!pos1.containsKey(u) + || !pos2.containsKey(u)) { + p.sendMessage("§c请先用工具选点"); + return true; + } + String areaName = args[1]; + File f = new File(rootDir, areaName + ".txt"); + if (f.exists()) { + p.sendMessage("§c区域已存在"); + return true; + } + Location l1 = pos1.get(u); + Location l2 = pos2.get(u); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 区域: " + areaName); + pw.println("起点: " + l1.getWorld().getName() + + "," + l1.getBlockX() + + "," + l1.getBlockZ()); + pw.println("终点: " + l2.getWorld().getName() + + "," + l2.getBlockX() + + "," + l2.getBlockZ()); + pw.println("高度范围: 0-255"); + pw.println(); + pw.println("# 禁止放置方块"); + pw.println("# 禁止破坏方块"); + pw.println("# 禁止PVP"); + pw.println("# 效果: 夜视 1 999"); + pw.println("# 进入提示: 欢迎来到保护区"); + pw.println("# 离开提示: 已离开保护区"); + pw.close(); + p.sendMessage("§a§l[防护] §f区域 " + + areaName + " 已创建"); + loadAllAreas(); + } catch (IOException ex) { + p.sendMessage("§c创建失败"); + } + 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; + } + ac.peaceWhitelist.add(parsed[1]); + saveAreaConfig(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; + } + ac.peaceWhitelist.remove(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.add(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.remove(parsed[1]); + saveAreaConfig(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; + } + + // ===== 重载 ===== + if (sub.equals("重载") || sub.equals("reload")) { + reload(); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); + return true; + } + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { + if (args.length == 2) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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; + } + + // ===== 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; + } + 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); + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格"); + 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; + } + 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); + 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; + } + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格"); + return true; + } + + // ===== 删除 ===== + if (sub.equals("删除") || sub.equals("delete")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 删除 <名>"); + return true; + } + File f = new File(rootDir, args[1] + ".txt"); + if (f.exists()) { + f.delete(); + areas.remove(args[1]); + sender.sendMessage("§a已删除: " + args[1]); + } else { + sender.sendMessage("§c不存在: " + args[1]); + } + return true; + } + + // ===== add 加白名单 ===== + if (sub.equals("add")) { + if (args.length == 2) { + String name = args[1]; + 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]; + 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) { + String name = args[1]; + if (!globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); + return true; + } + globalPlayerWhitelist.remove(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]; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.remove(playerName); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); + } else { + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + wl.remove(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; + } + + showHelp(sender); + return true; + } + + + private String[] parseAreaAndTarget(String arg1, String arg2) { + String r1 = resolveAreaName(arg1); + if (r1 != null) { + return new String[]{r1, arg2}; + } + String r2 = resolveAreaName(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) {} + } + + private void showHelp(CommandSender s) { + s.sendMessage("§e§l==== 区域防护 ===="); + s.sendMessage("§a/protect 工具 §7选地工具"); + s.sendMessage("§a/protect 创建 <名> §7创建区域"); + s.sendMessage("§a/protect 列表 §7区域列表"); + s.sendMessage("§a/protect 重载 §7重载配置"); + s.sendMessage("§a/protect 删除 <名> §7删除区域"); + s.sendMessage("§a/protect add [区域] <玩家> §7玩家加白"); + s.sendMessage("§a/protect remove [区域] <玩家> §7玩家删白"); + s.sendMessage("§a/protect additem [区域] <物品> §7物品加黑"); + s.sendMessage("§a/protect removeitem [区域] <物品> §7物品删黑"); + s.sendMessage("§a/protect on §7显示边框"); + s.sendMessage("§a/protect off §7关闭边框"); + s.sendMessage("§a/protect tempon §7临时边框(15秒)"); + s.sendMessage("§a/protect list §7列出全局白名单"); + s.sendMessage("§a/protect list <区域> §7列出区域白名单"); + s.sendMessage("§a/protect listitem §7列出全局物品黑名单"); + s.sendMessage("§a/protect listitem <区域> §7列出区域物品黑名单"); + + } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return globalPlayerWhitelist.contains(playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + return wl != null && wl.contains(playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = wl != null && wl.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + + + public int handleConfiscate(Player p, AreaConfig ac) { + if (isPlayerWhitelisted(p.getName(), ac)) 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 = ""; + + 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(); + all[i] = null; + } + } + p.getInventory().setContents(all); + + ItemStack off = p.getInventory().getItemInOffHand(); + if (off != null && off.getType() == mat) { + count += off.getAmount(); + p.getInventory().setItemInOffHand(null); + } + + if (count > 0) { + totalRemoved += count; + if (!confiscatedNames.isEmpty()) + confiscatedNames += "§7、§e"; + confiscatedNames += "§e" + matname(mat) + + " x" + count; + } + } + + if (totalRemoved > 0) { + String msg = (ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty()) + ? ac.confiscateMsg + : "你携带了违禁品,已被没收"; + 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("[防护] 已清空所有黑名单"); + } + + @EventHandler + public void onEntityDamage( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑 ===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + // 展示框被点击(旋转/放入/取出) + @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(); + + // 全局白放行 + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(clicked); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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 + public void onEntityDamageByEntity( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + // 展示框被攻击 + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑(原有的,不动)===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + 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(); + + AreaConfig ac = getArea(world, bx, by, bz); + if (ac != null && ac.denyItemFrame) 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 && ac.denyItemFrame) return ac; + } + return null; + } + + // ===== 投射物击中展示框(弓箭/弩)===== + @EventHandler + public void onProjectileHit( + org.bukkit.event.entity.ProjectileHitEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + Entity hit = e.getHitEntity(); + if (hit == null) return; + String typeName = hit.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(shooter.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(hit); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(shooter.getName().toLowerCase())) { + 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 onIgnite(BlockIgniteEvent e) { + Player p = e.getPlayer(); + if (p == null) return; // 非玩家点燃的(闪电等) + Block block = e.getBlock(); + AreaConfig ac = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 区域内方块着火(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); + } + + + 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 AreaConfig { + public String name = ""; + public String world = ""; + public int x1, z1, x2, z2; + public int yMin = 0, yMax = 255; + public List confiscateItems = new ArrayList<>(); + public List denyUseItems = new ArrayList<>(); + public List clearEffects = new ArrayList<>(); // 清除单个效果名 + public boolean clearAllBadEffects = false; // 清除所有负面+中性 + public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + public List punishCommands = new ArrayList<>(); + public boolean denyBlockPlace = false; + public boolean denyBlockBreak = false; + public boolean denyPVP = false; + public boolean denyFallDamage = false; + public boolean denyHunger = false; + public boolean denyAllDamage = false; + public boolean denyMount = false; + public boolean denyDrop = false; + public boolean denyEnderPearl = false; + public boolean denyBow = false; + public boolean denyPotion = false; + public boolean denyExplosion = false; + public boolean enableAnnounce = false; + public String announceTemplate = ""; + 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 boolean denyItemFrame = false; + public Set peaceWhitelist = new HashSet<>(); + public String enforceGameMode = null; // 强制游戏模式 + public Set modeExempt = new HashSet<>(); // 模式排除名单 + + + + + 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 (denyDrop) 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 (!peaceWhitelist.isEmpty()) c++; + + return c; + } + } + } \ No newline at end of file diff --git a/backup/src/main/java/Sdf1_login/BondManager.java b/backup/src/main/java/Sdf1_login/BondManager.java new file mode 100644 index 0000000..ea78152 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/BondManager.java @@ -0,0 +1,697 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import java.io.File; +import java.sql.*; +import java.util.*; + +public class BondManager { + + private final Main plugin; + private Connection db; + + // ===== 交易类型常量 ===== + public static final String TX_ADMIN_GIVE = "admin_give"; + public static final String TX_ADMIN_DEDUCT = "admin_deduct"; + public static final String TX_TRANSFER_OUT = "transfer_out"; + public static final String TX_TRANSFER_IN = "transfer_in"; + public static final String TX_REDEEM = "redeem"; + public static final String TX_SHOP_BUY = "shop_buy"; + public static final String TX_DAILY_SIGN = "daily_sign"; + public static final String TX_FREEZE = "freeze"; + public static final String TX_UNFREEZE = "unfreeze"; + + + public static final int MAX_WATER_DAYS = 14; + public static final int ADMIN_MAX_WATER_DAYS = 30; + + public BondManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + // ======================== 数据库初始化 ======================== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 债券余额(含冻结状态) + st.execute("CREATE TABLE IF NOT EXISTS bonds (" + + "player_name TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "status TEXT DEFAULT 'normal')"); + + // 兼容旧表 + try { + st.execute("ALTER TABLE bonds " + + "ADD COLUMN status " + + "TEXT DEFAULT 'normal'"); + } catch (SQLException ignored) {} + + // 旧流水(保留兼容) + st.execute("CREATE TABLE IF NOT EXISTS bond_log (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "reason TEXT DEFAULT ''," + + "time INTEGER DEFAULT 0)"); + + // 新交易流水表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "bond_transaction (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "type TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "target_player TEXT DEFAULT ''," + + "operator TEXT DEFAULT ''," + + "reason TEXT DEFAULT ''," + + "balance_after INTEGER DEFAULT 0," + + "time INTEGER DEFAULT 0)"); + + try { + st.execute("CREATE INDEX " + + "IF NOT EXISTS idx_tx_pt " + + "ON bond_transaction(" + + "player_name, time)"); + } catch (SQLException ignored) {} + try { + st.execute("ALTER TABLE bond_transaction " + + "ADD COLUMN balance_before " + + "INTEGER DEFAULT 0"); + } catch (SQLException ignored) {} + + + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[Bond] DB init failed: " + + e.getMessage(), e); + } + } + + // ======================== 账户状态 ======================== + + public boolean isFrozen(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return "frozen".equals(s); + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return false; + } + + public String getAccountStatus(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return s != null ? s : "normal"; + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return "normal"; + } + + public void freezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='frozen' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + public void unfreezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='normal' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + private void ensureAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR IGNORE INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,0,'normal')"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + // ======================== 余额查询 ======================== + + public int getBonds(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + int v = 0; + if (rs.next()) v = rs.getInt("amount"); + rs.close(); ps.close(); + return v; + } catch (SQLException e) { return 0; } + } + + /** 该玩家是否在 bond 表中有真实记录 */ + public boolean hasBondAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM bonds " + + "WHERE player_name=? " + + "AND amount > 0"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next(); + rs.close(); ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ======================== 增加债券 ======================== + + public int addBonds(String player, int amount) { + return addBondsInternal(player, amount, + TX_ADMIN_GIVE, "", "", + "管理员发放"); + } + + public int addBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return -1; + return addBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + private int addBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + int after = before + amount; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status)" + + " VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, amount); + return after; + } + + // ======================== 扣除债券 ======================== + + + + private boolean deductBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + if (before < amount) return false; + int after = before - amount; + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET amount=? " + + "WHERE player_name=?"); + ps.setInt(1, after); + ps.setString(2, player); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, -amount); + return true; + } + + // ======================== 流水写入 ======================== + + private void logTransaction(String player, + String type, + int amount, + String targetPlayer, + String operator, + String reason, + int balanceBefore, + int balanceAfter) { + try { + // 合并模式:shop_buy 30秒内同商品合并 + if ("shop_buy".equals(type) + && reason != null + && !reason.isEmpty()) { + long mergeWindow = + System.currentTimeMillis() - 30000L; + PreparedStatement chk = + db.prepareStatement( + "SELECT id, amount " + + "FROM bond_transaction " + + "WHERE player_name=? " + + "AND type=? " + + "AND reason=? " + + "AND time > ? " + + "ORDER BY time DESC " + + "LIMIT 1"); + chk.setString(1, player); + chk.setString(2, type); + chk.setString(3, reason); + chk.setLong(4, mergeWindow); + ResultSet rs = chk.executeQuery(); + if (rs.next()) { + int existId = rs.getInt("id"); + int existAmt = rs.getInt("amount"); + rs.close(); + chk.close(); + PreparedStatement upd = + db.prepareStatement( + "UPDATE bond_transaction " + + "SET amount=?, " + + "balance_after=?, " + + "time=? " + + "WHERE id=?"); + upd.setInt(1, existAmt + amount); + upd.setInt(2, balanceAfter); + upd.setLong(3, + System.currentTimeMillis()); + upd.setInt(4, existId); + upd.executeUpdate(); + upd.close(); + return; + } + rs.close(); + chk.close(); + } + + // 原始插入 + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_transaction " + + "(player_name, type, amount," + + " target_player, operator," + + " reason, balance_before," + + " balance_after, time)" + + " VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, type); + ps.setInt(3, amount); + ps.setString(4, + targetPlayer != null ? targetPlayer : ""); + ps.setString(5, + operator != null ? operator : ""); + ps.setString(6, + reason != null ? reason : ""); + ps.setInt(7, balanceBefore); + ps.setInt(8, balanceAfter); + ps.setLong(9, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + // ======================== 扣除债券 ======================== + + /** 完整版:含冻结检查 + 流水记录 */ + public boolean deductBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return false; + return deductBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + public boolean checkoutCart(String player, + String[] itemNames, + int[] subtotals) { + int total = 0; + for (int s : subtotals) total += s; + if (isFrozen(player)) return false; + + int before = getBonds(player); + if (before < total) return false; + int after = before - total; + + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?,'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + + int running = before; + for (int i = 0; i < itemNames.length; i++) { + int next = running - subtotals[i]; + logTransaction(player, "shop_buy", subtotals[i], + "", "商店系统", + "购物车购买-" + itemNames[i], + running, next); + running = next; + } + + logLegacy(player, -total); + return true; + } + + public boolean setBonds(String player, int amount) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setInt(3, amount); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return true; + } + + // ======================== 转账 ======================== + + public TransferResult transfer(String from, + String to, + int amount) { + if (from.equalsIgnoreCase(to)) + return new TransferResult(false, + "不能给自己转账"); + if (isFrozen(from)) + return new TransferResult(false, + "你的账户已被冻结"); + if (isFrozen(to)) + return new TransferResult(false, + to + " 的账户已被冻结"); + if (isSameIP(from, to)) + return new TransferResult(false, + "不能与同IP玩家转账"); + + if (!deductBondsInternal(from, amount, + TX_TRANSFER_OUT, to, from, + "转账给" + to)) + return new TransferResult(false, "债券不足"); + + addBondsInternal(to, amount, + TX_TRANSFER_IN, from, from, + "收到来自" + from + "的转账"); + + return new TransferResult(true, "转账成功"); + } + + // ======================== 同IP检测 ======================== + + public boolean isSameIP(String p1, String p2) { + String ip1 = getPlayerIPAny(p1); + String ip2 = getPlayerIPAny(p2); + if (ip1 == null || ip2 == null) return false; + return ip1.equals(ip2); + } + + /** 获取同IP下的所有其他在线玩家 */ + public List getSameIPPlayers(String player) { + String ip = getPlayerIPAny(player); + if (ip == null) return Collections.emptyList(); + List list = new ArrayList<>(); + for (Player p : + plugin.getServer().getOnlinePlayers()) { + String pIp = plugin.getPlayerIP(p); + if (pIp != null && pIp.equals(ip) + && !p.getName() + .equalsIgnoreCase(player)) { + list.add(p.getName()); + } + } + return list; + } + + private String getPlayerIPAny(String player) { + // 在线:直接取 + Player p = plugin.getServer() + .getPlayerExact(player); + if (p != null) { + String ip = plugin.getPlayerIP(p); + if (ip != null) return ip; + } + // 离线:查主库 + try { + Object v = plugin.getDb() + .getField(player, "last_login_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + try { + Object v = plugin.getDb() + .getField(player, "register_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + return null; + } + + // ======================== 流水查询 ======================== + + public List> getTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } +// 放在 getTransactions 方法后面: + + /** + * 获取该玩家全部流水(不限时间,供打印使用) + */ + public List> + getAllTransactions(String player) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "ORDER BY time ASC"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("player_name", + rs.getString("player_name")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } +// 放在 getAllTransactions 方法后面: + + /** + * 获取所有有流水记录的玩家名(去重) + */ + public List getAllPlayerNames() { + List list = new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT DISTINCT player_name " + + "FROM bond_transaction " + + "ORDER BY player_name ASC"); + while (rs.next()) { + list.add(rs.getString("player_name")); + } + rs.close(); st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTransferTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "AND (type=? OR type=?) " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ps.setString(3, TX_TRANSFER_OUT); + ps.setString(4, TX_TRANSFER_IN); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } + + + private void logLegacy(String player, int amount) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_log " + + "(player_name,amount,time) " + + "VALUES (?,?,?)"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + public Map getLog( + String player, int limit) { + Map info = new LinkedHashMap<>(); + info.put("balance", getBonds(player)); + return info; + } + + // ======================== 转账结果 ======================== + + public static class TransferResult { + public final boolean success; + public final String message; + public TransferResult(boolean success, + String message) { + this.success = success; + this.message = message; + } + } +} diff --git a/backup/src/main/java/Sdf1_login/BondPrinter.java b/backup/src/main/java/Sdf1_login/BondPrinter.java new file mode 100644 index 0000000..674ec3c --- /dev/null +++ b/backup/src/main/java/Sdf1_login/BondPrinter.java @@ -0,0 +1,378 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +public class BondPrinter { + + private final Main plugin; + private final File outputDir; + + private static final SimpleDateFormat FILE_SDF = + new SimpleDateFormat( + "yyyy-MM-dd_HH-mm-ss"); + private static final SimpleDateFormat CELL_SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private static final LinkedHashMap + TYPE_NAMES = new LinkedHashMap<>(); + + static { + TYPE_NAMES.put("admin_give", "管理员发放"); + TYPE_NAMES.put("admin_deduct", "管理员扣除"); + TYPE_NAMES.put("transfer_out", "转出"); + TYPE_NAMES.put("transfer_in", "转入"); + TYPE_NAMES.put("redeem", "CDK兑换"); + TYPE_NAMES.put("shop_buy", "商城购买"); + TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上 + TYPE_NAMES.put("daily_sign", "签到奖励"); + TYPE_NAMES.put("freeze", "冻结"); + TYPE_NAMES.put("unfreeze", "解冻"); + + } + + public BondPrinter(Main plugin) { + this.plugin = plugin; + this.outputDir = new File( + plugin.getDataFolder(), "printer"); + if (!this.outputDir.exists()) { + this.outputDir.mkdirs(); + } + } + + // ============ 打印单个玩家 ============ + + public void printPlayer(String player, + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintPlayer( + player, sender)); + } + + private void doPrintPlayer(String player, + CommandSender sender) { + try { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + + String fileName = player + "_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + deleteOld(player); + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, + escXml(player)); + writeXmlRow(pw, + new String[]{ + "时间", "类型", + "金额", "对象", + "操作者", "理由", + "操作前余额", + "操作后余额"}); + + for (Map tx : txs) { + writeXmlRow(pw, new String[]{ + txTime(tx), + txTypeName(tx), + String.valueOf( + txInt(tx, + "amount")), + txStr(tx, + "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")) + }); + } + + writeXmlFooter(pw); + pw.flush(); + } finally { + pw.close(); + } + + if (sender != null) { + sender.sendMessage("§a[打印] §f" + + player + " §a流水已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + txs.size() + + " §7条,§724小时后删除"); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 打印全服 ============ + + public void printAll(CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintAll(sender)); + } + + private void doPrintAll(CommandSender sender) { + try { + List players = + plugin.getBonds() + .getAllPlayerNames(); + if (players.isEmpty()) { + if (sender != null) { + sender.sendMessage("§c没有流水数据"); + } + return; + } + + String fileName = "全服流水_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + // ===== 收集所有流水 ===== + // key = 时间戳+玩家+金额,用于去重 + // value = 一行数据 + List allRows = new ArrayList<>(); + Set seenKeys = new HashSet<>(); + + for (String player : players) { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + for (Map tx : txs) { + int txId = ((Number) tx.get("id")) + .intValue(); + String dedupKey = + String.valueOf(txId); + if (!seenKeys.add(dedupKey)) { + continue; + } + + String timeStr = txTime(tx); + long timeMs = 0; + Object t = tx.get("time"); + if (t != null) { + timeMs = + ((Number) t).longValue(); + } + + allRows.add(new String[]{ + player, + timeStr, + txTypeName(tx), + String.valueOf( + txInt(tx, "amount")), + txStr(tx, "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")), + // 额外存时间戳用于排序 + String.valueOf(timeMs) + }); + } + } + + // ===== 按时间戳排序 ===== + allRows.sort((a, b) -> { + long ta = Long.parseLong( + a[a.length - 1]); + long tb = Long.parseLong( + b[b.length - 1]); + return Long.compare(ta, tb); + }); + + // ===== 写入文件 ===== + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, "全服流水"); + writeXmlRow(pw, new String[]{ + "玩家", "时间", "类型", + "金额", "对象", "操作者", + "理由", "操作前余额", + "操作后余额" + }); + + for (String[] row : allRows) { + // 去掉最后的时间戳列 + String[] cells = + new String[row.length - 1]; + System.arraycopy(row, 0, + cells, 0, cells.length); + writeXmlRow(pw, cells); + } + + writeXmlFooter(pw); + pw.flush(); + + if (sender != null) { + sender.sendMessage( + "§a[打印] §f全服流水 §a已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + players.size() + + " 名玩家,§f" + + allRows.size() + " §7条"); + sender.sendMessage( + "§7按时间从早到晚排序"); + sender.sendMessage( + "§724小时后自动删除"); + } + } finally { + pw.close(); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 清理 ============ + + public void cleanupOldFiles() { + long now = System.currentTimeMillis(); + long maxAge = 24L * 60 * 60 * 1000; + File[] files = outputDir.listFiles( + (d, n) -> n.endsWith(".xls")); + if (files == null) { + return; + } + int count = 0; + for (File f : files) { + if (now - f.lastModified() > maxAge) { + if (f.delete()) { + count++; + } + } + } + if (count > 0) { + plugin.getLogger().info( + "[Printer] 清理 " + + count + " 个过期文件"); + } + } + + private void deleteOld(String player) { + File[] old = outputDir.listFiles( + (d, n) -> n.startsWith(player + "_") + && n.endsWith(".xls")); + if (old != null) { + for (File f : old) { + f.delete(); + } + } + } + + // ============ XML工具 ============ + + private void writeXmlHeader(PrintWriter pw, + String sheetName) { + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(" "); + pw.println(" "); + } + + private void writeXmlRow(PrintWriter pw, + String[] cells) { + pw.println(" "); + for (String cell : cells) { + pw.println(" " + + "" + + escXml(cell != null ? cell : "") + + ""); + } + pw.println(" "); + } + + private void writeXmlFooter(PrintWriter pw) { + pw.println("
"); + pw.println("
"); + pw.println("
"); + } + + private String escXml(String val) { + if (val == null) { + return ""; + } + return val.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private String txTime(Map tx) { + Object t = tx.get("time"); + if (t == null) return ""; + long ms = ((Number) t).longValue(); + return CELL_SDF.format(new Date(ms)); + } + + private String txTypeName( + Map tx) { + String type = (String) tx.get("type"); + return TYPE_NAMES.getOrDefault(type, type); + } + + private int txInt(Map tx, + String key) { + Object v = tx.get(key); + if (v == null) return 0; + return ((Number) v).intValue(); + } + + private String txStr(Map tx, + String key) { + Object v = tx.get(key); + return v != null ? v.toString() : ""; + } +} diff --git a/backup/src/main/java/Sdf1_login/CDKManager.java b/backup/src/main/java/Sdf1_login/CDKManager.java new file mode 100644 index 0000000..e07569f --- /dev/null +++ b/backup/src/main/java/Sdf1_login/CDKManager.java @@ -0,0 +1,293 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.nio.charset.StandardCharsets; + +public class CDKManager { + + private final Main plugin; + private Connection db; + private final Map pendingInputs + = new ConcurrentHashMap<>(); + + public CDKManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS cdk (" + + "code TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "type TEXT DEFAULT 'bond'," + + "used INTEGER DEFAULT 0," + + "used_by TEXT DEFAULT ''," + + "created_time INTEGER DEFAULT 0," + + "used_time INTEGER DEFAULT 0)"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[CDK] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== CDK兑换(供SDF1调用) ===== + + /** + * 兑换CDK,返回结果字符串 + * @return "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeem(String code, String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount,type,used FROM cdk " + + "WHERE code=?"); + ps.setString(1, code); + ResultSet rs = ps.executeQuery(); + if (!rs.next()) { + rs.close(); ps.close(); + return "fail:not_found"; + } + int amount = rs.getInt("amount"); + String type = rs.getString("type"); + int used = rs.getInt("used"); + rs.close(); ps.close(); + + if (used == 1) + return "fail:already_used"; + + if (plugin.getBonds() + .isFrozen(player)) + return "fail:frozen"; + + PreparedStatement up = db.prepareStatement( + "UPDATE cdk SET used=1," + + "used_by=?,used_time=? " + + "WHERE code=?"); + up.setString(1, player); + up.setLong(2, + System.currentTimeMillis()); + up.setString(3, code); + up.executeUpdate(); up.close(); + + if ("bond".equals(type)) { + int bef = plugin.getBonds() + .getBonds(player); + plugin.getBonds().addBonds(player, + amount, + BondManager.TX_REDEEM, + "", "CDK系统", + "CDK兑换: " + code); + int aft = plugin.getBonds() + .getBonds(player); + return "success:" + amount + + ":" + bef + ":" + aft; + } + return "fail:unknown_type"; + } catch (SQLException e) { + return "fail:" + e.getMessage(); + } + } + + // ===== 聊天输入系统 ===== + + public void requestInput(Player p, + String type, + String context) { + CDKInput input = new CDKInput(); + input.type = type; + input.context = context; + input.time = System.currentTimeMillis(); + pendingInputs.put(p.getName(), input); + } + + public boolean isListening(String playerName) { + return pendingInputs + .containsKey(playerName); + } + + public void onChat(Player p, String msg) { + CDKInput input = pendingInputs + .remove(p.getName()); + if (input == null) return; + + if (System.currentTimeMillis() + - input.time > 60000) { + p.sendMessage("§c输入超时"); + return; + } + + if ("cdk".equals(input.type)) { + String result = redeem( + msg.trim(), p.getName()); + if (result != null + && result.startsWith("success:")) { + String[] parts = result.split(":"); + int amt = Integer.parseInt(parts[1]); + int bef = Integer.parseInt(parts[2]); + int aft = Integer.parseInt(parts[3]); + p.sendMessage("§e§l[CDK] §f兑换成功!" + + "获得 §c" + amt + "§f债券"); + p.sendMessage("§7余额: §e" + bef + + " §7-> §a" + aft); + } else { + p.sendMessage("§c§l[CDK] §f" + + (result != null + ? result.replace("fail:", "") + : "无效的兑换码")); + } + return; + } + + if ("econ".equals(input.type)) { + handleEconInput(p, msg.trim(), + input.context); + return; + } + + if ("bond".equals(input.type)) { + handleBondInput(p, msg.trim(), + input.context); + } + } + + private void handleEconInput(Player admin, + String msg, + String target) { + net.milkbowl.vault.economy.Economy econ + = null; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy + .Economy.class); + if (reg != null) + econ = reg.getProvider(); + } catch (Exception ignored) {} + if (econ == null) { + admin.sendMessage("§c经济系统不可用"); + return; + } + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String numStr = + msg.substring(1).trim(); + double amount = + Double.parseDouble(numStr); + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + if (msg.startsWith("-")) { + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp == null) { + admin.sendMessage("§c" + target + + " 不在线"); + return; + } + if (!econ.has(tp, amount)) { + admin.sendMessage("§c余额不足"); + return; + } + econ.withdrawPlayer(tp, amount); + admin.sendMessage("§a扣款成功"); + } else { + econ.depositPlayer(target, amount); + admin.sendMessage("§a到账成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + private void handleBondInput(Player admin, + String msg, + String target) { + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String sign = msg.substring(0, 1); + String rest = + msg.substring(1).trim(); + String[] parts = + rest.split("\\s+", 2); + int amount = Integer.parseInt(parts[0]); + String reason = parts.length >= 2 + ? parts[1].trim() + : "管理员手动调整"; + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + String opName = admin.getName(); + String txType = "-".equals(sign) + ? BondManager.TX_ADMIN_DEDUCT + : BondManager.TX_ADMIN_GIVE; + + if ("-".equals(sign)) { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + if (plugin.getBonds().deductBonds( + target, amount, txType, + "", opName, reason)) { + admin.sendMessage("§a扣除成功"); + } else { + admin.sendMessage("§c债券不足"); + } + } else { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + plugin.getBonds().addBonds(target, + amount, txType, + "", opName, reason); + admin.sendMessage("§a发放成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + public void cancelInput(String player) { + pendingInputs.remove(player); + } + + static class CDKInput { + String type; + String context; + long time; + } +} diff --git a/backup/src/main/java/Sdf1_login/ChatFilterManager.java b/backup/src/main/java/Sdf1_login/ChatFilterManager.java new file mode 100644 index 0000000..59cb5de --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ChatFilterManager.java @@ -0,0 +1,612 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatFilterManager { + + private final JavaPlugin plugin; + private final List whitelistUrls = + new ArrayList<>(); + private final Set whitelistPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map + punishmentRules = + new LinkedHashMap<>(); + private final Map + punishmentDurations = + new LinkedHashMap<>(); + private final Map + violationCount = + new ConcurrentHashMap<>(); + private final Map + mutedPlayers = + new ConcurrentHashMap<>(); + private final Map messages = + new LinkedHashMap<>(); + private boolean notifyAdmin = true; + private boolean notifyAll = false; + private int muteDuration = 300; + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean v) { + enabled = v; + } + + + private static final Pattern URL_PATTERN = + Pattern.compile( + "(?i)(?:https?://\\S+)" + + "|(?:(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9\\-]*" + + "[a-zA-Z0-9])?\\.)+" + + "[a-zA-Z]{2,})" + + "(?:/\\S*)?"); + private static final Pattern DOT_VARIANTS = + Pattern.compile( + "[\uff0e\u3002\u2025\u2026\u00b7]"); + private static final Pattern COLOR_CODES = + Pattern.compile( + "\u00a7[0-9a-fk-orA-FK-OR]"); + + private static final Set FILE_EXT; + static { + Set s = new HashSet<>( + Arrays.asList( + "txt", "yml", "yaml", + "json", "xml", "java", + "class", "jar", "png", + "jpg", "jpeg", "gif", + "bmp", "svg", "ico", + "webp", "mp3", "mp4", + "avi", "mkv", "wav", + "flac", "ogg", "zip", + "rar", "7z", "tar", + "gz", "cfg", "conf", + "ini", "log", "db", + "sql", "sh", "bat", + "html", "htm", "css", + "js", "md", "pdf", + "dll", "so", "exe", + "bin", "c", "cpp", + "py", "rb", "go", + "rs", "kt", "lua", + "php", "asp", "jsp")); + FILE_EXT = Collections.unmodifiableSet(s); + } + + public ChatFilterManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + // ========== 配置加载 ========== + + public void loadConfig() { + whitelistUrls.clear(); + whitelistPlayers.clear(); + punishmentRules.clear(); + punishmentDurations.clear(); + messages.clear(); + + File f = new File( + plugin.getDataFolder(), "chat.txt"); + if (!f.exists()) writeDefaultFile(f); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("白名单") + || t.equals("白名单:")) { + section = "whitelist"; + continue; + } + if (t.equals("处罚规则") + || t.equals("处罚规则:")) { + section = "punishment"; + continue; + } + if (t.equals("白名单玩家") + || t.equals("白名单玩家:")) { + section = "players"; + continue; + } + if (t.contains(":")) { + String[] kv = t.split(":", 2); + String k = kv[0].trim(); + String v = kv[1].trim(); + if (equals("启用过滤") + || equals("enabled")) { + enabled = parseBool(v); + continue; + } + + if (k.equals("通知管理员")) { + notifyAdmin = parseBool(v); + continue; + } + if (k.equals("全服通报")) { + notifyAll = parseBool(v); + continue; + } + if (k.equals("禁言时长")) { + try { + muteDuration = + Integer.parseInt(v); + } catch (Exception ignored) { + } + continue; + } + } + switch (section) { + case "whitelist": + whitelistUrls.add( + t.toLowerCase()); + break; + case "punishment": + String[] parts = t.split(":"); + if (parts.length >= 2) { + try { + int cnt = + Integer.parseInt( + parts[0] + .trim()); + String type = + parts[1].trim() + .toLowerCase(); + int dur = + parts.length >= 3 + ? Integer.parseInt( + parts[2] + .trim()) + : muteDuration; + punishmentRules.put( + cnt, type); + punishmentDurations.put( + cnt, dur); + } catch (Exception ignored) { + } + } + break; + case "players": + whitelistPlayers.add(t); + break; + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_chat] 配置加载失败: " + + e.getMessage()); + } + + // 从消息.txt读取chat_开头的消息 + loadMessages(); + } + + private void loadMessages() { + File msgFile = new File( + plugin.getDataFolder(), + "消息.txt"); + if (!msgFile.exists()) return; + try (BufferedReader mr = + new BufferedReader( + new InputStreamReader( + new FileInputStream( + msgFile), + StandardCharsets.UTF_8))) { + String mline; + while ((mline = mr.readLine()) != null) { + mline = mline.trim(); + if (mline.isEmpty() + || mline.startsWith("#")) + continue; + // 支持 = 和 : 两种分隔符 + int eqIdx = mline.indexOf('='); + int coIdx = mline.indexOf(':'); + int splitIdx = -1; + if (eqIdx >= 0 && coIdx >= 0) { + splitIdx = Math.min(eqIdx, coIdx); + } else if (eqIdx >= 0) { + splitIdx = eqIdx; + } else if (coIdx >= 0) { + splitIdx = coIdx; + } + if (splitIdx < 0) continue; + String k = mline.substring(0, + splitIdx).trim(); + String v = mline.substring( + splitIdx + 1).trim(); + if (k.startsWith("chat_")) { + messages.put(k, v); + } + } + } catch (Exception ignored) { + } + } + + + private void writeDefaultFile(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("启用过滤: " + enabled); + pw.println("通知管理员: true"); + pw.println("全服通报: false"); + pw.println("禁言时长: 300"); + pw.println(); + pw.println("白名单:"); + pw.println("*.minecraft.net"); + pw.println("baidu.com"); + pw.println("github.com"); + pw.println(); + pw.println("处罚规则:"); + pw.println("1:warn:0"); + pw.println("2:warn:0"); + pw.println("3:mute:300"); + pw.println("5:mute:600"); + pw.println("8:kick:0"); + pw.println("10:ban:0"); + pw.println(); + pw.println("白名单玩家:"); + } catch (IOException ignored) { + } + } + + public void saveConfig() { + File f = new File( + plugin.getDataFolder(), "chat.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: " + notifyAdmin); + pw.println("全服通报: " + notifyAll); + pw.println("禁言时长: " + muteDuration); + pw.println(); + pw.println("白名单:"); + for (String u : whitelistUrls) + pw.println(u); + pw.println(); + pw.println("处罚规则:"); + for (Map.Entry entry + : punishmentRules.entrySet()) { + int dur = punishmentDurations + .getOrDefault(entry.getKey(), + muteDuration); + pw.println(entry.getKey() + ":" + + entry.getValue() + + ":" + dur); + } + pw.println(); + pw.println("白名单玩家:"); + for (String p : whitelistPlayers) + pw.println(p); + } catch (IOException e) { + plugin.getLogger().severe( + "[Sdf1_chat] 保存失败: " + + e.getMessage()); + } + } + + // ========== 消息 ========== + + public String msg(String key, + String... args) { + String template = messages.getOrDefault( + key, key); + for (int i = 0; i < args.length - 1; + i += 2) { + template = template.replace( + "{" + args[i] + "}", + args[i + 1]); + } + return template; + } + + // ========== URL检测 ========== + + private String normalizeMessage(String msg) { + msg = COLOR_CODES.matcher(msg) + .replaceAll(""); + msg = DOT_VARIANTS.matcher(msg) + .replaceAll("."); + return msg; + } + + public List extractUrls( + String message) { + String normalized = + normalizeMessage(message); + List urls = new ArrayList<>(); + Matcher m = URL_PATTERN + .matcher(normalized); + while (m.find()) { + String url = m.group().toLowerCase(); + url = url.replaceAll( + "[\\s.,;!?,。;!?、)》」』】" + + "\\]\\[\\((]+$", + ""); + if (!url.isEmpty()) urls.add(url); + } + return urls; + } + + public boolean isWhitelisted(String url) { + String clean = url + .replaceAll("^https?://", "") + .toLowerCase(); + int colon = clean.indexOf(':'); + int slash = clean.indexOf('/'); + if (colon > 0 && (slash < 0 + || colon < slash)) { + clean = clean.substring(0, colon); + } + slash = clean.indexOf('/'); + if (slash > 0) { + clean = clean.substring(0, slash); + } + for (String entry : whitelistUrls) { + String e = entry.toLowerCase().trim(); + if (e.startsWith("*.")) { + String suffix = e.substring(1); + if (clean.endsWith(suffix) + || clean.equals( + suffix.substring(1))) + return true; + } else { + if (clean.equals(e) + || clean.endsWith( + "." + e)) + return true; + } + } + return false; + } + + public boolean isLikelyDomain(String url) { + int lastDot = url.lastIndexOf('.'); + if (lastDot < 0 + || lastDot >= url.length() - 1) + return false; + String ext = url.substring(lastDot + 1) + .toLowerCase(); + return !FILE_EXT.contains(ext); + } + + // ========== 玩家操作 ========== + + public boolean isMuted(String name) { + Long expiry = mutedPlayers.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() + >= expiry) { + mutedPlayers.remove(name); + return false; + } + return true; + } + + public String fmtDuration(int seconds) { + if (seconds <= 0) return "0秒"; + int day = seconds / 86400; + int hr = (seconds % 86400) / 3600; + int min = (seconds % 3600) / 60; + int sec = seconds % 60; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hr > 0) + sb.append(hr).append("小时"); + if (min > 0) + sb.append(min).append("分"); + if (sec > 0 || sb.length() == 0) + sb.append(sec).append("秒"); + return sb.toString(); + } + + public void applyPunishment(Player player, + int violation) { + String type = "warn"; + int duration = muteDuration; + int maxRule = 0; + for (Map.Entry entry + : punishmentRules.entrySet()) { + if (violation >= entry.getKey() + && entry.getKey() > maxRule) { + maxRule = entry.getKey(); + type = entry.getValue(); + duration = punishmentDurations + .getOrDefault( + entry.getKey(), + muteDuration); + } + } + if (type.equals("mute")) { + if (isMuted(player.getName())) + return; + mutedPlayers.put(player.getName(), + System.currentTimeMillis() + + (long) duration + * 1000L); + } + final String fType = type; + final int fDur = duration; + final int fV = violation; + final String fName = player.getName(); + Bukkit.getScheduler().runTask(plugin, + () -> { + Player p = Bukkit.getPlayer(fName); + if (p == null) return; + switch (fType) { + case "warn": + // 警告不处罚,只提示 + break; + case "mute": + p.sendMessage( + msg("chat_muted")); + break; + case "kick": + p.kickPlayer( + msg("chat_muted")); + break; + case "ban": + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban " + fName + + " §c多次发送违规链接"); + break; + case "banip": + InetSocketAddress addr = + p.getAddress(); + if (addr != null) { + String ip = addr + .getAddress() + .getHostAddress(); + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban-ip " + ip + + " §c发送违规链接"); + } + break; + } + }); + } + + public void unmutePlayer(String name) { + mutedPlayers.remove(name); + } + + public void resetPlayer(String name) { + mutedPlayers.remove(name); + violationCount.remove(name); + } + + public boolean isPlayerWhitelisted( + String name) { + return whitelistPlayers.contains(name); + } + + public boolean isNotifyAdmin() { + return notifyAdmin; + } + + public boolean isNotifyAll() { + return notifyAll; + } + + public int getViolationCount(String name) { + return violationCount + .getOrDefault(name, 0); + } + + public void incrementViolation( + String name) { + violationCount.put(name, + getViolationCount(name) + 1); + } + + public List getWhitelistUrls() { + return whitelistUrls; + } + + public Set getWhitelistPlayers() { + return whitelistPlayers; + } + + public Map + getPunishmentRules() { + return punishmentRules; + } + + public Map + getPunishmentDurations() { + return punishmentDurations; + } + + public Map + getMutedPlayers() { + return mutedPlayers; + } + + public void setNotifyAdmin(boolean v) { + notifyAdmin = v; + } + + public void setNotifyAll(boolean v) { + notifyAll = v; + } + + public void setMuteDuration(int v) { + muteDuration = v; + } + + public void addUrl(String url) { + whitelistUrls.add( + url.toLowerCase().trim()); + saveConfig(); + } + + public boolean removeUrl(String url) { + boolean r = whitelistUrls.remove( + url.toLowerCase().trim()); + saveConfig(); + return r; + } + + public void addWhitelistPlayer( + String name) { + whitelistPlayers.add(name); + saveConfig(); + } + + public boolean removeWhitelistPlayer( + String name) { + boolean r = + whitelistPlayers.remove(name); + saveConfig(); + return r; + } + + public void cleanExpired() { + long now = System.currentTimeMillis(); + mutedPlayers.entrySet().removeIf( + e -> e.getValue() <= now); + } + + private boolean parseBool(String v) { + String s = v.toLowerCase().trim(); + return s.equals("true") + || s.equals("on") + || s.equals("yes") + || s.equals("1") + || s.contains("开启") + || s.contains("启用") + || s.contains("是"); + } +} diff --git a/backup/src/main/java/Sdf1_login/ChatInputManager.java b/backup/src/main/java/Sdf1_login/ChatInputManager.java new file mode 100644 index 0000000..49f53fd --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ChatInputManager.java @@ -0,0 +1,842 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class ChatInputManager { + + public enum InputType { + NONE, + CHANGE_PWD_STEP1, + CHANGE_PWD_STEP2, + SET_EMAIL, + SET_EMAIL_VERIFY, + ADMIN_AUTH, + ADMIN_SET_POINTS, + ADMIN_SET_PWD, + ADMIN_DELETE_CONFIRM, + ADMIN_SET_AFK_TIME, + SMTP_CONFIG, + SMTP_VERIFY, + BACK_CHECKIN, + INVITE_INPUT_CODE, + TICKET_TITLE, + TICKET_DESC, + TICKET_SCORE, + TICKET_REJECT, + TICKET_ASSIGN, + TICKET_INVITE_PROVIDER, + TICKET_REMOVE_PROVIDER, + TICKET_COMPLETE, + TICKET_REPORT_ISSUE, + TICKET_REPLY, + TICKET_ADMIN_TYPE, + TICKET_ADMIN_TITLE, + TICKET_ADMIN_DESC, + TICKET_ADMIN_PROVIDER, + BACK_CHECK_ECONOMY_CONFIRM, + USER_MGMT_SEARCH, + + } + + public static class PlayerState { + public InputType type = InputType.NONE; + public String targetPlayer = ""; + public String ticketType = ""; + public String ticketTitle = ""; + public String ticketDesc = ""; + public int ticketId = 0; + public int smtpStep = 0; + public int scoreValue = 0; + + } + + private final Map states = + new HashMap<>(); + private Main plugin; + + public PlayerState getState(Player p) { + return states.computeIfAbsent( + p.getName(), + k -> new PlayerState()); + } + + public void reset(Player p) { + states.remove(p.getName()); + } + public boolean isInFlow(Player p) { + PlayerState state = states.get(p.getName()); + return state != null + && state.type != InputType.NONE; + } + + public void handleInput(Player p, String msg, + Main mainPlugin) { + if (this.plugin == null) + this.plugin = mainPlugin; + PlayerState state = getState(p); + ConfigManager config = + mainPlugin.getConfig2(); + DatabaseManager db = mainPlugin.getDb(); + + switch (state.type) { + + case CHANGE_PWD_STEP1: { + String name = p.getName(); + String salt = (String) db.getField( + name, "password_salt"); + String hash = + PasswordUtils.hash(msg, salt); + boolean mainOk = + db.checkPassword(name, hash); + if (mainOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "main"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + boolean tempOk = + db.checkPasswordOrTemp( + name, hash); + if (tempOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "temp"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + p.sendMessage(config.msg( + "password_wrong")); + return; + } + + case CHANGE_PWD_STEP2: { + String name = p.getName(); + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String oldHash = + (String) db.getField(name, + "password_hash"); + String oldSalt = + (String) db.getField(name, + "password_salt"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + msg, newSalt); + if (newHash.equals(oldHash)) { + p.sendMessage(config.msg( + "password_same")); + reset(p); + return; + } + mainPlugin.recordPasswordChange( + name, oldHash, oldSalt); + db.setField(name, + "password_hash", newHash); + db.setField(name, + "password_salt", newSalt); + mainPlugin + .getNeedsPasswordChange() + .remove(name); + p.sendMessage(config.msg( + "password_changed")); + reset(p); + return; + } + + case SET_EMAIL: { + if (!msg.contains("@") + || !msg.contains(".")) { + p.sendMessage(config.msg( + "email_format_error")); + reset(p); + return; + } + String oldEmail = (String) + db.getField(p.getName(), + "email"); + String emailAddr = msg.trim(); + // 已绑定邮箱:验证登录密码 + if (oldEmail != null + && !oldEmail.isEmpty()) { + state.targetPlayer = emailAddr; + state.type = + InputType.SET_EMAIL_VERIFY; + state.ticketTitle = + "need_pwd"; + p.sendMessage( + "§e您已绑定邮箱: " + + oldEmail); + p.sendMessage( + "§e请验证登录密码以确认修改:"); + return; + } + // 未绑定邮箱:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + + 100000)); + state.targetPlayer = emailAddr; + state.ticketTitle = code; + state.type = + InputType.SET_EMAIL_VERIFY; + final String to = emailAddr; + final String c = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + boolean sent = mainPlugin + .getEmail() + .sendVerifyCode( + to, c); + Bukkit.getScheduler() + .runTask(mainPlugin, + () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + reset(fp); + } + }); + }); + return; + } + + + case SET_EMAIL_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§c邮箱设置已取消"); + reset(p); + return; + } + String expected = + state.ticketTitle; + if (msg.trim().equals(expected)) { + db.setField(p.getName(), + "email", + state.targetPlayer); + p.sendMessage( + "§a邮箱已设置: " + + state.targetPlayer); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case ADMIN_AUTH: { + if (msg.equals( + config.adminPassword)) { + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openAdmin(p)); + } else { + p.sendMessage( + "§c管理密码错误"); + } + reset(p); + return; + } + + case ADMIN_SET_POINTS: { + try { + int pts = Integer.parseInt( + msg.trim()); + db.setField( + state.targetPlayer, + "points", pts); + p.sendMessage("§a已将 " + + state.targetPlayer + + " 积分设为 " + pts); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + String tgt = + state.targetPlayer; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openUserDetail( + p, tgt)); + reset(p); + return; + } + + case ADMIN_SET_PWD: { + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String tgt = state.targetPlayer; + String salt = + PasswordUtils + .generateSalt(); + String hash = + PasswordUtils.hash( + msg, salt); + db.setField(tgt, + "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + db.setField(tgt, + "temp_pw_used", 0); + p.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码," + + "请使用临时密码登录"); + // 邮件通知 + String emailAddr = (String) + db.getField(tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fBody = body; + final String fName = tgt; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> + mainPlugin.getEmail() + .sendTempPassword( + to, + fName, + fBody)); + } + reset(p); + return; + } + + case ADMIN_DELETE_CONFIRM: { + if (msg.equals( + state.targetPlayer)) { + db.deleteUser( + state.targetPlayer); + p.sendMessage("§a已删除 " + + state.targetPlayer); + Player tgt = + Bukkit.getPlayer( + state.targetPlayer); + if (tgt != null + && tgt.isOnline()) + tgt.kickPlayer( + "§c账号已被删除"); + } else { + p.sendMessage( + "§c输入不匹配,已取消"); + } + reset(p); + return; + } + + case ADMIN_SET_AFK_TIME: { + try { + int minutes = + Integer.parseInt( + msg.trim()); + config.afkTimeout = + minutes * 60; + config.saveSettings(); + p.sendMessage( + "§a挂机时长已设为 " + + minutes + + " 分钟"); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + reset(p); + return; + } + + case SMTP_CONFIG: { + String m = msg.trim(); + if ("000".equals(m) + || "取消取消取消" + .equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if ("取消".equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + int step = state.smtpStep; + if (step <= 1) { + if (!"0".equals(m)) + config.setSmtp( + "smtp地址", m); + state.smtpStep = 2; + p.sendMessage("§e当前端口: " + + config.getSmtp( + "smtp端口")); + p.sendMessage( + "§e输入端口(0跳过):"); + } else if (step == 2) { + if (!"0".equals(m)) + config.setSmtp( + "smtp端口", m); + state.smtpStep = 3; + p.sendMessage( + "§e输入发件邮箱:"); + } else if (step == 3) { + if (!"0".equals(m)) + config.setSmtp( + "smtp账号", m); + state.smtpStep = 4; + p.sendMessage( + "§e输入SMTP密码(0跳过):"); + } else if (step == 4) { + if (!"0".equals(m)) + config.setSmtp( + "smtp密码", m); + state.smtpStep = 5; + p.sendMessage( + "§e输入接收邮箱(留空发到发件邮箱):"); + } else if (step == 5) { + String recv = m; + if (recv.isEmpty() + || "0".equals(recv)) + recv = config.getSmtp( + "smtp账号"); + config.setSmtp( + "验证码接收邮箱", recv); + if (!config + .canSendSmtpVerify()) { + p.sendMessage( + "§c发送间隔不足1分钟"); + reset(p); + return; + } + String code = config + .generateSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String acct = config.getSmtp( + "smtp账号"); + if (acct.isEmpty() + || addr.isEmpty()) { + p.sendMessage( + "§c地址或账号为空"); + reset(p); + return; + } + config.recordSmtpSendTime(); + final String fRecv = recv; + final String fCode = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> fp.sendMessage( + "§e正在发送验证码...")); + boolean sent = + mainPlugin.getEmail() + .sendVerifyCode( + fRecv, + fCode); + final boolean fSent = sent; + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> { + if (fSent) { + state.type = + InputType + .SMTP_VERIFY; + state.smtpStep = 0; + fp.sendMessage( + "§a验证码已发送到 " + + fRecv); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败,查看控制台"); + reset(fp); + } + }); + }); + } + return; + } + + case SMTP_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if (config.checkSmtpVerifyCode( + msg.trim())) { + config.saveSmtp(); + config.clearSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String port = config.getSmtp( + "smtp端口"); + String acct = config.getSmtp( + "smtp账号"); + String recv = config.getSmtp( + "验证码接收邮箱"); + boolean ssl = config.smtpSsl; + p.sendMessage("§a§l=== SMTP配置完成 ==="); + p.sendMessage("§7地址: §f" + addr); + p.sendMessage("§7端口: §f" + port); + p.sendMessage("§7账号: §f" + acct); + p.sendMessage("§7接收: §f" + recv); + p.sendMessage("§7加密: §f" + ssl); + p.sendMessage("§a§l===================="); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case BACK_CHECKIN: { + String result = + mainPlugin.getCheckIn() + .backCheckIn(p, msg); + if (result == null) { + // 等待选择(NEED_CONFIRM) + return; + } + if (result.startsWith( + "NEED_CONFIRM:")) { + String[] parts = + result.split(":"); + // 存储到Main的Map + mainPlugin.pendingBackCheck.put( + p.getName(), + parts[1] + ":" + parts[2]); + return; + } + if (result.startsWith("§c")) { + p.sendMessage(result); + } else { + p.sendMessage(result); + } + reset(p); + return; + } + + + case INVITE_INPUT_CODE: { + String code = msg.trim(); + String myName = p.getName(); + String myCode = + (String) db.getField( + myName, + "invite_code"); + if (code.isEmpty()) { + p.sendMessage( + "§c邀请码不能为空"); + reset(p); + return; + } + if (code.equals(myCode)) { + p.sendMessage( + "§c不能用自己邀请码"); + reset(p); + return; + } + boolean found = false; + for (Map u : + db.getAllUsers()) { + String uc = (String) + u.getOrDefault( + "invite_code", ""); + if (code.equals(uc)) { + String inv = (String) + u.get("player_name"); + db.setField(myName, + "invited_by", inv); + db.addPoints(inv, 50); + p.sendMessage( + "§a邀请成功!邀请人: " + + inv); + Player ip = + Bukkit.getPlayer(inv); + if (ip != null + && ip.isOnline()) + ip.sendMessage("§a" + + myName + + " 使用了您的邀请码"); + found = true; + break; + } + } + if (!found) + p.sendMessage( + "§c邀请码不存在"); + reset(p); + return; + } + + case TICKET_TITLE: { + state.ticketTitle = msg; + state.type = + InputType.TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return; + } + + case TICKET_DESC: { + String tt = state.ticketType; + String tl = state.ticketTitle; + reset(p); + mainPlugin.getTicket() + .submitTicket( + p, tt, tl, msg); + return; + } + + case TICKET_SCORE: { + int tid = state.ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + reset(p); + mainPlugin.getTicket() + .confirmAndScore( + p, tid, sc); + } catch ( + NumberFormatException e) { + reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return; + } + + case TICKET_REJECT: { + int tid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .rejectTicket( + p, tid, msg); + return; + } + + case TICKET_ASSIGN: { + int tid = state.ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + reset(p); + mainPlugin.getTicket() + .assignToProvider( + p, tid, prov, + price); + final int fTid = tid; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getTicket() + .openDetail( + p, fTid)); + return; + } + + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return; + } + db.addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return; + } + + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.isServiceProvider(tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return; + } + db.removeServiceProvider(tgt); + p.sendMessage("§a已移除 " + tgt); + return; + } + + case TICKET_COMPLETE: { + try { + int tid = Integer.parseInt( + msg.trim()); + mainPlugin.getTicket() + .completeAsProvider( + p, tid); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效工单号"); + } + reset(p); + return; + } + + case TICKET_REPORT_ISSUE: { + String report = msg.trim(); + reset(p); + p.sendMessage("§e异常已上报"); + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§c§l[工单异常] §f" + + p.getName() + + ": " + report); + } + } + return; + } + + case TICKET_REPLY: { + int replyTid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .replyTicket( + p, replyTid, msg); + return; + } + + case TICKET_ADMIN_TYPE: { + state.ticketType = msg.trim(); + state.type = + InputType + .TICKET_ADMIN_TITLE; + p.sendMessage( + "§e请输入工单标题:"); + return; + } + + case TICKET_ADMIN_TITLE: { + state.ticketTitle = msg; + state.type = + InputType + .TICKET_ADMIN_DESC; + p.sendMessage( + "§e请输入工单描述:"); + return; + } + + case TICKET_ADMIN_DESC: { + state.targetPlayer = msg; + state.type = + InputType + .TICKET_ADMIN_PROVIDER; + p.sendMessage( + "§e输入服务商名和价格:"); + p.sendMessage( + "§7例: playerA 20"); + return; + } + + case TICKET_ADMIN_PROVIDER: { + String[] parts2 = msg.trim() + .split("\\s+"); + String prov2 = parts2[0]; + int price2 = 0; + if (parts2.length >= 2) { + try { + price2 = Integer.parseInt( + parts2[1]); + } catch ( + NumberFormatException e) { + } + } + String t2 = state.ticketType; + String ti2 = state.ticketTitle; + String d2 = state.targetPlayer; + reset(p); + mainPlugin.getTicket() + .adminCreateAndAssign( + p, t2, ti2, + d2, prov2, price2); + return; + } + + default: + reset(p); + return; + } + } +} diff --git a/backup/src/main/java/Sdf1_login/CheckInManager.java b/backup/src/main/java/Sdf1_login/CheckInManager.java new file mode 100644 index 0000000..b594b0f --- /dev/null +++ b/backup/src/main/java/Sdf1_login/CheckInManager.java @@ -0,0 +1,302 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class CheckInManager { + + private final Main plugin; + + public CheckInManager(Main plugin) { + this.plugin = plugin; + } + + // ===== 今日是否已签到 ===== + public boolean isCheckedInToday( + String name) { + String last = (String) plugin.getDb() + .getField(name, + "last_checkin_date"); + String today = new SimpleDateFormat( + "yyyy-MM-dd") + .format(new Date()); + return today.equals(last); + } + + // ===== 签到 ===== + // ===== 签到 ===== + public String checkIn(Player p) { + String name = p.getName(); + if (isCheckedInToday(name)) { + return plugin.getConfig2() + .msg("checkin_already"); + } + + Object streakObj = plugin.getDb() + .getField(name, "checkin_streak"); + int streak = streakObj != null + ? ((Number) streakObj).intValue() + : 0; + + String lastDate = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + + if (lastDate != null && !lastDate.isEmpty()) { + try { + java.util.Date lastD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(lastDate); + java.util.Date todayD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(today); + long diff = todayD.getTime() + - lastD.getTime(); + if (diff > 86400000L * 2) { + streak = 0; + } else if (diff > 86400000L) { + streak++; + } + } catch (Exception ignored) { + } + } + + if (streak == 0) streak = 1; + else streak++; + + double multi = 1.0; + if (streak >= 21) multi = 1.5; + else if (streak >= 14) multi = 1.3; + else if (streak >= 7) multi = 1.1; + + int basePoints = + plugin.getConfig2().checkinPoints; + int points = (int) (basePoints * multi); + + plugin.getDb().addPoints(name, points); + plugin.getDb().setField(name, + "checkin_streak", streak); + plugin.getDb().setField(name, + "last_checkin_date", today); + + Object totalObj = plugin.getDb() + .getField(name, "total_checkin_days"); + int total = totalObj != null + ? ((Number) totalObj).intValue() + : 0; + plugin.getDb().setField(name, + "total_checkin_days", total + 1); + + // ★ 债券盲盒奖励(6参数版,写入流水)★ + int bondMin = + plugin.getConfig2().checkinBondMin; + int bondMax = + plugin.getConfig2().checkinBondMax; + if (bondMin <= 0) bondMin = 1; + if (bondMax < bondMin) bondMax = bondMin; + int bondReward = bondMin + + new java.util.Random() + .nextInt(bondMax - bondMin + 1); + plugin.getBonds().addBonds(name, bondReward, + "daily_sign", "", "签到系统", + "每日签到连续" + streak + "天"); + + return plugin.getConfig2() + .msg("checkin_success") + .replace("{points}", + String.valueOf(points)) + .replace("{streak}", + String.valueOf(streak)) + .replace("{multi}", + String.valueOf(multi)) + + " §e+" + bondReward + "债券"; + } + + // ===== 获取连续天数 ===== + public int getStreak(String name) { + Object val = plugin.getDb() + .getField(name, + "checkin_streak"); + if (val instanceof Number) { + return ((Number) val) + .intValue(); + } + return 0; + } + // 标准化日期 + private String normalizeDate(String input) { + String s = input.trim().replace("/", "-"); + if (s.matches("\\d{8}")) { + return s.substring(0, 4) + "-" + + s.substring(4, 6) + "-" + + s.substring(6, 8); + } + return s; + } + // ===== 补签入口 ===== + public String backCheckIn( + Player p, String dateStr) { + String name = p.getName(); + String normalized = + normalizeDate(dateStr); + + // 严格日期验证 + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd"); + sdf.setLenient(false); + java.util.Date target; + try { + target = sdf.parse(normalized); + } catch (Exception ex) { + return "§c无效日期: " + dateStr; + } + String reformat = sdf.format(target); + if (!reformat.equals(normalized)) { + return "§c无效日期: " + dateStr; + } + + String today = sdf.format(new Date()); + if (normalized.compareTo(today) >= 0) { + return "§c今天不需要补签"; + } + + String threeDaysAgo = getPastDate(3); + if (normalized.compareTo( + threeDaysAgo) < 0) { + return "§c只能补签3天内" + + "(最多到 " + threeDaysAgo + ")"; + } + + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + boolean alreadyBacked = false; + if (!existing.isEmpty()) { + for (String d : existing.split(",")) { + if (normalizeDate(d.trim()) + .equals(normalized)) { + alreadyBacked = true; + break; + } + } + } + if (alreadyBacked) { + return "§c该日期已补签: " + normalized; + } + + Object normalCheck = + plugin.getDb().getField( + name, "last_checkin_date"); + String lastCheckin = normalCheck != null + ? normalCheck.toString() : ""; + if (lastCheckin.equals(normalized)) { + return "§c该日期已签到: " + normalized; + } + + // ★ 补签消耗:3债券 + 10积分 + // 积分不够时:用 9债券(3倍)代替 ★ + int bondCost = 3; + int pointCost = 10; + int bondAltCost = 9; // 3倍债券替代 + + int myBonds = plugin.getBonds() + .getBonds(name); + Object ptsObj = plugin.getDb() + .getField(name, "points"); + int myPoints = ptsObj != null + ? ((Number) ptsObj).intValue() + : 0; + + + // 方案A:3债券 + 10积分 + boolean canA = myBonds >= bondCost + && myPoints >= pointCost; + // 方案B:9债券(积分不够时) + boolean canB = myBonds >= bondAltCost + && myPoints < pointCost; + + if (!canA && !canB) { + if (myPoints < pointCost + && myBonds < bondAltCost) { + return "§c补签费用不足" + + "(需3债券+10积分," + + "或9债券代替)" + + " 当前: " + + myBonds + "债券 " + + myPoints + "积分"; + } + if (myBonds < bondCost) { + return "§c债券不足(需" + + bondCost + "枚)"; + } + return "§c积分不足(需" + + pointCost + "分)"; + } + + // 执行扣款 + if (canA) { + // 方案A:3债券 + 10积分 + plugin.getBonds().deductBonds( + name, bondCost, + "admin_deduct", "", + "补签系统", "补签扣款"); + plugin.getDb().deductPoints( + name, pointCost); + } else { + // 方案B:9债券代替 + plugin.getBonds().deductBonds( + name, bondAltCost, + "admin_deduct", "", + "补签系统", "补签扣款(债券替代)"); + } + + doBackCheckIn(p, normalized); + + String costMsg = canA + ? bondCost + "债券+" + pointCost + "积分" + : bondAltCost + "债券(替代)"; + return "§a补签成功!日期: " + + normalized + + " §7消耗: " + costMsg; + } + + + + // ===== 执行补签 ===== + // ===== 执行补签 ===== + public void doBackCheckIn( + Player p, String dateStr) { + String name = p.getName(); + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + String updated = existing.isEmpty() + ? dateStr + : existing + "," + dateStr; + plugin.getDb().setField(name, + "backcheck_dates", updated); + int streak = getStreak(name); + plugin.getDb().setField(name, + "checkin_streak", streak + 1); + // ★ 补签不发放奖励,只记录 + } + + // ===== 获取N天前日期 ===== + private String getPastDate( + int daysAgo) { + Calendar cal = + Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, + -daysAgo); + return new SimpleDateFormat( + "yyyy-MM-dd") + .format(cal.getTime()); + } +} \ No newline at end of file diff --git a/backup/src/main/java/Sdf1_login/CommissionManager.java b/backup/src/main/java/Sdf1_login/CommissionManager.java new file mode 100644 index 0000000..cb24687 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/CommissionManager.java @@ -0,0 +1,227 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Logger; + +public class CommissionManager { + + private final Main main; + private final Logger logger; + private final List rules = + new ArrayList<>(); + + public CommissionManager(Main main) { + this.main = main; + this.logger = main.getLogger(); + loadRules(); + } + + public void loadRules() { + rules.clear(); + File f = new File( + main.getDataFolder(), + "提成规则.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + // 格式: 邀请人数:提成百分比 + // 如 "5:15" = 邀请5人提成15% + String[] parts = + line.split(":"); + if (parts.length >= 2) { + try { + int count = Integer.parseInt( + parts[0].trim()); + double rate = Double + .parseDouble( + parts[1] + .trim()) / 100.0; + rules.add(new double[]{ + count, rate}); + } catch (Exception ignored) { + } + } + } + // 按人数排序(从大到小) + rules.sort((a, b) -> + Double.compare(b[0], a[0])); + logger.info( + "[Commission] 加载 " + + rules.size() + " 条提成规则"); + } catch (Exception e) { + logger.warning( + "[Commission] 加载失败: " + + e.getMessage()); + } + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 邀请提成规则"); + pw.println("# 格式: 邀请人数:提成百分比"); + pw.println("# 邀请10人提成20%"); + pw.println("10:20"); + pw.println("# 邀请5人提成15%"); + pw.println("5:15"); + pw.println("# 邀请1人提成10%"); + pw.println("1:10"); + } catch (IOException ignored) { + } + } + + /** + * 获取邀请人的提成比例 + * 根据已邀请人数匹配最高规则 + */ + public double getCommissionRate( + String inviter) { + int inviteCount = getInviteCount( + inviter); + for (double[] rule : rules) { + if (inviteCount >= (int) rule[0]) { + return rule[1]; + } + } + return 0; + } + + /** + * 获取邀请人数 + */ + public int getInviteCount( + String inviter) { + try { + Object val = + main.getDb().getField( + inviter, + "monthly_invite_count"); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + Object total = + main.getDb().getField( + inviter, + "total_invite_count"); + if (total instanceof Number) { + return ((Number) total).intValue(); + } + } catch (Exception ignored) { + } + return 0; + } + + /** + * 发放提成 + * 被邀请人获得奖励时,邀请人获得提成 + */ + public void payCommission( + String invitee, + double originalAmount) { + // 查找邀请人 + String inviter = getInviter(invitee); + if (inviter == null + || inviter.isEmpty()) return; + + double rate = getCommissionRate( + inviter); + if (rate <= 0) return; + + double commission = + originalAmount * rate; + if (commission <= 0) return; + + // 通过 Vault 发放 + var rsp = main.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy.Economy.class); + if (rsp == null) return; + net.milkbowl.vault.economy.Economy econ = + rsp.getProvider(); + if (econ == null) return; + + econ.depositPlayer(inviter, commission); + + // 通知邀请人 + Player inviterP = + main.getServer() + .getPlayer(inviter); + if (inviterP != null + && inviterP.isOnline()) { + inviterP.sendMessage( + "§a§l[邀请提成] §f" + + invitee + " 完成任务," + + "你获得 §e" + + String.format("%.2f", + commission) + + " §f游戏币提成 (" + + (int)(rate * 100) + "%)"); + } + + logger.info( + "[Commission] 提成: " + + inviter + " ← " + + invitee + " | " + + String.format("%.2f", + originalAmount) + + " × " + + (int)(rate * 100) + "% = " + + String.format("%.2f", + commission)); + } + + /** + * 获取邀请人 + */ + private String getInviter(String player) { + try { + Object val = + main.getDb().getField( + player, + "invited_by"); + if (val != null + && !val.toString() + .isEmpty()) { + return val.toString(); + } + } catch (Exception ignored) { + } + return null; + } + + /** + * 判断金额是否包含经济内容 + */ + public boolean hasEconomyContent( + String text) { + String t = text.toLowerCase(); + return t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额"); + } +} diff --git a/backup/src/main/java/Sdf1_login/ConfigManager.java b/backup/src/main/java/Sdf1_login/ConfigManager.java new file mode 100644 index 0000000..a15ada0 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ConfigManager.java @@ -0,0 +1,372 @@ +package Sdf1_login; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class ConfigManager { + + private final File dataFolder; + private final Map messages + = new LinkedHashMap<>(); + private final Map smtpSettings + = new LinkedHashMap<>(); + + // ===== 公共字段 ===== + public String adminTag = "admin"; + public String adminPassword = "qweasd"; + public int maxAccountsPerIP = 3; + public String approvalMode = "manual"; + public int autoApproveDelayMinutes = 30; + public int loginTimeout = 60; + public boolean afkEnabled = false; + public int afkTimeout = 300; + public boolean smtpSsl = true; + public String smtpVerifyCode = ""; + public long smtpVerifyExpire = 0; + public long smtpLastSendTime = 0; + public String smtpConfiguringPlayer = ""; + public String broadcastServerUrl = ""; + public int httpPort = 8080; + public String garbageTag = "admin"; + + public double checkinRewardFixed = 0; + public double checkinRewardMin = 0; + public double checkinRewardMax = 0; + public boolean checkinGivePoints = true; + public int checkinPoints = 10; + public String checkinRewardType = "none"; + public int backCheckPointMultiplier = 5; + public int checkinBondMin = 1; + public int checkinBondMax = 3; + + public double providerPointsPerScore = 2.0; + public int baseEconomyReward = 100; + public double providerEconomyPerScore = 50.0; + public int requesterPointsReward = 10; + + public boolean garbageEnabled = true; + public int garbageInterval = 300; + public int garbageMaxRounds = 1; + + // ★ 构造函数接收 File,和 Main.java 调用一致 ★ + public ConfigManager(File dataFolder) { + this.dataFolder = dataFolder; + } + public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济 + + + // ==================== 消息 ==================== + + public void loadMessages() { + messages.clear(); + File file = new File(dataFolder, "消息.txt"); + if (!file.exists()) createDefaultMessages(file); + loadMap(file, messages); + } + + public String msg(String key) { + String val = messages.getOrDefault(key, key); + val = val.replace("", "&l"); + val = val.replace("", "&r"); + val = val.replace("
", "\n"); + val = val.replace("\\n", "\n"); + val = val.replaceAll( + "&([0-9a-fk-orA-FK-OR])", "§$1"); + return val; + } + + private void createDefaultMessages(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 消息配置 ====="); + L.add("login_timeout=§c登录超时"); + L.add("not_registered=§e您尚未注册"); + L.add("not_logged_in=§e您尚未登录"); + L.add("already_registered=§c您已注册过"); + L.add("already_logged_in=§c您已登录"); + L.add("reg_success=§a注册成功!"); + L.add("login_success=§a登录成功!"); + L.add("login_failed=§c密码错误"); + L.add("password_wrong=§c密码错误"); + L.add("password_changed=§a密码修改成功!"); + L.add("password_format_error=§c密码需6位以上含大小写和数字"); + L.add("password_same=§c新密码不能与旧密码相同"); + L.add("checkin_already=§c您今日已签到"); + L.add("checkin_success=§a签到成功!获得 {points} 积分"); + L.add("chat_muted=§c你已被禁言"); + L.add("未知参数=§c未知参数或权限不足"); + writeLines(f, L); + } + + // ==================== SMTP ==================== + + public void loadSmtp() { + smtpSettings.clear(); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); + loadMap(file, smtpSettings); + } + + public String getSmtp(String key) { + return smtpSettings.getOrDefault(key, ""); + } + + public void setSmtp(String key, String value) { + smtpSettings.put(key, value); + } + + public void saveSmtp() { + File file = new File(dataFolder, "SMTP设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + for (Map.Entry e + : smtpSettings.entrySet()) { + L.add(e.getKey() + "=" + e.getValue()); + } + writeLines(file, L); + } + + private void createDefaultSmtp(File f) { + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + L.add("smtp地址=smtp.example.com"); + L.add("smtp端口=465"); + L.add("smtp账号="); + L.add("smtp密码="); + L.add("发件人名称=Sdf1_login"); + L.add("smtp加密=true"); + writeLines(f, L); + } + + // ==================== 插件设置 ==================== + + public void loadSettings() { + File file = new File(dataFolder, "插件设置.txt"); + if (!file.exists()) createDefaultSettings(file); + Map m = new LinkedHashMap<>(); + loadMap(file, m); + + adminTag = m.getOrDefault("管理标签", adminTag); + adminPassword = m.getOrDefault("管理密码", adminPassword); + maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3); + approvalMode = m.getOrDefault("审批模式", approvalMode); + autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30); + loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60); + afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false")); + afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300); + smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true")); + httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080); + broadcastServerUrl = m.getOrDefault("resource-pack-url", ""); + + providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0); + baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100); + providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0); + requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10); + + garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true")); + garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300); + garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1); + + checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0); + checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0); + checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0); + checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true")); + checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10); + backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5); + checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1); + checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3); + if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin; + + if (checkinRewardFixed > 0) checkinRewardType = "fixed"; + else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range"; + else checkinRewardType = "none"; + rewardChannel = m.getOrDefault("奖励发放方式", "债券"); + rewardChannel = normalizeRewardChannel(rewardChannel); + +// 验证合法值 + if (!"economy".equalsIgnoreCase(rewardChannel) + && !"bonds".equalsIgnoreCase(rewardChannel)) { + rewardChannel = "bonds"; + } + + + + // ★ 自动补充新配置项 ★ + boolean needAppend = false; + List newKeys = new ArrayList<>(); + if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); } + if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); } + if (!m.containsKey("奖励发放方式")) { + needAppend = true; + newKeys.add("奖励发放方式=债券"); + } + if (needAppend) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + for (String key : newKeys) { bw.write(key); bw.newLine(); } + bw.flush(); bw.close(); + System.out.println("[Config] 已补充签到债券配置"); + + } catch (IOException ignored) {} + if (!m.containsKey("奖励发放方式")) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + bw.write("奖励发放方式=bonds"); + bw.newLine(); + bw.flush(); bw.close(); + } catch (IOException ignored) {} + } + + } + } + /** + * 奖励方式标准化:中英文全兼容 + * "债券" "bonds" "Bond" "BONDS" → "bonds" + * "经济" "economy" "Economy" "ECONOMY" → "economy" + * 其他任何值 → "bonds"(默认债券) + */ + private String normalizeRewardChannel(String raw) { + if (raw == null) return "bonds"; + String s = raw.trim().toLowerCase(); + // 中文 + if (s.contains("债券")) return "bonds"; + if (s.contains("经济")) return "economy"; + // 英文 + if (s.contains("bond")) return "bonds"; + if (s.contains("econ")) return "economy"; + if (s.contains("Vault")) return "economy"; + // 兜底 + return "bonds"; + } + + public void saveSettings() { + File file = new File(dataFolder, "插件设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=" + adminTag); + L.add("管理密码=" + adminPassword); + L.add("每IP最大账号数=" + maxAccountsPerIP); + L.add("审批模式=" + approvalMode); + L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); + L.add("登录超时秒数=" + loginTimeout); + L.add("挂机踢出=" + afkEnabled); + L.add("挂机超时秒数=" + afkTimeout); + L.add("resource-pack-url=" + broadcastServerUrl); + L.add("http-port=" + httpPort); + L.add("垃圾站_启用=" + garbageEnabled); + L.add("垃圾站_清理间隔秒=" + garbageInterval); + L.add("垃圾站_保留轮数=" + garbageMaxRounds); + L.add("签到固定金额=" + checkinRewardFixed); + L.add("签到最小金额=" + checkinRewardMin); + L.add("签到最大金额=" + checkinRewardMax); + L.add("签到给积分=" + checkinGivePoints); + L.add("签到积分数量=" + checkinPoints); + L.add("签到债券最小=" + checkinBondMin); + L.add("签到债券最大=" + checkinBondMax); + L.add("补签积分倍率=" + backCheckPointMultiplier); + L.add("奖励发放方式=债券"); + writeLines(file, L); + } + + private void createDefaultSettings(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=admin"); + L.add("管理密码=qweasd"); + L.add("每IP最大账号数=3"); + L.add("审批模式=manual"); + L.add("自动审批延迟分钟=30"); + L.add("登录超时秒数=60"); + L.add("挂机踢出=false"); + L.add("挂机超时秒数=300"); + L.add("resource-pack-url="); + L.add("http-port=8080"); + L.add("垃圾站_启用=true"); + L.add("垃圾站_清理间隔秒=300"); + L.add("垃圾站_保留轮数=1"); + L.add("签到固定金额=0"); + L.add("签到最小金额=50"); + L.add("签到最大金额=500"); + L.add("签到给积分=true"); + L.add("签到积分数量=10"); + L.add("签到债券最小=1"); + L.add("签到债券最大=3"); + L.add("补签积分倍率=5"); + L.add("奖励发放方式=债券"); + writeLines(f, L); + } + public List getAfkWhitelist() { + List list = new ArrayList<>(); + File file = new File(dataFolder, "挂机白名单.txt"); + if (!file.exists()) { + try { + file.createNewFile(); + FileWriter fw = new FileWriter(file); + fw.write("# 每行一个玩家名\n"); + fw.flush(); + fw.close(); + } catch (IOException ignored) {} + return list; + } + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) + list.add(line); + } + } catch (IOException ignored) {} + return list; + } + + // ==================== 其他 ==================== + + public String generateSmtpVerifyCode() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length()))); + smtpVerifyCode = sb.toString(); + smtpVerifyExpire = System.currentTimeMillis() + 300000L; + return smtpVerifyCode; + } + + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; } + return smtpVerifyCode.equalsIgnoreCase(code); + } + + public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; } + public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; } + public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); } + + // ==================== 文件工具 ==================== + + private void loadMap(File file, Map map) { + try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); + } + } catch (IOException ignored) {} + } + + private void writeLines(File file, List lines) { + try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + for (String l : lines) { w.write(l); w.newLine(); } + } catch (IOException ignored) {} + } + + private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } } + private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } } +} diff --git a/backup/src/main/java/Sdf1_login/CypayCommand.java b/backup/src/main/java/Sdf1_login/CypayCommand.java new file mode 100644 index 0000000..7fd9067 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/CypayCommand.java @@ -0,0 +1,577 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +public class CypayCommand + implements CommandExecutor, TabCompleter { + + private final Main plugin; + private static final SimpleDateFormat SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + public CypayCommand(Main plugin) { + this.plugin = plugin; + } + + private boolean hasAdminPerm(CommandSender s) { + if (s.isOp()) return true; + if (!(s instanceof Player)) return true; + Player p = (Player) s; + String tag = + plugin.getConfig2().adminTag; + return !tag.isEmpty() + && p.getScoreboardTags().contains(tag); + } + + private int parseAmount(String s) { + return Integer.parseInt( + s.trim().replaceAll("[+]", "")); + } + + private List filter( + List list, String prefix) { + String low = prefix.toLowerCase(); + return list.stream() + .filter(x -> x.toLowerCase() + .startsWith(low)) + .collect(Collectors.toList()); + } + + @Override + public boolean onCommand(CommandSender sender, + Command cmd, + String label, + String[] args) { + + // ===== /cypay — 查自己 ===== + if (args.length == 0) { + String name = sender instanceof Player + ? sender.getName() : "Console"; + int bonds = + plugin.getBonds().getBonds(name); + String status = plugin.getBonds() + .getAccountStatus(name); + sender.sendMessage( + "§e§l[债券] §f" + name + + " 余额: §a" + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals(status) + ? "§c冻结中" + : "§a正常")); + return true; + } + + // ===== /cypay import ===== + /* if (args.length == 2 + && "import".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String file = args[1]; + if (!file.endsWith(".txt")) + file += ".txt"; + int result = plugin.getCDK() + .importFromFile(file); + if (result == -1) + sender.sendMessage("§c文件不存在: " + + file); + else + sender.sendMessage("§a导入完成: " + + result + " 条"); + return true; + }*/ + + // ===== /cypay freeze ===== + if (args.length == 2 + && "freeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (plugin.getBonds().isFrozen(target)) { + sender.sendMessage("§c" + target + + " 已处于冻结状态"); + return true; + } + plugin.getBonds().freezeAccount(target); + sender.sendMessage("§a已冻结 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§c§l[系统] 你的债券账户" + + "已被管理员冻结"); + return true; + } + + // ===== /cypay unfreeze ===== + if (args.length == 2 + && "unfreeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (!plugin.getBonds() + .isFrozen(target)) { + sender.sendMessage("§c" + target + + " 未被冻结"); + return true; + } + plugin.getBonds().unfreezeAccount(target); + sender.sendMessage("§a已解冻 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a§l[系统] 你的债券账户" + + "已被解冻"); + return true; + } + + // ===== /cypay water [days] ===== + if (args.length >= 2 + && "water".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + int days = 7; + if (args.length >= 3) { + try { + days = Integer.parseInt(args[2]); + } catch (NumberFormatException ignored) { + } + } + days = Math.min(days, + BondManager.ADMIN_MAX_WATER_DAYS); + + if (days <= 0) days = 1; + + List> txs = + plugin.getBonds() + .getTransactions( + target, days); + + sender.sendMessage( + "§e§l===== " + target + + " 流水记录 (近" + + days + "天) ====="); + sender.sendMessage("§f当前余额: §a" + + plugin.getBonds() + .getBonds(target) + + " §f状态: " + + (plugin.getBonds() + .isFrozen(target) + ? "§c冻结" : "§a正常")); + + if (txs.isEmpty()) { + sender.sendMessage("§7 (暂无记录)"); + } else { + for (Map tx : txs) { + sender.sendMessage( + formatTxLine(tx)); + } + } + sender.sendMessage( + "§e§l==========================" + + "=========="); + return true; + } + // ===== /cypay printer [player] ===== + if ("printer".equalsIgnoreCase(args[0])) { + if (!hasAdminPerm(sender) + && !(sender instanceof Player) + == false) { + // 控制台也可以用 + if (!(sender instanceof Player)) + ; // 放行 + else if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + } + if (args.length == 1) { + // 打印全服 + sender.sendMessage( + "§e正在导出全服流水..."); + plugin.getBonds(); // 确保实例就绪 + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printAll(sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + if (args.length == 2) { + // 打印指定玩家 + String target = args[1]; + sender.sendMessage("§e正在导出 " + + target + " 的流水..."); + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printPlayer(target, sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + } + + // ===== /cypay give/remove ===== + if (args.length == 3) { + String action = args[0].toLowerCase(); + if ("give".equals(action) + || "remove".equals(action)) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + try { + int amount = + parseAmount(args[2]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + String txType = + "give".equals(action) + ? BondManager + .TX_ADMIN_GIVE + : BondManager + .TX_ADMIN_DEDUCT; + String opName = + sender instanceof Player + ? sender.getName() + : "Console"; + + if ("give".equals(action)) { + plugin.getBonds().addBonds( + target, amount, + txType, "", + opName, "管理员发放"); + sender.sendMessage( + "§a已给 " + target + + " " + amount + + " 债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a管理员给了你 " + + amount + + " 债券"); + } else { + if (plugin.getBonds() + .deductBonds(target, + amount, + txType, "", + opName, + "管理员扣除")) { + sender.sendMessage( + "§a已扣除 " + target + + " " + amount + + " 债券"); + Player tp = plugin + .getServer() + .getPlayerExact( + target); + if (tp != null) + tp.sendMessage( + "§c管理员扣除了" + + "你的 " + + amount + + " 债券"); + } else { + sender.sendMessage( + "§c" + target + + " 债券不足" + + "或账户冻结"); + } + } + } catch (NumberFormatException e) { + sender.sendMessage( + "§c金额格式错误"); + } + return true; + } + } + + // ===== /cypay — 仅玩家转账 ===== + if (args.length == 2) { + try { + int amount = parseAmount(args[1]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + // ★ 控制台禁止转账 + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c控制台不能转账,请使用 " + + "/cypay give <玩家> <金额>"); + return true; + } + Player p = (Player) sender; + String target = args[0]; + BondManager.TransferResult r = + plugin.getBonds().transfer( + p.getName(), + target, amount); + if (r.success) { + p.sendMessage("§e§l[债券转账] §f向 §a" + + target + " §f转出 §c" + + amount + " §f枚债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage("§e§l[债券转账] §f收到 §a" + + p.getName() + + " §f转入 §a" + + amount + " §f枚债券"); + } else { + p.sendMessage("§c" + r.message + + ",当前: " + + plugin.getBonds() + .getBonds(p.getName())); + } + return true; + } catch (NumberFormatException ignored) { + } + } + + // ===== /cypay — 查指定玩家 ===== + if (args.length == 1) { + String target = args[0]; + // ★ 验证玩家在 login.db 中存在 ★ + if (!plugin.getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + + // String target = args[0]; + if (hasAdminPerm(sender)) { + int bonds = plugin.getBonds() + .getBonds(target); + String status = plugin.getBonds() + .getAccountStatus(target); + sender.sendMessage( + "§e§l[债券] §f" + target + + " 余额: §a" + + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals( + status) + ? "§c冻结中" + : "§a正常")); + } else { + int bonds = plugin.getBonds() + .getBonds( + sender.getName()); + sender.sendMessage( + "§e§l[债券] §f你的余额: §a" + + bonds); + } + return true; + } + + showHelp(sender); + return true; + } + + // ===== 流水行格式化 ===== + + private String formatTxLine( + Map tx) { + String type = (String) tx.get("type"); + int amount = ((Number) tx.get("amount")) + .intValue(); + String tgt = (String) tx.get("target_player"); + String reason = (String) tx.get("reason"); + int bal = ((Number) tx.get("balance_after")) + .intValue(); + long time = ((Number) tx.get("time")) + .longValue(); + + boolean pos = amount > 0; + String sign = pos ? "+" : ""; + String color = pos ? "§a" : "§c"; + + StringBuilder sb = new StringBuilder(); + sb.append("§7[") + .append(SDF.format(new Date(time))) + .append("] ") + .append(formatTxType(type)) + .append(" ") + .append(color).append(sign) + .append(amount).append("§7"); + if (tgt != null && !tgt.isEmpty()) + sb.append(" §e↔ ").append(tgt); + if (reason != null && !reason.isEmpty()) + sb.append(" §7(").append(reason) + .append(")"); + sb.append(" §7余额:§f").append(bal); + return sb.toString(); + } + + private String formatTxType(String type) { + if (type == null) return "§7未知"; + switch (type) { + case BondManager.TX_ADMIN_GIVE: + return "§a管理员发放"; + case BondManager.TX_ADMIN_DEDUCT: + return "§c管理员扣除"; + case BondManager.TX_TRANSFER_OUT: + return "§c转出"; + case BondManager.TX_TRANSFER_IN: + return "§a转入"; + case BondManager.TX_REDEEM: + return "§aCDK兑换"; + case BondManager.TX_SHOP_BUY: + return "§e商城购买"; + case BondManager.TX_DAILY_SIGN: + return "§a签到奖励"; + case BondManager.TX_FREEZE: + return "§c冻结"; + case BondManager.TX_UNFREEZE: + return "§a解冻"; + default: + return "§7" + type; + } + } + + private void showHelp(CommandSender sender) { + sender.sendMessage("§e§l债券命令帮助:"); + sender.sendMessage( + "§e/cypay §7- 查询自己余额"); + sender.sendMessage( + "§e/cypay <玩家> §7- 查余额(管理员)"); + sender.sendMessage( + "§e/cypay <玩家> <金额> §7- 转账"); + if (hasAdminPerm(sender)) { + sender.sendMessage( + "§e/cypay give <玩家> <金额> §7- 给债券"); + sender.sendMessage( + "§e/cypay remove <玩家> <金额> §7- 扣债券"); + sender.sendMessage( + "§e/cypay import <文件> §7- 导入CDK"); + sender.sendMessage( + "§e/cypay freeze <玩家> §7- 冻结"); + sender.sendMessage( + "§e/cypay unfreeze <玩家> §7- 解冻"); + sender.sendMessage( + "§e/cypay water <玩家> [天数] §7- 流水"); + sender.sendMessage( + "§e/cypay printer §7- 导出全服流水xlsx"); + sender.sendMessage( + "§e/cypay printer <玩家> §7- 导出指定玩家xlsx"); + + } + } + + @Override + public List onTabComplete( + CommandSender sender, Command command, + String label, String[] args) { + List list = new ArrayList<>(); + + if (args.length == 1) { + String last = args[0].toLowerCase(); + if (hasAdminPerm(sender)) { + list.add("give"); + list.add("remove"); + list.add("import"); + list.add("freeze"); + list.add("unfreeze"); + list.add("water"); + list.add("printer"); + + File cdkDir = new File( + plugin.getDataFolder(), + "cdk"); + if (cdkDir.exists()) { + File[] files = cdkDir.listFiles( + (d, n) -> n + .endsWith(".txt")); + if (files != null) + for (File f : files) + list.add(f.getName()); + } + } + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, last); + } + + if (args.length == 2) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first) + || "freeze".equals(first) + || "unfreeze".equals(first) + || "water".equals(first)) { + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, + args[1].toLowerCase()); + } + list.add("10"); + list.add("50"); + list.add("100"); + return filter(list, + args[1].toLowerCase()); + } + + if (args.length == 3) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first)) { + list.add("10"); + list.add("50"); + list.add("100"); + list.add("999"); + return filter(list, + args[2].toLowerCase()); + } + if ("water".equals(first)) { + list.add("1"); + list.add("3"); + list.add("7"); + list.add("14"); + return filter(list, + args[2].toLowerCase()); + } + } + + return list; + } +} diff --git a/backup/src/main/java/Sdf1_login/DatabaseManager.java b/backup/src/main/java/Sdf1_login/DatabaseManager.java new file mode 100644 index 0000000..cb2b8f7 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/DatabaseManager.java @@ -0,0 +1,1758 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + + ")"); + + + // 安全添加 users 缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 inventory_data 列判断) + boolean hasOldTable = false; + try { + ResultSet rs = st.executeQuery( + "SELECT inventory_data " + + "FROM inventory_backups " + + "LIMIT 1"); + hasOldTable = true; + rs.close(); + } catch (SQLException e) { + hasOldTable = false; + } + + if (hasOldTable) { + // 旧表存在,迁移数据到新表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups_new (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 迁移旧数据 + try { + st.execute("INSERT INTO " + + "inventory_backups_new " + + "(player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience) " + + "SELECT player_name, " + + "inventory_data, " + + "armor_data, " + + "extra_data, " + + "player_level, " + + "player_exp " + + "FROM inventory_backups"); + } catch (SQLException ignored) { + } + st.execute("DROP TABLE " + + "inventory_backups"); + st.execute("ALTER TABLE " + + "inventory_backups_new " + + "RENAME TO " + + "inventory_backups"); + } else { + // 无旧表或已迁移,直接创建 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + } + + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + // PVP区域表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER DEFAULT 0" + + ")"); + +// PVP击杀总榜表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT NOT NULL," + + "region_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, region_name)" + + ")"); + + +// 菜单图标表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "menu_icons (" + + "player_name TEXT PRIMARY KEY," + + "icon_b64 TEXT DEFAULT ''," + + "item_name TEXT DEFAULT ''," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 垃圾回收站物品表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "item_data TEXT DEFAULT ''," + + "amount INTEGER DEFAULT 1," + + "create_time INTEGER DEFAULT 0" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +// ==================== 菜单图标 ==================== + + public void saveMenuIcon(String name, + String iconB64, String itemName) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO menu_icons " + + "(player_name, icon_b64, " + + "item_name, save_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, iconB64); + ps.setString(3, itemName); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT icon_b64 FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String b64 = null; + if (rs.next()) { + b64 = rs.getString("icon_b64"); + } + rs.close(); + ps.close(); + return b64; + } catch (SQLException e) { + return null; + } + } + + public void deleteMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM menu_icons " + + "WHERE player_name=? " + + "AND icon_b64 IS NOT NULL " + + "AND icon_b64 != '' " + + "LIMIT 1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public void createUser(String name, String hash, + String salt) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + public boolean checkPasswordOrTemp( + String name, String hash) { + // 先检查主密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean checkPassword(String name, + String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +// ==================== PVP区域 ==================== + + public void savePVPRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions " + + "(name, world, " + + "x1, y1, z1, " + + "x2, y2, z2, " + + "create_time) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map + getPVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getAllPVPRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + db.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deletePVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + +// ==================== PVP击杀 ==================== + + public int getPVPKills( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int kills = 0; + if (rs.next()) + kills = rs.getInt("kills"); + rs.close(); + ps.close(); + return kills; + } catch (SQLException e) { + return 0; + } + } + + public int getPVPDeaths( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int deaths = 0; + if (rs.next()) + deaths = rs.getInt("deaths"); + rs.close(); + ps.close(); + return deaths; + } catch (SQLException e) { + return 0; + } + } + + public void addPVPKill( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,1,0) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "kills=kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void addPVPDeath( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,0,1) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "deaths=deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getPVPKillTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT player_name, " + + "kills, deaths " + + "FROM pvp_kills " + + "WHERE " + + "region_name=? " + + "AND kills>0 " + + "ORDER BY kills " + + "DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Connection getDb() { + return db; + } +} diff --git a/backup/src/main/java/Sdf1_login/EmailManager.java b/backup/src/main/java/Sdf1_login/EmailManager.java new file mode 100644 index 0000000..b6906e5 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/EmailManager.java @@ -0,0 +1,284 @@ +package Sdf1_login; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Base64; +import java.util.logging.Logger; + +public class EmailManager { + + private final ConfigManager config; + private final Logger logger; + + public EmailManager(ConfigManager config) { + this.config = config; + this.logger = Logger.getLogger("Sdf1_login"); + } + + public boolean isConfigured() { + String host = config.getSmtp("smtp地址"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + return host != null && !host.isEmpty() + && user != null && !user.isEmpty() + && pass != null && !pass.isEmpty(); + } + + public boolean sendVerifyCode(String to, + String code) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置," + + "无法发送验证码"); + return false; + } + logger.info("[Sdf1_login] 发送验证码到 " + to); + return sendEmailSync(to, + "[Sdf1_login] 验证码", + "您的验证码: " + code + + "\n5分钟内有效。\n" + + "如非本人操作请忽略。"); + } + + public boolean sendTempPassword(String to, + String player, String tempPwd) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, + "[Sdf1_login] 临时密码", + "玩家 " + player + + " 的临时密码: " + tempPwd + + "\n请尽快登录并修改密码!"); + } + + public boolean sendBody(String to, String subject, String body) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, subject, body); + } + + + private boolean sendEmailSync(String to, + String subject, String body) { + final boolean[] result = {false}; + Thread t = new Thread(() -> { + try { + result[0] = sendEmail( + to, subject, body); + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件线程异常: " + + e.getMessage()); + } + }, "Sdf1_login-SMTP"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch (InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + logger.severe( + "[Sdf1_login] SMTP超时(15秒)"); + return false; + } + return result[0]; + } + + private boolean sendEmail(String to, + String subject, String body) { + String host = config.getSmtp("smtp地址"); + String portStr = config.getSmtp("smtp端口"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + String fromName = config.getSmtp( + "发件人名称"); + + int port; + try { + port = Integer.parseInt(portStr); + } catch (Exception e) { + port = 465; + } + + try { + String encSubject = "=?UTF-8?B?" + + Base64.getEncoder() + .encodeToString( + subject.getBytes("UTF-8")) + + "?="; + String encBody = Base64.getEncoder() + .encodeToString( + body.getBytes("UTF-8")); + + if (port == 465) { + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket socket = (SSLSocket) + factory.createSocket( + host, port); + socket.setSoTimeout(10000); + doSmtp(socket, host, user, pass, + to, encSubject, encBody, + fromName); + socket.close(); + } else { + Socket plainSocket = + new Socket(host, port); + plainSocket.setSoTimeout(10000); + BufferedReader preReader = + new BufferedReader( + new InputStreamReader( + plainSocket + .getInputStream(), + "UTF-8")); + PrintWriter preWriter = + new PrintWriter( + plainSocket + .getOutputStream(), + true); + checkResponse(preReader, "220"); + sendLine(preWriter, + "EHLO " + host); + checkResponse(preReader, "250"); + sendLine(preWriter, "STARTTLS"); + checkResponse(preReader, "220"); + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket sslSocket = (SSLSocket) + factory.createSocket( + plainSocket, host, + port, true); + sslSocket.startHandshake(); + doSmtp(sslSocket, host, user, pass, + to, encSubject, encBody, + fromName); + sslSocket.close(); + } + logger.info( + "[Sdf1_login] 邮件发送成功 -> " + + to); + return true; + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件异常: " + + e.getClass() + .getSimpleName() + + ": " + e.getMessage()); + if (e.getCause() != null) { + logger.severe( + "[Sdf1_login] 根因: " + + e.getCause() + .getMessage()); + } + return false; + } + } + + private void doSmtp(SSLSocket socket, + String host, String user, String pass, + String to, String subject, String body, + String fromName) throws Exception { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + socket.getInputStream(), + "UTF-8")); + OutputStream rawOut = + socket.getOutputStream(); + PrintWriter writer = + new PrintWriter( + new java.io.BufferedOutputStream( + rawOut), true); + + checkResponse(reader, "220"); + sendLine(writer, "EHLO " + host); + checkResponse(reader, "250"); + sendLine(writer, "AUTH LOGIN"); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + user.getBytes("UTF-8"))); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + pass.getBytes("UTF-8"))); + checkResponse(reader, "235"); + sendLine(writer, + "MAIL FROM:<" + user + ">"); + checkResponse(reader, "250"); + sendLine(writer, + "RCPT TO:<" + to + ">"); + checkResponse(reader, "250"); + sendLine(writer, "DATA"); + checkResponse(reader, "354"); + sendLine(writer, + "From: " + fromName + + " <" + user + ">"); + sendLine(writer, + "To: <" + to + ">"); + sendLine(writer, + "Reply-To: <" + user + ">"); + sendLine(writer, + "Subject: " + subject); + sendLine(writer, "MIME-Version: 1.0"); + sendLine(writer, + "Content-Type: text/plain; " + + "charset=UTF-8"); + sendLine(writer, + "Content-Transfer-Encoding: " + + "base64"); + sendLine(writer, ""); + sendLine(writer, body); + sendLine(writer, "."); + checkResponse(reader, "250"); + sendLine(writer, "QUIT"); + } + + private void sendLine(PrintWriter writer, + String line) { + writer.print(line + "\r\n"); + writer.flush(); + } + + private void checkResponse( + BufferedReader reader, + String expected) throws Exception { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + if (line.length() >= 4 + && Character.isDigit( + line.charAt(0)) + && line.charAt(3) == ' ') + break; + } + String resp = sb.toString().trim(); + if (!resp.startsWith(expected)) { + if (resp.startsWith("535")) + logger.severe( + "[Sdf1_login] SMTP 535 " + + "认证失败"); + throw new Exception( + "SMTP响应异常: 期望 " + + expected + + ",实际: " + resp); + } + } +} diff --git a/backup/src/main/java/Sdf1_login/GUIManager.java b/backup/src/main/java/Sdf1_login/GUIManager.java new file mode 100644 index 0000000..cbc7102 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/GUIManager.java @@ -0,0 +1,2681 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + + + + +import java.util.*; + +public class GUIManager implements Listener { + + private final Main plugin; + + public static final String T_MAIN = + "§6§l服务器菜单"; + public static final String T_MORE = + "§6§l更多功能"; + public static final String T_EDITOR = + "§6§l编辑菜单项"; + public static final String T_BIOME = + "§b§l目标群系传送"; + public static final String T_MY_INFO = + "§6§l我的信息"; + public static final String T_GIFT_STAGES = + "§6§l礼包阶段"; + public static final String T_ADMIN = + "§6§l管理员面板"; + public static final String T_INVITE = + "§6§l邀请数据"; + public static final String T_USER_MGMT = + "§6§l用户管理"; + public static final String T_TASK_CENTER = + "§6§l任务中心"; + private final Map userMgmtPages = new HashMap<>(); + private static final int UMGMT_PAGE_SIZE = 45; + + + + public GUIManager(Main plugin) { + this.plugin = plugin; + } + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_MAIN); +// ===== 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + + // 已占用的槽位 + Set occupied = new HashSet<>(); + occupied.add(10); + occupied.add(12); + occupied.add(14); + occupied.add(16); + occupied.add(19); + occupied.add(21); + occupied.add(23); + occupied.add(25); + occupied.add(29); + occupied.add(31); + occupied.add(32); + occupied.add(34); + + // 自定义区域:第1排(0-8) + 第5排(36-44) + 第6排(45-53) + int[] customArea = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53 + }; + + int autoIdx = 0; + for (int ci = 0; + ci < customs.size() && ci < 27; ci++) { + MenuManager.MenuItem mi = + customs.get(ci); + + // 权限过滤 + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + + int targetSlot = -1; + + // 玩家指定了格子? + if (mi.slot != null) { + targetSlot = findBestSlot( + mi.slot - 1, occupied); + } + + // 未指定或冲突 → 自动排队 + if (targetSlot < 0) { + while (autoIdx < customArea.length) { + int s = customArea[autoIdx++]; + if (!occupied.contains(s)) { + targetSlot = s; + break; + } + } + } + + if (targetSlot < 0 + || targetSlot >= 54) continue; + occupied.add(targetSlot); + + // 放置物品 + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(java.util.Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(targetSlot, icon); + } + } + +// 背景填充 + fillBg(g); + p.openInventory(g); + + // ===== Row1(9-17): 核心功能 ===== + g.setItem(10, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(12, mkItem(Material.PAPER, + "§b§l邀请数据")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + g.setItem(16, mkItem(Material.COMPASS, + "§b§l群系传送", + "§7传送到指定群系")); + + // ===== Row2(18-26): 日常功能 ===== + g.setItem(19, mkItem(Material.BOOK, + "§6工单系统")); + g.setItem(21, mkItem(Material.EMERALD_BLOCK, + "§a§l每日签到", + "§7签到获取积分和惊喜奖励")); + g.setItem(23, mkItem(Material.ENDER_CHEST, + "§6§l任务中心")); + if (p.isOp() || isAdmin(p)) { + g.setItem(25, mkItem(Material.HOPPER, + "§6§l垃圾回收站", + "§7查看和取回清理的物品")); + + + } + // ===== Row3(27-35): 管理功能 ===== + // ★ 商店 - 所有人都能看 ★ + g.setItem(27, mkItem(Material.CHEST, + "§6§l商店", + "§7浏览、购买、出售物品", + "§7货币: 债券")); + + if (isAdmin(p)) { + g.setItem(29, mkItem( + Material.REDSTONE_BLOCK, + "§c§l管理员面板")); + } + if (p.isOp()) { + g.setItem(31, mkItem(Material.ECHO_SHARD, + "§c§l管理菜单", + "§7添加、编辑、删除菜单项")); + } + // 在 openMain 方法中,Row3 区域加: + + // ★ 余额查询 - 所有人都能看 ★ + double myBal = 0; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault.economy + .Economy.class); + if (reg != null) + myBal = reg.getProvider() + .getBalance(p); + } catch (Exception ignored) {} + int myBonds = plugin.getBonds().getBonds(p.getName()); + g.setItem(32, mkItem(Material.EMERALD, + "§b§l我的钱包", + "§7查看债券余额与流水记录")); + +// ★ 余额操作 - 仅OP和tag玩家 ★ + if (isAdmin(p)) { + g.setItem(34, mkItem(Material.GOLD_INGOT, + "§6§l余额操作", + "§7给钱、扣钱")); + } + + + + // ===== Row0(0-8): 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + for (int i = 0; + i < customs.size() && i < 9; + i++) { + MenuManager.MenuItem mi = + customs.get(i); + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(i, icon); + } + } + + // 背景最后填充(不覆盖物品) + fillBg(g); + p.openInventory(g); + } + private int findBestSlot( + int requested, + Set occupied) { + // 目标位空闲直接用 + if (requested >= 0 && requested < 54 + && !occupied.contains(requested)) { + return requested; + } + // BFS上下左右找最近空位 + int row = requested / 9; + int col = requested % 9; + boolean[][] visited = + new boolean[6][9]; + java.util.Queue queue = + new java.util.LinkedList<>(); + queue.add(new int[]{row, col}); + visited[row][col] = true; + int[][] dirs = { + {-1, 0}, {1, 0}, + {0, -1}, {0, 1}}; + while (!queue.isEmpty()) { + int[] cur = queue.poll(); + for (int[] d : dirs) { + int nr = cur[0] + d[0]; + int nc = cur[1] + d[1]; + if (nr >= 0 && nr < 6 + && nc >= 0 && nc < 9 + && !visited[nr][nc]) { + visited[nr][nc] = true; + int s = nr * 9 + nc; + if (!occupied.contains(s)) { + return s; + } + queue.add(new int[]{nr, nc}); + } + } + } + return -1; + } + + public void openMenuManager(Player p) { + List list = + plugin.getMenu().getItems(); + Inventory g = Bukkit.createInventory( + null, 27, + "§c§l菜单管理"); + fillBg(g); + + for (int i = 0; + i < list.size() && i < 9; i++) { + MenuManager.MenuItem mi = list.get(i); + g.setItem(i, mkItem( + mi.icon.getType(), + "§e" + mi.title, + "§7指令: " + mi.command, + "§7权限: " + mi.permType, + "", + "§a左键编辑", + "§7右键切换权限")); + } + + g.setItem(22, mkItem(Material.LIME_WOOL, + "§a§l添加菜单项")); + + g.setItem(26, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + + public void openBalanceOps(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作"); + fillBg(g); + + net.milkbowl.vault.economy.Economy eco = null; + try { + var reg = plugin.getServer().getServicesManager() + .getRegistration(net.milkbowl.vault.economy.Economy.class); + if (reg != null) eco = reg.getProvider(); + } catch (Exception ignored) {} + + int slot = 0; + for (Player op : Bukkit.getOnlinePlayers()) { + if (slot >= 45) break; + double bal = 0; + if (eco != null) bal = eco.getBalance(op); + boolean isSelf = op.getName().equals(p.getName()); + int bondBal = plugin.getBonds().getBonds(op.getName()); + g.setItem(slot, mkItem( + isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD, + "§e" + op.getName(), + "§7金币: §a$" + String.format("%.2f", bal), + "§7债券: §e" + bondBal + " §6枚", + "", + "§a左键经济(±) §6右键债券(±)")); + slot++; + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openShop(Player p) { + p.closeInventory(); + plugin.getShopManager().openShopMain(p); + } + + public void openTicket(Player p) { + p.closeInventory(); + p.performCommand("sdf1_login ticket"); + } + @EventHandler + public void onInvClose( + + InventoryCloseEvent e) { + if (!(e.getPlayer() instanceof Player)) + return; + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + Player p3 = (Player) e.getPlayer(); + String t = e.getView().getTitle(); + if (!(e.getPlayer() instanceof Player)) return; + Player p2 = (Player) e.getPlayer(); + + if (!t.equals(T_EDITOR)) return; + + // ★ 编辑器关闭时清理空项 ★ + List list = + plugin.getMenu().getItems(); + if (!list.isEmpty()) { + MenuManager.MenuItem last = + list.get(list.size() - 1); + if ((last.command == null + || last.command.trim().isEmpty() + || last.command.trim() + .equals("/")) + && "新菜单项" + .equals(last.title)) { + list.remove(list.size() - 1); + } + } + } + + public void openSubMenu( + Player p, String fileName) { + String menuTitle = + "§6§l" + fileName + .replace(".txt", ""); + Inventory g = Bukkit.createInventory( + null, 54, menuTitle); + fillBg(g); + + java.io.File f = new java.io.File( + plugin.getDataFolder(), fileName); + if (!f.exists()) { + p.sendMessage("§c文件不存在: " + + fileName); + return; + } + try { + List lines = + java.nio.file.Files + .readAllLines( + f.toPath(), + java.nio.charset + .StandardCharsets + .UTF_8); + List items = + new ArrayList<>(); + StringBuilder buf = + new StringBuilder(); + boolean inItem = false; + + for (String raw : lines) { + String l = raw.trim(); + if (l.startsWith("#") + || l.startsWith("//")) + continue; + if (l.contains("")) { + inHtml = false; + l = l.substring( + l.indexOf("-->") + 3) + .trim(); + } else { + plugin.getLogger().info( + "[Menu] L" + lineNum + + ": [HTML注释中]"); + continue; + } + } + + // ===== 第3优先:整行注释(最高裁决) ===== + // 以 # 或 // 开头的行,直接跳过 + // 不管后面包含什么 /* "); + pw.println(); + pw.println("{"); + pw.println(" 标题:主城传送"); + pw.println(" 指令:/spawn"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:COMPASS"); + pw.println("}"); + pw.println(); + pw.println("{"); + pw.println(" 标题:随机传送"); + pw.println(" 指令:/rtp"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:ENDER_PEARL"); + pw.println("}"); + } catch (Exception ignored) { + } + } + + // ===== 保存 ===== + public void saveMenu() { + File f = new File( + plugin.getDataFolder(), + "菜单.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 服务器菜单"); + pw.println(); + for (MenuItem mi : items) { + pw.println("{"); + pw.println(" 标题:" + + mi.title); + pw.println(" 指令:" + + mi.command); + pw.println(" 权限类型:" + + mi.permType); + // ★ 保存实际材质名 ★ + pw.println(" 图标:" + + mi.icon.getType().name()); + pw.println("}"); + pw.println(); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Menu] 保存失败: " + + e.getMessage()); + } + } + + // ===== 聊天编辑 ===== + public boolean isEditing(String name) { + return chatWait.contains(name); + } + + public void editTitle( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "title"); + p.closeInventory(); + p.sendMessage("§e请输入菜单标题:"); + } + + public void editCommand( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "command"); + p.closeInventory(); + p.sendMessage("§e请输入指令" + + "(自动补全/):"); + } + + public void onChat(Player p, String input) { + String name = p.getName(); + if (!chatWait.remove(name)) return; + + String field = chatField.remove(name); + Integer idx = chatIdx.get(name); + if (idx == null || idx >= items.size()) return; + + MenuItem mi = items.get(idx); + + if ("title".equals(field)) { + mi.title = input; + p.sendMessage("§a标题: " + input); + } else if ("command".equals(field)) { + if (!input.startsWith("/")) + input = "/" + input; + mi.command = input; + p.sendMessage("§a指令: " + input); + } + + Bukkit.getScheduler().runTask(plugin, () -> + plugin.gui.openEditor(p, idx)); + } +} diff --git a/backup/src/main/java/Sdf1_login/OrderManager.java b/backup/src/main/java/Sdf1_login/OrderManager.java new file mode 100644 index 0000000..9dcb625 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/OrderManager.java @@ -0,0 +1,1345 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; + +public class OrderManager implements Listener { + private final Main plugin; + private final List orders = + Collections.synchronizedList(new ArrayList<>()); + private final Map userViewPage = new HashMap<>(); + private final Map adminViewPage = new HashMap<>(); + private final Map adminViewTarget = new HashMap<>(); + private final Set partialRefundListening = new HashSet<>(); + private final Map partialRefundTarget = new HashMap<>(); + private final Map receiptBuilderSection = new HashMap<>(); + private String tplHeader = ""; + private String tplItemLine = "{品名} x{数量} {原价}→{执行价} 小计{小计}枚"; + private String tplDiscountLine = "优惠: -{优惠额}枚 ({优惠类型})"; + private String tplPackLine = "打包: +{打包费}枚 ({打包颜色})"; + private String tplSummary = "原价{原价合计} → 实付{实付}"; + private String tplFooter = "感谢惠顾,欢迎再来!"; + private static final long RETENTION_30D = 30L * 24 * 60 * 60 * 1000; + private static final long SELF_REFUND_WINDOW = 5 * 60 * 1000; + private static final int PAGE_SIZE = 21; + private static final int[] SLOTS = { + 10,11,12,13,14,15,16,19,20,21,22,23,24,25,28,29,30,31,32,33,34}; + // ===== 字段 ===== + private final Set shulkerUsedOrders = + Collections.synchronizedSet(new HashSet<>()); + // ===== 待审批的退货退款 ===== + private final Map pendingReturns = + new HashMap<>(); + // ===== 退货申请确认 ===== + private final Map returnConfirmWaiting = + new HashMap<>(); // uuid -> orderId + private static final int RETURN_CONFIRM_TIMEOUT = 600; // 30秒 + + + private static class PendingReturn { + long orderId; + UUID playerUUID; + String playerName; + int totalPaid; + long createTime; + String packColor; + String packType; + List items; + + PendingReturn(OrderRecord r) { + this.orderId = r.orderId; + this.playerUUID = r.uuid; + this.playerName = r.player; + this.totalPaid = r.totalPaid; + this.createTime = System.currentTimeMillis(); + this.packColor = r.packColor; + this.packType = r.packType; + this.items = new ArrayList<>(r.items); + } + } + + + public static class OrderItem { + public String name; public String mat; + public int originalPrice; public int finalPrice; public int qty; + public OrderItem(String n,String m,int op,int fp,int q){ + name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;} + public int subtotal(){return finalPrice*qty;} + public int originalSubtotal(){return originalPrice*qty;} + } + public static class OrderRecord { + public long orderId; public UUID uuid; public String player; + public List items; public int totalOriginal; public int totalPaid; + public int discount; public String discountType; public String couponCode; + public int packFee; public String packType; public String packColor; + public long timestamp; public long printTime; public int status; + public int refundedAmount; public String refundType; + } + + public OrderManager(Main plugin) { + this.plugin = plugin; + loadReceiptConfig(); + loadOrders(); + startCleanupTask(); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + public List getOrders() { return orders; } + + // ===== 小票模板 ===== + private void loadReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + if (!f.exists()) createDefaultReceiptConfig(f); + Map m = loadMap(f); + tplHeader = m.getOrDefault("头部", tplHeader); + tplItemLine = m.getOrDefault("商品行", tplItemLine); + tplDiscountLine = m.getOrDefault("优惠行", tplDiscountLine); + tplPackLine = m.getOrDefault("打包行", tplPackLine); + tplSummary = m.getOrDefault("汇总", tplSummary); + tplFooter = m.getOrDefault("页脚", tplFooter); + } + private void saveReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=" + tplHeader); + L.add("商品行=" + tplItemLine); + L.add("优惠行=" + tplDiscountLine); + L.add("打包行=" + tplPackLine); + L.add("汇总=" + tplSummary); + L.add("页脚=" + tplFooter); + writeLines(f, L); + } + private void createDefaultReceiptConfig(File f) { + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}"); + L.add("商品行=§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚"); + L.add("优惠行=优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})"); + L.add("打包行=打包: §c+{打包费}枚§r ({打包颜色})"); + L.add("汇总=原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚"); + L.add("页脚=感谢惠顾,欢迎再来!"); + writeLines(f, L); + } + + // ===== 订单记录 ===== + public void recordOrder(Player p, List items, + int totalOriginal, int totalPaid, int discount, + String discountType, String couponCode, int packFee, + String packType, String packColor) { + OrderRecord r = new OrderRecord(); + r.orderId = System.currentTimeMillis(); + r.uuid = p.getUniqueId(); r.player = p.getName(); + r.items = items; r.totalOriginal = totalOriginal; + r.totalPaid = totalPaid; r.discount = discount; + r.discountType = discountType; r.couponCode = couponCode; + r.packFee = packFee; r.packType = packType; + r.packColor = packColor; r.timestamp = r.orderId; + r.printTime = 0; r.status = 0; r.refundedAmount = 0; + r.refundType = null; + orders.add(r); saveOrders(); + } + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + // ===== 小票聊天栏 ===== + /** + * 聊天栏打印小票(给定参数 Player + OrderRecord) + * 不要改动方法签名 + */ + public void sendReceiptChat(Player p, OrderRecord r) { + if (p == null || r == null) return; + + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + p.sendMessage("§7§m "); + p.sendMessage("§6§l======== 商店购物小票 ========"); + + // 退款标记 + String statusTag = getStatusTag(r); + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§7订单号: §f#" + r.orderId); + p.sendMessage("§7下单时间: §f" + + df.format(new Date(r.timestamp))); + p.sendMessage("§7玩家: §f" + r.player); + p.sendMessage("§7------------------------------"); + + for (OrderItem it : r.items) { + String price; + if (it.originalPrice != it.finalPrice) { + price = "§f" + it.originalPrice + + "§7→§6" + it.finalPrice; + } else { + price = "§6" + it.finalPrice; + } + p.sendMessage("§7 §e" + it.name + + " §7x" + it.qty + + " " + price + "枚" + + " §6小计" + it.subtotal() + "枚"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7原价: §f" + r.totalOriginal + "枚"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "§7(优惠券 -" + pct + "%)" + : "eco".equals(r.discountType) + ? "§7(环保 -" + pct + "%)" + : "§7(-" + pct + "%)"; + p.sendMessage("§7优惠: §a-" + r.discount + + "枚 " + dt); + } + + if (r.packFee > 0) { + String cn = r.packColor != null + ? r.packColor + .replace("_SHULKER_BOX", "") + .replace("SHULKER_BOX", "原色") + : "默认"; + p.sendMessage("§7打包: §c+" + r.packFee + + "枚 §7(" + cn + "潜影盒)"); + } else { + p.sendMessage("§7包装: 直接放入背包"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7实付: §6" + r.totalPaid + "枚"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + p.sendMessage("§7余额: §a" + bal + "枚"); + + // 退款标记 + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§6§l=============================="); + r.printTime = System.currentTimeMillis(); + } + + /** + * 生成书本小票内容 + */ + private String buildReceiptText(OrderRecord r) { + String statusTag = getStatusTag(r); + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + StringBuilder sb = new StringBuilder(); + + sb.append("§6§l===== 商店小票 =====\n"); + + // 退款标记 + String tag = getStatusTag(r); + if (!tag.isEmpty()) sb.append(tag).append("\n"); + + sb.append("订单号: #").append(r.orderId).append("\n"); + sb.append("时间: ").append( + df.format(new Date(r.timestamp))).append("\n"); + sb.append("玩家: ").append(r.player).append("\n"); + sb.append("------------------------------\n"); + + for (OrderItem it : r.items) { + String pr = (it.originalPrice != it.finalPrice) + ? it.originalPrice + "->" + it.finalPrice + : String.valueOf(it.finalPrice); + sb.append(it.name).append(" x").append(it.qty) + .append(" ").append(pr).append("枚") + .append(" 小计").append(it.subtotal()) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("原价: ").append(r.totalOriginal) + .append("枚\n"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "优惠券(-" + pct + "%)" + : "环保(-" + pct + "%)"; + sb.append("优惠: -").append(r.discount) + .append("枚 ").append(dt).append("\n"); + } + + if (r.packFee > 0) { + sb.append("打包: +").append(r.packFee) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("实付: ").append(r.totalPaid).append("枚\n"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + sb.append("余额: ").append(bal).append("枚\n"); + + // 退款标记 + if (!statusTag.isEmpty()) sb.append("\n").append(tag).append("\n"); + + return sb.toString(); + } + + /** + * 退款状态标记文本 + */ + private String getStatusTag(OrderRecord r) { + switch (r.status) { + case 2: + return "§c§l<<<<<<< 已全额退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 3: + return "§c§l<<<<<<< 已部分退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 4: + return "§c§l<<<<<<< 退款申请已拒绝 >>>>>>>"; + default: + return ""; + } + } + + /** + * 创建小票书 + */ + public ItemStack createReceiptBook(OrderRecord r) { + String text = buildReceiptText(r); + String[] lines = text.split("\n"); + + ItemStack book = new ItemStack(Material.WRITTEN_BOOK); + BookMeta bm = (BookMeta) book.getItemMeta(); + bm.setTitle("购物小票 #" + r.orderId); + bm.setAuthor("商店"); + + StringBuilder page = new StringBuilder(); + int lineCount = 0; + for (String line : lines) { + // ★ 保留颜色代码 + page.append(line).append("\n"); + lineCount++; + if (lineCount >= 13) { + bm.addPage(page.toString().trim()); + page = new StringBuilder(); + lineCount = 0; + } + } + if (page.length() > 0) { + bm.addPage(page.toString().trim()); + } + + book.setItemMeta(bm); + return book; + } + + // ===== 退款工具 ===== + private boolean withinSelfRefundWindow(OrderRecord r) { return System.currentTimeMillis() - r.timestamp < SELF_REFUND_WINDOW; } + private int countDirectItems(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private int countShulkers(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private void removeItems(Player p, String matName, int amt) { + Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt)); + } + private boolean removeOneShulker(Player p, + String materialName) { + // 先尝试精确匹配 + Material mat = Material.matchMaterial(materialName); + if (mat != null) { + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + return true; + } + } + } + // 匹配不到,移除任意一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .endsWith("_SHULKER_BOX")) { + p.getInventory().clear(i); + return true; + } + } + return false; + } + public String validateRefund(OrderRecord r, Player p) { + if (r.status != 0) + return "订单状态: " + getStatusName(r.status); + + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // ★ 检查:潜影盒是否被放置过 + if (shulkerUsedOrders.contains(r.orderId)) { + return "§c潜影盒已被使用过,无法自助退款"; + } + + // ★ 检查:背包里是否有同一订单的潜影盒 + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im == null || im.getLore() == null) + continue; + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + return null; // 找到了,可退 + } + } + } + return "§c对应的潜影盒已不在背包中"; + } else { + // ★ 散装:检测物品数量 + for (OrderItem it : r.items) { + int have = countDirectItems(p, it.mat); + if (have < it.qty) + return "§c" + it.name + "不足(有" + + have + "/" + it.qty + ")"; + } + } + return null; + } + + + + private void notifyPlayer(UUID uuid, String msg) { Player p = Bukkit.getPlayer(uuid); if (p != null && p.isOnline()) p.sendMessage(msg); } + private void notifyAdmins(Player p, OrderRecord r) { + String m = "§e[订单] " + p.getName() + " 申请退款 #" + r.orderId + " §6" + r.totalPaid + "枚"; + for (Player a : Bukkit.getOnlinePlayers()) if (isAdmin(a) && !a.equals(p)) a.sendMessage(m); + } + private String getStatusName(int s) { switch(s){case 0:return "正常";case 1:return "申请中";case 2:return "已退";case 3:return "部分退";case 4:return "已拒绝";default:return "未知";} } + private boolean isAdmin(Player p) { + if (p.isOp()) return true; + try { ConfigManager cm = plugin.getConfigMgr(); if (cm == null) return false; return p.hasPermission("sdf1.admin") || p.hasPermission("sdf1." + cm.adminTag); } catch (Exception e) { return p.hasPermission("sdf1.admin"); } + } + + // ===== 订单中心 ===== + public void openOrderCenter(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l订单中心"); + g.setItem(13, mkItem(Material.BOOK, "§e§l购买记录", "§7查看历史订单", "", "§a点击进入")); + if (isAdmin(p)) { g.setItem(31, mkItem(Material.NETHER_STAR, "§c§l管理员面板", "§7在线玩家购物记录", "§7支持退款操作", "", "§a点击进入")); + g.setItem(33, mkItem(Material.CRAFTING_TABLE, "§e§l小票配置", "§7配置小票模板样式", "", "§a点击进入")); } + g.setItem(49, mkItem(Material.ARROW, "§7返回商店")); + p.openInventory(g); + } + + // ===== 购买记录 ===== + public void openMyOrders(Player p, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l购买记录"); + List list = getPlayerAllOrders(p.getUniqueId()); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + userViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon; String st; String act; + if (r.status != 0) { icon = r.status == 1 ? Material.PAPER : Material.BARRIER; st = "§7状态: " + getStatusName(r.status); act = "§b左键: 补打小票"; } + else if (withinSelfRefundWindow(r)) { icon = Material.EMERALD; st = "§7状态: §a可退款(5分钟内)"; act = "§b左键: 补打小票\n§a右键: 退款退货\n§eShift+右键: 仅退款"; } + else { icon = Material.BOOK; st = "§7状态: §7已过自助退款期"; act = "§b左键: 补打小票\n§e右键: 申请退款退货\n§6Shift+右键: 申请仅退款"; } + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7原价: §f" + r.totalOriginal + "枚"); + if (r.discount > 0) lore.add("§7优惠: §a-" + r.discount + "枚"); + lore.add("§7实付: §6" + r.totalPaid + "枚"); lore.add(st); lore.add(""); + for (String line : act.split("\n")) lore.add(line); + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7暂无订单记录")); + addPageButtons(g, page, total); + p.openInventory(g); + } + + // ===== 管理员面板 ===== + public void openAdminPanel(Player p, int page) { + if (!isAdmin(p)) { p.sendMessage("§c权限不足"); return; } + Inventory g = Bukkit.createInventory(null, 54, "§c§l管理员订单面板"); + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int total = Math.max(1, (int) Math.ceil((double) online.length / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); adminViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, online.length); + // ★ 替换 openAdminPanel 中的物品创建部分 + for (int i = start; i < end; i++) { + int si = i - start; + if (si >= SLOTS.length) break; + Player t = online[i]; + List tl = getPlayerOrdersWithin( + t.getUniqueId(), RETENTION_30D); + int ts = 0; + for (OrderRecord r : tl) ts += r.totalPaid; + + ItemStack is = new ItemStack(Material.PLAYER_HEAD); + org.bukkit.inventory.meta.SkullMeta sm = + (org.bukkit.inventory.meta.SkullMeta) + is.getItemMeta(); + if (sm != null) { + sm.setOwner(t.getName()); // ★ 设置头颅皮肤 + sm.setDisplayName("§e" + t.getName()); + sm.setLore(Arrays.asList( + "§7订单: §f" + tl.size() + "笔", + "§7消费: §6" + ts + "枚", + "", "§a点击进入")); + is.setItemMeta(sm); // ★ 这行之前缺失了 + } + g.setItem(SLOTS[si], is); + } + + if (online.length == 0) g.setItem(31, mkItem(Material.BARRIER, "§7无在线玩家")); + addPageButtons(g, page, total); p.openInventory(g); + } + + // ===== 管理员订单 ===== + public void openAdminOrders(Player p, UUID targetUUID, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§c§l订单管理"); + String name = Bukkit.getOfflinePlayer(targetUUID).getName(); + List list = getPlayerOrdersWithin(targetUUID, RETENTION_30D); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + adminViewPage.put(p.getUniqueId(), page); adminViewTarget.put(p.getUniqueId(), targetUUID); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon = r.status == 2 || r.status == 4 ? Material.BARRIER : r.status == 1 ? Material.PAPER : Material.BOOK; + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7实付: §6" + r.totalPaid + "枚"); + if (r.status == 1) { lore.add("§7状态: §e申请中(" + nn(r.refundType) + ")"); lore.add(""); lore.add("§a右键: 批准"); lore.add("§c左键: 拒绝"); } + else if (r.status == 2) lore.add("§7已退: §a" + r.refundedAmount + "枚"); + else if (r.status == 3) lore.add("§7部分退: §a" + r.refundedAmount + "枚"); + else if (r.status == 4) lore.add("§7已拒绝"); + else { lore.add(""); lore.add("§a右键: 整单退款"); lore.add("§e左键: 部分退款(输入%)"); } + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7该玩家暂无订单")); + addPageButtons(g, page, total); + g.setItem(49, mkItem(org.bukkit.Material.PAPER, name + " §e" + (page + 1) + "/" + total)); + p.openInventory(g); + } + private void addPageButtons(Inventory g, int page, int total) { + g.setItem(48, page > 0 ? mkItem(Material.ARROW, "§7上一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + if (g.getItem(49) == null) g.setItem(49, mkItem(Material.PAPER, "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 ? mkItem(Material.ARROW, "§7下一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + } + + // ===== 小票配置 ===== + public void openReceiptConfig(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l小票配置"); + g.setItem(10, mkItem(Material.PAPER, "§e§l头部", "§7" + brief(tplHeader), "", "§a点击编辑")); + g.setItem(11, mkItem(Material.PAPER, "§e§l商品行", "§7" + brief(tplItemLine), "", "§a点击编辑")); + g.setItem(12, mkItem(Material.PAPER, "§e§l优惠行", "§7" + brief(tplDiscountLine), "", "§a点击编辑")); + g.setItem(14, mkItem(Material.PAPER, "§e§l打包行", "§7" + brief(tplPackLine), "", "§a点击编辑")); + g.setItem(15, mkItem(Material.PAPER, "§e§l汇总", "§7" + brief(tplSummary), "", "§a点击编辑")); + g.setItem(16, mkItem(Material.PAPER, "§e§l页脚", "§7" + brief(tplFooter), "", "§a点击编辑")); + g.setItem(22, mkItem(Material.BOOK, "§6§l预览(随机价格)", "§7预览小票效果")); + g.setItem(48, mkItem(Material.LIME_WOOL, "§a§l保存")); g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(50, mkItem(Material.RED_WOOL, "§c§l重置")); + p.openInventory(g); + } + public void openReceiptBuilder(Player p, String section) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l编辑: " + sectionName(section)); + g.setItem(10, mkItem(Material.EMERALD, "§a{品名}")); g.setItem(11, mkItem(Material.EMERALD, "§a{原价}")); + g.setItem(12, mkItem(Material.EMERALD, "§a{执行价}")); g.setItem(13, mkItem(Material.EMERALD, "§a{数量}")); + g.setItem(14, mkItem(Material.EMERALD, "§a{小计}")); g.setItem(15, mkItem(Material.EMERALD, "§a{商店名}")); + g.setItem(16, mkItem(Material.EMERALD, "§a{玩家}")); g.setItem(19, mkItem(Material.DIAMOND, "§b{订单号}")); + g.setItem(20, mkItem(Material.DIAMOND, "§b{订单时间}")); g.setItem(21, mkItem(Material.DIAMOND, "§b{打印时间}")); + g.setItem(22, mkItem(Material.DIAMOND, "§b{日期}")); g.setItem(23, mkItem(Material.DIAMOND, "§b{时间}")); + g.setItem(24, mkItem(Material.DIAMOND, "§b{原价合计}")); g.setItem(25, mkItem(Material.DIAMOND, "§b{实付}")); + g.setItem(28, mkItem(Material.GOLD_INGOT, "§6{优惠额}")); g.setItem(29, mkItem(Material.GOLD_INGOT, "§6{优惠类型}")); + g.setItem(30, mkItem(Material.GOLD_INGOT, "§6{优惠券码}")); g.setItem(31, mkItem(Material.GOLD_INGOT, "§6{打包费}")); + g.setItem(32, mkItem(Material.GOLD_INGOT, "§6{打包颜色}")); g.setItem(33, mkItem(Material.GOLD_INGOT, "§6{余额}")); + g.setItem(37, mkItem(Material.STONE, "§f═══")); g.setItem(38, mkItem(Material.STONE, "§f───")); + g.setItem(39, mkItem(Material.STONE, "§f═════════════")); g.setItem(40, mkItem(Material.PAPER, "§e换行(\\n)")); + g.setItem(47, mkItem(Material.RED_STAINED_GLASS_PANE, "§c删末尾")); g.setItem(48, mkItem(Material.RED_WOOL, "§c清空")); + g.setItem(49, mkItem(Material.ARROW, "§7返回")); g.setItem(50, mkItem(Material.LIME_WOOL, "§a保存")); + g.setItem(51, mkItem(Material.BLUE_WOOL, "§9重置")); + p.openInventory(g); + } + // ===== 点击处理 ===== + public boolean handleCenterClick(Player p, int raw) { + if (raw == 13) { openMyOrders(p, 0); return true; } + if (raw == 31 && isAdmin(p)) { openAdminPanel(p, 0); return true; } + if (raw == 33 && isAdmin(p)) { openReceiptConfig(p); return true; } + if (raw == 49) { plugin.getShopManager().openShopMain(p); return true; } + return true; + } + public boolean handleMyOrdersClick(Player p, int raw, boolean left, boolean shift) { + List list = getPlayerAllOrders(p.getUniqueId()); + int page = userViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openMyOrders(p, page - 1); return true; } + if (raw == 50) { openMyOrders(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (left) { + try { ItemStack book = createReceiptBook(r); + HashMap ov = p.getInventory().addItem(book); + for (ItemStack d : ov.values()) p.getWorld().dropItemNaturally(p.getLocation(), d); + r.printTime = System.currentTimeMillis(); p.sendMessage("§a已补打小票书: #" + r.orderId); + } catch (Exception e) { p.sendMessage("§c补打失败"); } + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // ★ Shift+右键:仅退款申请(需管理员审批) + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请,等待管理员审批"); + notifyAdmins(p, r); + } else { + // ★ 普通右键:5分钟内直接退(自动通过) + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason + + ",Shift+右键可申请仅退款"); + } else { + // 扣货 + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + if (isShulker) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory() + .getItem(i); + if (is != null && is.getType() + .name().contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + // 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + } + + + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // 仅退款申请 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } else { + // 5分钟内退货退款 + String reason = validateRefund(r, p); + if (reason != null) { + // ★ 不满足条件:询问是否申请 + p.sendMessage("§c" + reason); + askReturnConfirm(p, r); + } else { + // 满足条件:自动退 + doAutoReturn(p, r); + } + } + } else { + if (shift) { + // 申请仅退款 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请"); + notifyAdmins(p, r); + } else { + // ★ 申请退货退款:先检查条件 + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason); + } + // ★ 无论是否满足条件都询问 + askReturnConfirm(p, r); + } + } + } + } + if (left) { + // 已退款订单仍然允许补打小票,但小票内会显示退款标记 + try { + ItemStack book = createReceiptBook(r); + HashMap overflow = + p.getInventory().addItem(book); + for (ItemStack drop : overflow.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + r.printTime = System.currentTimeMillis(); + if (r.status != 0) { + p.sendMessage("§e已补打小票(含退款标记): #" + + r.orderId); + } else { + p.sendMessage("§a已补打小票书: #" + + r.orderId); + } + } catch (Exception e) { + p.sendMessage("§c补打失败"); + } + } + + final int fp = page; Bukkit.getScheduler().runTask(plugin, () -> openMyOrders(p, fp)); + return true; + } + public boolean handleAdminClick(Player p, int raw) { + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openAdminPanel(p, page - 1); return true; } + if (raw == 50) { openAdminPanel(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= online.length) return true; + openAdminOrders(p, online[actual].getUniqueId(), 0); return true; + } + public boolean handleAdminOrdersClick(Player p, int raw, boolean left, boolean shift) { + UUID tu = adminViewTarget.get(p.getUniqueId()); + if (tu == null) return false; + List list = getPlayerOrdersWithin(tu, RETENTION_30D); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { + openAdminOrders(p, tu, page - 1); + return true; + } + if (raw == 50) { + openAdminOrders(p, tu, page + 1); + return true; + } + if (raw == 53) { + openAdminPanel(p, 0); + return true; + } + int si = slotIndex(raw); + if (si < 0) return true; + int actual = si + page * PAGE_SIZE; + if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (r.status == 2 || r.status == 3 || r.status == 4) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + return true; + } + if (left) { + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款被拒绝"); + openAdminOrders(p, tu, page); + } else if (r.status == 0) { + partialRefundListening.add(p.getUniqueId()); + partialRefundTarget.put(p.getUniqueId(), r); + p.closeInventory(); + p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚"); + } + } else { + if (r.status == 0 || r.status == 1) { + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + } + } + // ★ 替换 handleAdminOrdersClick 中的审批逻辑 + if (left) { + // 左键 = 拒绝 + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款申请已被拒绝"); + openAdminOrders(p, tu, page); + } + } else { + // 右键 = 批准 + if (r.status == 1) { + String rt = r.refundType != null + ? r.refundType : ""; + + // ★ 退款退货申请 → 检查潜影盒 + if (rt.equals("refund_return_apply")) { + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 检查玩家背包里有没有带订单标签的盒子 + Player target = Bukkit.getPlayer(r.uuid); + if (target == null || !target.isOnline()) { + p.sendMessage("§c玩家不在线,无法执行退货退款"); + openAdminOrders(p, tu, page); + return true; + } + + boolean foundTagged = false; + boolean foundAny = false; + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is == null) continue; + if (!is.getType().name() + .contains("SHULKER")) continue; + foundAny = true; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + foundTagged = true; + // 收走盒子 + target.getInventory().clear(i); + break; + } + } + } + if (foundTagged) break; + } + + if (foundTagged) { + // ★ 有带标记的盒子 → 收走 + 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + + r.player + " §6" + + r.totalPaid + "枚(盒子已收走)"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + return true; + } else if (foundAny) { + // ★ 有盒子但没标记 → 等管理员决定 + pendingReturns.put(r.orderId, + new PendingReturn(r)); + p.closeInventory(); + p.sendMessage("§e§l[退货审批] 订单 #" + + r.orderId); + p.sendMessage("§7玩家背包有潜影盒但" + + "无订单标记,可能已被替换"); + p.sendMessage("§a输入 §e1 §a= 收走盒子并退款"); + p.sendMessage("§e输入 §e2 §e= 仅退款(不收盒子)"); + p.sendMessage("§c输入 §c3 §c= 拒绝退款"); + p.sendMessage("§72分钟内未操作自动拒绝"); + // 启动2分钟超时 + startReturnTimeout(r.orderId); + return true; + } else { + // ★ 背包里没有任何盒子 + p.sendMessage("§c玩家背包里没有潜影盒"); + p.sendMessage("§7可拒绝并通知玩家" + + "申请仅退款或把货拿回来"); + openAdminOrders(p, tu, page); + return true; + } + } + // ★ 散装退货退款:扣货+退钱 + Player target = Bukkit.getPlayer(r.uuid); + if (target != null && target.isOnline()) { + for (OrderItem it : r.items) + removeItems(target, it.mat, it.qty); + } + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + return true; + } + + // ★ 仅退款申请 / 5分钟内自动退款 + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已批准退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退款已批准,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + } + } + return true; + } + + /** + * 询问用户是否提交退货退款申请 + */ + private void askReturnConfirm(Player p, OrderRecord r) { + returnConfirmWaiting.put(p.getUniqueId(), r.orderId); + p.sendMessage(""); + p.sendMessage("§e§l[退货退款申请]"); + p.sendMessage("§7订单 #" + r.orderId + + " 实付" + r.totalPaid + "枚"); + p.sendMessage("§a输入 §e1 §a= 提交退货退款申请"); + p.sendMessage("§c输入 §e2 §c= 取消"); + p.sendMessage("§730秒内未操作自动取消"); + p.sendMessage(""); + + // 30秒超时 + final UUID uuid = p.getUniqueId(); + final long oid = r.orderId; + new BukkitRunnable() { + @Override + public void run() { + Long waiting = returnConfirmWaiting.get(uuid); + if (waiting != null && waiting == oid) { + returnConfirmWaiting.remove(uuid); + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§7退货退款申请已超时取消"); + } + } + } + }.runTaskLater(plugin, RETURN_CONFIRM_TIMEOUT); + } + + /** + * 执行5分钟内自动退货退款 + */ + private void doAutoReturn(Player p, OrderRecord r) { + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 找并收走带标签的盒子 + boolean found = false; + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + p.getInventory().clear(i); + found = true; + break; + } + } + } + if (found) break; + } + // 没找到标签盒子,收任意潜影盒 + if (!found) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + + + // ===== 2分钟超时 ===== + private void startReturnTimeout(long orderId) { + new BukkitRunnable() { + @Override + public void run() { + PendingReturn pr = pendingReturns + .remove(orderId); + if (pr == null) return; // 已处理 + Player admin = Bukkit.getPlayer(pr.playerUUID); + if (admin != null && admin.isOnline()) { + admin.sendMessage("§c[退货审批] 订单 #" + + orderId + " 超时,自动拒绝"); + } + // 通知玩家 + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款申请已超时拒绝"); + // 恢复订单状态 + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 0; + r.refundType = null; + saveOrders(); + } + } + }.runTaskLater(plugin, 2400L); // 2分钟 + } + + // ===== 查找订单 ===== + private OrderRecord findOrder(long orderId) { + synchronized (orders) { + for (OrderRecord r : orders) { + if (r.orderId == orderId) return r; + } + } + return null; + } + + + public boolean handleReceiptConfigClick(Player p, int raw) { + if (raw == 49) { openOrderCenter(p); return true; } + if (raw == 48) { saveReceiptConfig(); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 50) { createDefaultReceiptConfig(new File(plugin.getDataFolder(), "小票设置.txt")); loadReceiptConfig(); p.sendMessage("§e已重置"); openReceiptConfig(p); return true; } + if (raw == 22) { sendRandomPreview(p); return true; } + String sec = null; + if (raw == 10) sec = "header"; else if (raw == 11) sec = "itemLine"; else if (raw == 12) sec = "discountLine"; + else if (raw == 14) sec = "packLine"; else if (raw == 15) sec = "summary"; else if (raw == 16) sec = "footer"; + if (sec != null) { receiptBuilderSection.put(p.getUniqueId(), sec); openReceiptBuilder(p, sec); } + return true; + } + public boolean handleReceiptBuilderClick(Player p, int raw) { + String sec = receiptBuilderSection.get(p.getUniqueId()); if (sec == null) return false; + String token = getTokenBySlot(raw); + if (token != null) { String cur = getSection(sec); setSection(sec, cur.isEmpty() ? token : cur + token); openReceiptBuilder(p, sec); return true; } + if (raw == 47) { String c = getSection(sec); int lb = c.lastIndexOf('}'); if (lb >= 0) { int ls = c.lastIndexOf('{', lb); if (ls >= 0) setSection(sec, c.substring(0, ls)); } else setSection(sec, c.substring(0, Math.max(0, c.length() - 8))); openReceiptBuilder(p, sec); return true; } + if (raw == 48) { setSection(sec, ""); openReceiptBuilder(p, sec); return true; } + if (raw == 49) { receiptBuilderSection.remove(p.getUniqueId()); openReceiptConfig(p); return true; } + if (raw == 50) { saveReceiptConfig(); receiptBuilderSection.remove(p.getUniqueId()); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 51) { setSection(sec, getDefaultVal(sec)); openReceiptBuilder(p, sec); return true; } + return true; + } + + // ===== 聊天事件 ===== + @EventHandler + public void onAdminChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); UUID uuid = p.getUniqueId(); + // ★ 在 onAdminChat 方法开头添加 +// ===== 退货审批输入 ===== + for (Map.Entry entry : + new HashMap<>(pendingReturns).entrySet()) { + Long pendingOrderId = entry.getKey(); + PendingReturn pr = entry.getValue(); + if (pr.playerUUID.equals(p.getUniqueId()) + || isAdmin(p)) { + try { + int choice = Integer.parseInt( + e.getMessage().trim()); + pendingReturns.remove(pendingOrderId); + OrderRecord r = findOrder(pendingOrderId); + if (r == null) { + p.sendMessage("§c订单不存在"); + return; + } + + if (choice == 1) { + // ★ 收走盒子并退款 + Player target = Bukkit.getPlayer( + pr.playerUUID); + if (target != null && target.isOnline()) { + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + pendingOrderId)) { + target.getInventory().clear(i); + break; + } + } + } + break; + } + } + } + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(盒子已收走)"); + notifyPlayer(pr.playerUUID, + "§a[订单] 退货退款成功,§6" + + pr.totalPaid + "枚已退还"); + + } else if (choice == 2) { + // ★ 仅退款(不收盒子) + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§e已仅退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(未收盒子)"); + notifyPlayer(pr.playerUUID, + "§e[订单] 仅退款成功,§6" + + pr.totalPaid + "枚已退还" + + ",盒子无需退回"); + + } else if (choice == 3) { + // ★ 拒绝 + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款 #" + pendingOrderId); + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款已被拒绝" + + ",您可以申请仅退款" + + "或把货拿回来后重新申请"); + } else { + p.sendMessage("§c请输入 1、2 或 3"); + pendingReturns.put(pendingOrderId, pr); + } + return; + + } catch (NumberFormatException ignored) { + // 不是数字,跳过 + } + } + } +// ★ 在 onAdminChat 方法开头,处理用户退货确认 + if (returnConfirmWaiting.containsKey(uuid)) { + String msg = e.getMessage().trim(); + if (msg.equals("1")) { + // ★ 确认提交 + Long orderId = returnConfirmWaiting.remove(uuid); + if (orderId != null) { + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 1; + r.refundType = "refund_return_apply"; + saveOrders(); + p.sendMessage("§e已提交退货退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } + } + e.setCancelled(true); + return; + } else if (msg.equals("2")) { + // ★ 取消 + returnConfirmWaiting.remove(uuid); + p.sendMessage("§7已取消退货退款申请"); + e.setCancelled(true); + return; + } else { + // 输入其他内容:取消等待 + returnConfirmWaiting.remove(uuid); + // 不 return,让消息正常发送 + } + } + + if (partialRefundListening.contains(uuid)) { + e.setCancelled(true); partialRefundListening.remove(uuid); + OrderRecord t = partialRefundTarget.remove(uuid); if (t == null) return; + try { int pct = Integer.parseInt(e.getMessage().trim()); if (pct < 1 || pct > 100) { p.sendMessage("§c1-100"); return; } + int ref = t.totalPaid * pct / 100; if (ref <= 0) ref = 1; + plugin.getBonds().addBonds(t.player, ref); t.status = 3; t.refundedAmount = ref; saveOrders(); + p.sendMessage("§a部分退款 §e" + t.player + " §6" + ref + "枚(" + pct + "%)"); + notifyPlayer(t.uuid, "§a退款 §6" + ref + "枚"); + final UUID tu = adminViewTarget.get(uuid); final int pg = adminViewPage.getOrDefault(uuid, 0); + Bukkit.getScheduler().runTask(plugin, () -> openAdminOrders(p, tu, pg)); + } catch (NumberFormatException ex) { p.sendMessage("§c请输入数字"); } catch (Exception ex) { plugin.getLogger().log(Level.WARNING, "[Order]退款异常", ex); } + return; + } + if (receiptBuilderSection.containsKey(uuid)) { + e.setCancelled(true); String sec = receiptBuilderSection.remove(uuid); if (sec == null) return; + String msg = e.getMessage().trim(); + if ("cancel".equalsIgnoreCase(msg)) { Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); return; } + setSection(sec, msg.replace("\\n", "\n")); p.sendMessage("§a已更新 §e" + sec); + Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); + } + } + + // ===== 数据查询 ===== + private List getPlayerAllOrders(UUID uuid) { + List r = new ArrayList<>(); + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && System.currentTimeMillis() - o.timestamp < RETENTION_30D) r.add(o); } + Collections.reverse(r); return r; + } + private List getPlayerOrdersWithin(UUID uuid, long ms) { + List r = new ArrayList<>(); long cut = System.currentTimeMillis() - ms; + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && o.timestamp >= cut) r.add(o); } + Collections.reverse(r); return r; + } + + // ===== 持久化 ===== + private void loadOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); if (!f.exists()) return; + try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)); + OrderRecord cur = null; String line; + while ((line = br.readLine()) != null) { line = line.trim(); + if (line.equals("==")) { cur = new OrderRecord(); cur.items = new ArrayList<>(); } + else if (cur != null && line.contains("=")) { int eq = line.indexOf('='); String k = line.substring(0, eq); String v = line.substring(eq + 1); + switch (k) { case "id": cur.orderId = Long.parseLong(v); cur.timestamp = cur.orderId; break; case "uuid": cur.uuid = UUID.fromString(v); break; + case "player": cur.player = v; break; case "totalOriginal": cur.totalOriginal = Integer.parseInt(v); break; case "totalPaid": cur.totalPaid = Integer.parseInt(v); break; + case "discount": cur.discount = Integer.parseInt(v); break; case "discountType": cur.discountType = v; break; case "couponCode": cur.couponCode = v; break; + case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break; + case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break; + case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break; + case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; } + } else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; } + } if (cur != null && cur.uuid != null) orders.add(cur); br.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } + } + private void saveOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); + try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); + synchronized (orders) { for (OrderRecord r : orders) { pw.println("=="); + pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player); + pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); + pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); + pw.println("couponCode=" + nn(r.couponCode)); pw.println("packFee=" + r.packFee); + pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor)); + pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount); + pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType)); + for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty); + } } pw.flush(); pw.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); } + } + private void startCleanupTask() { new BukkitRunnable() { public void run() { synchronized (orders) { if (orders.removeIf(OrderManager.this::isExpired)) saveOrders(); } } }.runTaskTimer(plugin, 36000L, 36000L); } + /** + * 将小票书塞入潜影盒 + */ + public void addBookToShulker(ItemStack shulker, ItemStack book) { + try { + org.bukkit.inventory.meta.BlockStateMeta bm = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (bm == null) return; + org.bukkit.block.BlockState st = bm.getBlockState(); + if (st instanceof org.bukkit.block.Container) { + ((org.bukkit.block.Container) st) + .getInventory().addItem(book); + bm.setBlockState(st); + shulker.setItemMeta(bm); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Order] 塞书失败: " + e.getMessage()); + } + } + + // ===== 工具 ===== + private String nn(String s) { return s != null ? s : ""; } + private boolean isExpired(OrderRecord r) { return System.currentTimeMillis() - r.timestamp > RETENTION_30D; } + private String fmtDate(long ts) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts)); } + private int slotIndex(int raw) { for (int i = 0; i < SLOTS.length; i++) if (SLOTS[i] == raw) return i; return -1; } + private String brief(String s) { if (s == null || s.isEmpty()) return "(空)"; String c = sc(s); return c.length() > 30 ? c.substring(0, 30) + "..." : c; } + private String sectionName(String s) { switch(s){case "header":return "头部";case "itemLine":return "商品行";case "discountLine":return "优惠行";case "packLine":return "打包行";case "summary":return "汇总";case "footer":return "页脚";default:return s;} } + private String sc(String s) { + if (s == null) return ""; + return s.replaceAll("§[0-9a-fk-orA-FK-OR]", ""); + } + private String getSection(String s) { switch(s){case "header":return tplHeader;case "itemLine":return tplItemLine;case "discountLine":return tplDiscountLine;case "packLine":return tplPackLine;case "summary":return tplSummary;case "footer":return tplFooter;default:return "";} } + private void setSection(String s, String v) { switch(s){case "header":tplHeader=v;break;case "itemLine":tplItemLine=v;break;case "discountLine":tplDiscountLine=v;break;case "packLine":tplPackLine=v;break;case "summary":tplSummary=v;break;case "footer":tplFooter=v;break;} } + private String getDefaultVal(String s) { switch(s){case "header":return "§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}";case "itemLine":return "§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚";case "discountLine":return "优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})";case "packLine":return "打包: §c+{打包费}枚§r ({打包颜色})";case "summary":return "原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚";case "footer":return "感谢惠顾,欢迎再来!";default:return "";} } + private String getTokenBySlot(int raw) { switch(raw){case 10:return "{品名}";case 11:return "{原价}";case 12:return "{执行价}";case 13:return "{数量}";case 14:return "{小计}";case 15:return "{商店名}";case 16:return "{玩家}";case 19:return "{订单号}";case 20:return "{订单时间}";case 21:return "{打印时间}";case 22:return "{日期}";case 23:return "{时间}";case 24:return "{原价合计}";case 25:return "{实付}";case 28:return "{优惠额}";case 29:return "{优惠类型}";case 30:return "{优惠券码}";case 31:return "{打包费}";case 32:return "{打包颜色}";case 33:return "{余额}";case 37:return "═══";case 38:return "───";case 39:return "═════════════";case 40:return "\\n";default:return null;} } + private ItemStack mkItem(Material mat, String name, String... lore) { ItemStack it = new ItemStack(mat); ItemMeta im = it.getItemMeta(); if (im != null) { im.setDisplayName(name); if (lore.length > 0) im.setLore(Arrays.asList(lore)); it.setItemMeta(im); } return it; } + private Map loadMap(File f) { Map m = new LinkedHashMap<>(); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), 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) m.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); } br.close(); } catch (Exception ignored) {} return m; } + private void writeLines(File f, List lines) { try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); for (String l : lines) pw.println(l); pw.flush(); pw.close(); } catch (Exception ignored) {} } + private void sendRandomPreview(Player p) { String[] n = {"钻石剑","钻石镐","铁剑","金苹果"}; String[] m = {"DIAMOND_SWORD","DIAMOND_PICKAXE","IRON_SWORD","GOLDEN_APPLE"}; Random r = new Random(); List items = new ArrayList<>(); for (int i = 0; i < 2 + r.nextInt(3); i++) { int idx = r.nextInt(n.length); items.add(new OrderItem(n[idx], m[idx], 100 + r.nextInt(5000), 100 + r.nextInt(5000), 1 + r.nextInt(10))); } OrderRecord o = new OrderRecord(); o.orderId = System.currentTimeMillis(); o.uuid = p.getUniqueId(); o.player = p.getName(); o.items = items; o.totalOriginal = 0; for (OrderItem it : items) o.totalOriginal += it.originalSubtotal(); o.totalPaid = o.totalOriginal; o.timestamp = o.orderId; o.status = 0; p.sendMessage("§6§l===== 小票预览(随机) ====="); sendReceiptChat(p, o); p.sendMessage("§6§l===== 预览结束 ====="); } +} diff --git a/backup/src/main/java/Sdf1_login/PVPManager.java b/backup/src/main/java/Sdf1_login/PVPManager.java new file mode 100644 index 0000000..326c0fb --- /dev/null +++ b/backup/src/main/java/Sdf1_login/PVPManager.java @@ -0,0 +1,1380 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scoreboard.*; + + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PVPManager implements Listener { + + private final Main plugin; + private Connection pvpDb; + + private final Map + regions = new ConcurrentHashMap<>(); + private final Map + inRegion = new ConcurrentHashMap<>(); + private final Map + sessionKills = + new ConcurrentHashMap<>(); + private final Map + lastKillTime = + new ConcurrentHashMap<>(); + private final Map + selectState = + new ConcurrentHashMap<>(); + private final Map + killSessions = + new ConcurrentHashMap<>(); + private final Set tripleAnnounced = + ConcurrentHashMap.newKeySet(); + private final Map lastScoreboardRefresh = new ConcurrentHashMap<>(); + + + // 边框显示 + private final Set + permanentBorders = + ConcurrentHashMap.newKeySet(); + private final Set + tempBorders = + ConcurrentHashMap.newKeySet(); + + + public static class KillSession { + public int count = 0; + public long firstKill = 0; + public long lastKill = 0; + public boolean tripleAnnounced = + false; + public String region = ""; + } + + public static class PVPRegion { + public String name; + public String world; + public int x1, y1, z1; + public int x2, y2, z2; + public int minX, minY, minZ; + public int maxX, maxY, maxZ; + + public void calcBounds() { + minX = Math.min(x1, x2); + minY = Math.min(y1, y2); + minZ = Math.min(z1, z2); + maxX = Math.max(x1, x2); + maxY = Math.max(y1, y2); + maxZ = Math.max(z1, z2); + } + + public boolean contains( + Location loc) { + if (loc == null) return false; + if (!loc.getWorld().getName() + .equals(world)) + return false; + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + } + + public PVPManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadRegions(); + } + + // ===== 独立数据库 ===== + + private void initDB() { + try { + Class.forName( + "org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), + "pvp.db"); + pvpDb = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = + pvpDb.createStatement(); + st.execute( + "PRAGMA journal_mode=WAL"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "name TEXT NOT NULL " + + "UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER " + + "DEFAULT 0)"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT " + + "NOT NULL," + + "region_name TEXT " + + "NOT NULL," + + "kills INTEGER " + + "DEFAULT 0," + + "deaths INTEGER " + + "DEFAULT 0," + + "PRIMARY KEY" + + "(player_name, " + + "region_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[PVP] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== DB 操作 ===== + + private void saveRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions" + + " (name, world," + + " x1,y1,z1," + + " x2,y2,z2," + + " create_time)" + + " VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private Map + getRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + private List> + getAllRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + pvpDb.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + private void deleteRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private int getKills(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("kills"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private int getDeaths(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("deaths"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private void addKill(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,1,0) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET kills=" + + "kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private void addDeath(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,0,1) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET deaths=" + + "deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private List> + getTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT player_name," + + " kills,deaths" + + " FROM " + + "pvp_kills " + + "WHERE " + + "region_name=?" + + " AND kills>0" + + " ORDER BY kills" + + " DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = + ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString( + "player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Set getRegionNames() { + return regions.keySet(); + } + + // ===== 加载 ===== + + private void loadRegions() { + regions.clear(); + List> list = + getAllRegions(); + for (Map row : list) { + PVPRegion r = new PVPRegion(); + r.name = (String) row + .get("name"); + r.world = (String) row + .get("world"); + r.x1 = ((Number) row + .get("x1")).intValue(); + r.y1 = ((Number) row + .get("y1")).intValue(); + r.z1 = ((Number) row + .get("z1")).intValue(); + r.x2 = ((Number) row + .get("x2")).intValue(); + r.y2 = ((Number) row + .get("y2")).intValue(); + r.z2 = ((Number) row + .get("z2")).intValue(); + r.calcBounds(); + regions.put(r.name, r); + } + } + + // ===== 工具 ===== + + public ItemStack createTool() { + ItemStack tool = new ItemStack( + Material.BLAZE_ROD); + ItemMeta m = tool.getItemMeta(); + m.setDisplayName( + "§6§l[PVP] 区域选择工具"); + m.setLore(Arrays.asList( + "§7右键:设置第一个角", + "§7左键:设置第二个角")); + m.getPersistentDataContainer() + .set(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING, + "tool"); + tool.setItemMeta(m); + return tool; + } + + private boolean isPVPTool( + ItemStack item) { + if (item == null) return false; + ItemMeta m = item.getItemMeta(); + if (m == null) return false; + return m.getPersistentDataContainer() + .has(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING); + } + + public void giveTool(Player p) { + boolean has = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (isPVPTool(it)) { + has = true; + break; + } + } + if (!has) { + p.getInventory() + .addItem(createTool()); + } + } + + // ===== 边框粒子 ===== + + private void drawBorder(Player target, + PVPRegion r) { + if (r == null) return; + World w = Bukkit.getWorld(r.world); + if (w == null) return; + int step = 2; + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.maxX, r.minY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.minY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.minX, r.minY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.minY, r.minZ, step); + drawLine(target, w, + r.minX, r.maxY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.minZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.maxY, r.maxZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + } + + private void drawLine(Player target, + World w, + int x1, int y1, int z1, + int x2, int y2, int z2, + int step) { + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double dist = Math.sqrt( + dx * dx + dy * dy + dz * dz); + int steps = (int) (dist / step); + if (steps < 1) steps = 1; + for (int i = 0; i <= steps; i++) { + double t = (double) i / steps; + Location loc = new Location(w, + x1 + dx * t, + y1 + dy * t + 0.1, + z1 + dz * t); + w.spawnParticle( + Particle.FLAME, loc, 1, + 0.0, 0.0, 0.0, 0.0); + } + } + + // ===== 每秒 tick ===== + + public void tickRegions() { + long now = + System.currentTimeMillis(); + + // 玩家区域检测 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pn = p.getName(); + Location loc = p.getLocation(); + String inside = null; + for (PVPRegion r : + regions.values()) { + if (r.contains(loc)) { + inside = r.name; + break; + } + } + String was = inRegion.get(pn); + + if (inside != null && !inside.equals(was)) { + inRegion.put(pn, inside); + if (was == null) { + String reg = inside; + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> + updateScoreboard( + pp, reg), + 1L); + } + + } else if (inside == null + && was != null) { + inRegion.remove(pn); + // 不删 sessionKills,等倒计时结束再清 + removeScoreboard(p); + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> plugin + .giveMenuSnowball(pp), + 5L); + } + + } + + // 5分钟超时重置连杀session + for (String pname : + new ArrayList<>( + killSessions + .keySet())) { + KillSession ks = + killSessions.get(pname); + if (ks == null) continue; + if (now - ks.lastKill + > 5 * 60 * 1000) { + killSessions.remove(pname); + } + } + + // 5分钟无击杀 → 计数榜切总榜 + // 5分钟无击杀 → 归档并清零 + for (String rName : + new ArrayList<>( + lastKillTime + .keySet())) { + long last = lastKillTime + .getOrDefault(rName, 0L); + if (now - last > 5 * 60 * 1000) { + // 清零该区域所有sessionKills + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + rName + ":")); + // 切换到总榜 + refreshScoreboard(rName); + lastKillTime.remove(rName); + lastScoreboardRefresh.remove(rName); + + } + } + + + // 边框渲染 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (pr == null) continue; + PVPRegion r = regions.get(pr); + if (r == null) continue; + if (permanentBorders + .contains(pr)) { + drawBorder(p, r); + } + if (tempBorders + .contains(pr)) { + drawBorder(p, r); + } + // 每秒刷新活跃区域的计分板倒计时 + for (String rName : new ArrayList<>(lastKillTime.keySet())) { + long last = lastScoreboardRefresh.getOrDefault(rName, 0L); + if (now - last >= 1000) { + lastScoreboardRefresh.put(rName, now); + for (Player tp : Bukkit.getOnlinePlayers()) { + String inR = inRegion.get(tp.getName()); + if (rName.equals(inR)) { + showCounter(tp, rName); + } + } + } + } + + + + } + } + + private void broadcastWithSound(String msg, Sound sound) { + Bukkit.broadcastMessage(msg); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), sound, 1.0f, 1.0f); + } + } + + + private void initSession(String player, + String region) { + killSessions.remove(player); + tripleAnnounced.remove( + region + ":" + player); + KillSession s = new KillSession(); + s.region = region; + s.firstKill = + System.currentTimeMillis(); + s.lastKill = + System.currentTimeMillis(); + killSessions.put(player, s); + } + + // ===== 击杀事件 ===== + + @EventHandler + public void onDeath( + EntityDeathEvent event) { + if (!(event.getEntity() + instanceof Player)) return; + Player victim = + (Player) event.getEntity(); + Player killer = + victim.getKiller(); + if (killer == null) return; + if (killer.equals(victim)) return; + + String region = + inRegion.get( + victim.getName()); + if (region == null) return; + + String vName = victim.getName(); + String kName = killer.getName(); + + // 总榜 + addKill(kName, region); + addDeath(vName, region); + + // 计数榜 + String kKey = + region + ":" + kName; + sessionKills.merge(kKey, 1, + Integer::sum); + lastKillTime.put(region, + System.currentTimeMillis()); + + // 终结检测 + // 终结检测 + KillSession victimSession = + killSessions.get(vName); + if (victimSession != null + && victimSession.count >= 3) { + String endMsg = + "§b§l" + kName + + " §e终结了 " + + vName + " §e的 " + + victimSession.count + + "连杀!§a恭喜终结者 " + + kName + + "§e!§7快来挑战!"; + killSessions.remove(vName); + broadcastWithSound(endMsg, Sound.ENTITY_ENDER_DRAGON_GROWL); + + killSessions.remove(vName); + } + + + // 击杀者连杀 + KillSession ks = + killSessions.get(kName); + if (ks == null + || !ks.region.equals(region) + || System.currentTimeMillis() + - ks.lastKill > 5 * 60 * 1000) { + tripleAnnounced.remove( + region + ":" + kName); + KillSession ns = + new KillSession(); + ns.region = region; + ns.count = 1; + ns.firstKill = + System.currentTimeMillis(); + ns.lastKill = + System.currentTimeMillis(); + killSessions.put(kName, ns); + } else { + ks.count++; + ks.lastKill = + System.currentTimeMillis(); + } + + // 多杀通报 + KillSession killerSession = + killSessions.get(kName); + if (killerSession != null) { + plugin.getLogger().info( + "[PVP] checkMultiKill: " + + kName + " count=" + + killerSession.count + + " region=" + region); + checkMultiKill(killer, + region, killerSession); + } + + // 刷新计分板 + for (Player p : + Bukkit.getOnlinePlayers()) { + if (region.equals( + inRegion.get( + p.getName()))) { + updateScoreboard(p, region); + } + } + + victim.sendMessage("§c你在「" + + region + "」被 " + + kName + " 击杀"); + killer.sendMessage("§a你在「" + + region + "」击杀了 " + + vName); + } + + private void checkMultiKill( + Player killer, String region, + KillSession session) { + int count = session.count; + String kName = killer.getName(); + String key = region + ":" + kName; + String msg = null; + + if (count >= 5) { + msg = "§c§l" + kName + + " §e在「" + region + + "」达成 " + count + + "连杀!§7§l快来挑战!"; + } else if (count == 3) { + if (tripleAnnounced.add(key)) { + msg = "§6§l三杀!" + + kName + + " 势不可挡!" + + "§7快来挑战!"; + } + } + + if (msg != null) { + if (count >= 5) { + broadcastWithSound(msg, Sound.ENTITY_ENDER_DRAGON_GROWL); + } else { + broadcastWithSound(msg, Sound.BLOCK_END_PORTAL_SPAWN); + } + } + + } + + + + // ===== 计分板(英文ID) ===== + + private void ensureScoreboard( + Player p, String region) { + // 活跃击杀:每秒强制刷新(更新倒计时) + if (lastKillTime.containsKey(region)) { + showCounter(p, region); + return; + } + // 无活跃击杀:只在没有scoreboard时创建总榜 + Scoreboard sb = p.getScoreboard(); + if (sb == null + || sb.getObjective( + "sdf1pvp") == null) { + showTotal(p, region); + } + } + + + private void updateScoreboard( + Player p, String region) { + if (lastKillTime + .containsKey(region)) { + showCounter(p, region); + } else { + showTotal(p, region); + } + } + + private void showTotal(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = + mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§6§l击杀总榜 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List> top = + getTop(region, 10); + if (top.isEmpty()) { + obj.getScore("§7暂无记录") + .setScore(0); + } else { + int rank = 10; + for (Map row : + top) { + String name = (String) row + .get("player_name"); + int kills = ((Number) row + .get("kills")) + .intValue(); + int deaths = ((Number) row + .get("deaths")) + .intValue(); + String line = + "§e#" + (11 - rank) + + " §f" + name + + " §a" + kills + + "§7/§c" + deaths; + obj.getScore(line) + .setScore(rank); + rank--; + } + } + p.setScoreboard(sb); + } + private void showCounter(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§c§l实时战况 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List entries = + new ArrayList<>(); + for (Map.Entry e : + sessionKills.entrySet()) { + if (!e.getKey().startsWith( + region + ":")) + continue; + if (e.getValue() <= 0) continue; + String pName = e.getKey() + .substring( + region.length() + 1); + entries.add(new String[]{ + pName, + String.valueOf( + e.getValue())}); + } + + if (entries.isEmpty()) { + obj.getScore("§7等待击杀...") + .setScore(1); + } else { + entries.sort((a, b) -> + Integer.compare( + Integer.parseInt(b[1]), + Integer.parseInt(a[1]))); + int rank = 10; + for (String[] entry : entries) { + obj.getScore( + "§e" + entry[0] + + " §a" + entry[1]) + .setScore(rank); + rank--; + if (rank < 1) break; + } + } + + Long last = + lastKillTime.get(region); + if (last != null) { + long elapsed = + System.currentTimeMillis() + - last; + long remaining = + Math.max(0, + 5 * 60 * 1000 - elapsed); + long secs = remaining / 1000; + obj.getScore( + "§7冷却: " + secs + "s") + .setScore(-1); + } + + // 关键:强制赋值新scoreboard + p.setScoreboard(sb); + } + + private void removeScoreboard( + Player p) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + p.setScoreboard( + mgr.getNewScoreboard()); + } + + private void refreshScoreboard( + String regionName) { + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (regionName.equals(pr)) { + showTotal(p, regionName); + } + } + } + + // ===== 工具选择事件 ===== + + @EventHandler + public void onInteract( + PlayerInteractEvent event) { + Player p = event.getPlayer(); + ItemStack hand = + p.getInventory() + .getItemInMainHand(); + if (!isPVPTool(hand)) return; + + if (!event.getAction() + .isRightClick() + && !event.getAction() + .isLeftClick()) return; + + Location loc = p.getLocation(); + String[] state = + selectState.get(p.getName()); + if (state == null) { + state = new String[]{ + loc.getWorld() + .getName(), + "", ""}; + selectState.put( + p.getName(), state); + } + + String coord = + loc.getBlockX() + "," + + loc.getBlockY() + "," + + loc.getBlockZ(); + + if (event.getAction() + .isRightClick()) { + state[0] = loc.getWorld() + .getName(); + state[1] = coord; + p.sendMessage( + "§a第一个角: " + coord); + } else { + state[2] = coord; + p.sendMessage( + "§a第二个角: " + coord); + } + event.setCancelled(true); + } + + // ===== 退出清理 ===== + + @EventHandler + public void onQuit( + PlayerQuitEvent event) { + String name = + event.getPlayer().getName(); + inRegion.remove(name); + selectState.remove(name); + } + + // ===== 指令 ===== + + public boolean onCommand(Player p, + String[] args) { + if (args.length == 0) { + return showStats(p, + p.getName()); + } + switch (args[0].toLowerCase()) { + case "create": + return handleCreate( + p, args); + case "stats": + if (args.length >= 2) + return showStats(p, + args[1]); + return showStats(p, + p.getName()); + case "list": + return handleList(p); + case "delete": + if (args.length >= 2) + return handleDelete(p, + args[1]); + p.sendMessage( + "§c用法: " + + "/pvp delete <名字>"); + return true; + case "tool": + giveTool(p); + p.sendMessage( + "§a已发放" + + "PVP区域选择工具"); + return true; + case "on": + return handleBorderOn( + p, args); + case "off": + return handleBorderOff( + p, args); + case "tempban": + return handleTempBan( + p, args); + default: + return showStats(p, + p.getName()); + } + } + + private boolean handleBorderOn( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp on <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + permanentBorders.add(name); + p.sendMessage("§a已开启「" + + name + "」永久边框显示"); + return true; + } + + private boolean handleBorderOff( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp off <区域名>"); + return true; + } + permanentBorders.remove(name); + tempBorders.remove(name); + p.sendMessage("§a已关闭「" + + name + "」边框显示"); + return true; + } + + private boolean handleTempBan( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp tempban <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + tempBorders.add(name); + p.sendMessage("§a「" + name + + "」临时边框显示15秒"); + final String rName = name; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> + tempBorders + .remove(rName), + 300L); + return true; + } + + private boolean handleCreate( + Player p, String[] args) { + if (args.length >= 8) { + try { + int x1 = Integer.parseInt( + args[1]); + int y1 = Integer.parseInt( + args[2]); + int z1 = Integer.parseInt( + args[3]); + int x2 = Integer.parseInt( + args[4]); + int y2 = Integer.parseInt( + args[5]); + int z2 = Integer.parseInt( + args[6]); + String rName = args[7]; + return createRegion(p, + rName, + p.getWorld() + .getName(), + x1, y1, z1, + x2, y2, z2); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c坐标格式错误"); + return true; + } + } + + if (args.length >= 2) { + String[] state = + selectState + .get(p.getName()); + if (state == null + || state[1].isEmpty() + || state[2].isEmpty()) { + p.sendMessage( + "§c请先用工具选择两个角"); + return true; + } + String[] p1 = + state[1].split(","); + String[] p2 = + state[2].split(","); + return createRegion(p, + args[1], state[0], + Integer.parseInt(p1[0]), + Integer.parseInt(p1[1]), + Integer.parseInt(p1[2]), + Integer.parseInt(p2[0]), + Integer.parseInt(p2[1]), + Integer.parseInt(p2[2])); + } + + p.sendMessage( + "§c用法: /pvp create " + + " " + + " <名字>"); + p.sendMessage( + "§c或: /pvp create " + + "<名字> (先用工具选择)"); + return true; + } + + private boolean createRegion( + Player p, String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + if (getRegion(name) != null) { + p.sendMessage( + "§c区域已存在: " + name); + return true; + } + PVPRegion r = new PVPRegion(); + r.name = name; + r.world = world; + r.x1 = x1; + r.y1 = y1; + r.z1 = z1; + r.x2 = x2; + r.y2 = y2; + r.z2 = z2; + r.calcBounds(); + saveRegion(name, world, + x1, y1, z1, + x2, y2, z2); + regions.put(name, r); + p.sendMessage( + "§a§lPVP区域创建成功!"); + p.sendMessage( + "§7名称: §f" + name); + p.sendMessage("§7范围: §f" + + r.minX + "," + r.minY + + "," + r.minZ + " → " + + r.maxX + "," + r.maxY + + "," + r.maxZ); + selectState.remove(p.getName()); + return true; + } + + private boolean handleList( + Player p) { + List> all = + getAllRegions(); + if (all.isEmpty()) { + p.sendMessage( + "§7暂无PVP区域"); + return true; + } + p.sendMessage( + "§6§lPVP区域列表:"); + for (Map row : + all) { + p.sendMessage("§e- §f" + + row.get("name") + + " §7(" + row.get("world") + + ")"); + } + return true; + } + + private boolean handleDelete( + Player p, String name) { + if (getRegion(name) == null) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + deleteRegion(name); + regions.remove(name); + permanentBorders.remove(name); + tempBorders.remove(name); + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + name + ":")); + lastKillTime.remove(name); + lastScoreboardRefresh.remove(name); + p.sendMessage( + "§a已删除: " + name); + return true; + } + + private boolean showStats(Player p, + String target) { + String region = + inRegion.get(target); + if (region == null) { + p.sendMessage("§6§l" + target + + " 的PVP战绩:"); + List> all = + getAllRegions(); + boolean has = false; + for (Map row : + all) { + String rn = (String) row + .get("name"); + int kills = + getKills(target, rn); + int deaths = + getDeaths(target, rn); + if (kills > 0 + || deaths > 0) { + p.sendMessage( + "§e- §f" + rn + + " §a击杀:" + kills + + " §c死亡:" + deaths); + has = true; + } + } + if (!has) + p.sendMessage( + "§7暂无记录"); + } else { + int kills = + getKills(target, region); + int deaths = + getDeaths(target, region); + double kd = deaths > 0 + ? (double) kills / deaths + : kills; + p.sendMessage("§6§l" + target + + " @ " + region); + p.sendMessage( + "§a击杀: " + kills + + " §c死亡: " + deaths + + " §eKD: " + + String.format( + "%.2f", kd)); + } + return true; + } +} diff --git a/backup/src/main/java/Sdf1_login/PasswordUtils.java b/backup/src/main/java/Sdf1_login/PasswordUtils.java new file mode 100644 index 0000000..4029ed4 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/PasswordUtils.java @@ -0,0 +1,72 @@ +package Sdf1_login; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtils { + + private static final SecureRandom RANDOM = + new SecureRandom(); + + public static String generateSalt() { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hash(String password, String salt) { + try { + MessageDigest md = + MessageDigest.getInstance("SHA-256"); + md.update(Base64.getDecoder().decode(salt)); + byte[] hash = + md.digest(password.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean validate(String password) { + if (password == null) return false; + if (password.length() < 6 || password.length() > 20) + return false; + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + return hasUpper && hasLower && hasDigit; + } + + public static String generateTempPassword() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(8); + for (int i = 0; i < 8; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateInviteCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateVerifyCode() { + int code = 100000 + RANDOM.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/backup/src/main/java/Sdf1_login/PlayerStorage.java b/backup/src/main/java/Sdf1_login/PlayerStorage.java new file mode 100644 index 0000000..f93caa3 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/PlayerStorage.java @@ -0,0 +1,130 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.logging.Logger; + +public class PlayerStorage { + private Connection conn; + private final Logger logger; + + public PlayerStorage(File dataFolder, + Logger logger) { + this.logger = logger; + File dbFile = new File( + dataFolder, + "quest_data.db"); + try { + Class.forName( + "org.sqlite.JDBC"); + conn = DriverManager + .getConnection( + "jdbc:sqlite:" + + dbFile + .getAbsolutePath()); + conn.setAutoCommit(true); + Statement s = + conn.createStatement(); + s.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_kv (" + + "player TEXT NOT NULL," + + "key TEXT NOT NULL," + + "value TEXT," + + "PRIMARY KEY" + + "(player, key))"); + s.close(); + logger.info( + "[QuestStorage] 初始化: " + + dbFile.getAbsolutePath()); + } catch (Exception e) { + logger.severe( + "[QuestStorage] 失败: " + + e.getMessage()); + } + } + + public Object getCondition( + String player, String key) { + if (conn == null) return null; + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value " + + "FROM quest_kv " + + "WHERE player=? " + + "AND key=?"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ResultSet rs = + ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") + : null; + rs.close(); + ps.close(); + return parseValue(val); + } catch (SQLException e) { + return null; + } + } + + public void setCondition( + String player, String key) { + setConditionRaw( + player, key, "true"); + } + public void debugPlayer(String player) { + + } + public void setConditionRaw( + String player, String key, + String value) { + if (conn == null) return; + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO quest_kv" + + "(player,key,value)" + + " VALUES(?,?,?)"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ps.setString(3, value); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + logger.warning( + "[QuestStorage] 写入失败: " + + key + "=" + value + + " | " + e.getMessage()); + } + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) + conn.close(); + } catch (Exception e) {} + } + + private Object parseValue(String v) { + if (v == null) return null; + try { + return Long.parseLong(v); + } catch (Exception e1) { + try { + return Double.parseDouble(v); + } catch (Exception e2) { + if ("true".equalsIgnoreCase(v)) + return true; + if ("false".equalsIgnoreCase(v)) + return false; + return v; + } + } + } +} diff --git a/backup/src/main/java/Sdf1_login/PointsManager.java b/backup/src/main/java/Sdf1_login/PointsManager.java new file mode 100644 index 0000000..77fc195 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/PointsManager.java @@ -0,0 +1,374 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +import java.lang.reflect.Method; +import java.util.*; + +public class PointsManager { + + private final Main plugin; + private Object cyInstance = null; + private Method cyActivationMethod = null; + private Method cyGetShopItemsMethod = null; + + private final List> cyItems = + new ArrayList<>(); + + public PointsManager(Main plugin) { + this.plugin = plugin; + discoverCY(); + } + + private void discoverCY() { + try { + org.bukkit.plugin.Plugin p = Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (p != null && p.isEnabled()) { + cyInstance = p; + cyGetShopItemsMethod = p.getClass() + .getMethod("onSdf1GetShopItems"); + cyActivationMethod = p.getClass() + .getMethod("onSdf1Activation", + String.class, + int.class, int.class); + plugin.getLogger().info( + "[Sdf1_login] CY_beibao 联控已连接"); + } + } catch (Exception ignored) { + } + } + + private void refreshCYItems() { + cyItems.clear(); + if (cyInstance == null + || cyGetShopItemsMethod == null) return; + try { + Object result = + cyGetShopItemsMethod.invoke(cyInstance); + if (result == null) return; + String data = result.toString(); + if (data.isEmpty()) return; + String[] items = data.split(";"); + for (String itemStr : items) { + String[] p = itemStr.split("\\|"); + if (p.length < 8) continue; + Map item = + new LinkedHashMap<>(); + item.put("id", p[0]); + item.put("name", p[1]); + item.put("days", + Integer.parseInt(p[2])); + item.put("slots", + Integer.parseInt(p[3])); + item.put("price", + Integer.parseInt(p[4])); + item.put("stock", + Integer.parseInt(p[5])); + item.put("icon", p[6]); + item.put("lifetime", + "1".equals(p[7])); + cyItems.add(item); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 拉取CY商品失败: " + + e.getMessage()); + } + } + + private void activateCY(String name, + int slots, int days) { + try { + if (cyInstance == null + || cyActivationMethod == null) return; + cyActivationMethod.invoke( + cyInstance, name, slots, days); + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] CY激活失败: " + + e.getMessage()); + } + } + + public int getPoints(String name) { + return ((Number) plugin.getDb() + .getField(name, "points")).intValue(); + } + + public Inventory createShopGUI(Player p) { + int points = getPoints(p.getName()); + Inventory inv = Bukkit.createInventory( + null, 54, + "§d§l积分商城 | 当前积分: " + points); + fillBg(inv); + inv.setItem(10, mkItem(Material.EMERALD, + "§a游戏币充值", + "§7消耗积分兑换游戏币", + "§7100积分 = $1000", + "", + "§e点击兑换")); + inv.setItem(12, mkItem(Material.ENDER_CHEST, + "§b背包空间", + "§7消耗积分兑换背包格子", + "§750积分 = 9格永久空间", + "", + "§e点击兑换")); + inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, + "§e经验瓶", + "§7消耗积分兑换经验", + "§720积分 = 10级经验", + "", + "§e点击兑换")); + if (cyInstance != null) { + inv.setItem(31, mkItem(Material.CHEST, + "§6背包商城", + "§7来自CY_beibao的商品", + "", + "§e点击查看")); + } + inv.setItem(49, mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public Inventory createCYShopGUI(Player p) { + refreshCYItems(); + int points = getPoints(p.getName()); + int size = Math.max(54, + ((cyItems.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory inv = Bukkit.createInventory( + null, size, + "§d§lCY背包商城 | 当前积分: " + points); + fillBg(inv); + for (int i = 0; i < cyItems.size() + && i < 45; i++) { + Map item = cyItems.get(i); + String name = (String) item.get("name"); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + List lore = new ArrayList<>(); + lore.add("§7价格: §e" + price + " 积分"); + if (lifetime) { + lore.add("§7格子: §e终身 +" + slots); + } else { + lore.add("§7格子: §e+" + slots); + lore.add("§7时间: §e" + days + "天"); + } + if (stock == 0) { + lore.add("§7库存: §a无限"); + } else if (stock == -1) { + lore.add("§7库存: §c已下架"); + } else { + lore.add("§7库存: §e" + stock); + } + lore.add(""); + if (points >= price && stock != -1) { + lore.add("§e点击购买"); + } else if (stock == -1) { + lore.add("§c已下架"); + } else { + lore.add("§c积分不足"); + } + inv.setItem(i, mkItem(Material.CHEST, + "§a" + name, + lore.toArray(new String[0]))); + } + inv.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public void addItem(String name, + int price, String cmd2) { + // 追加到商品配置文件 + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.io.FileWriter fw = + new java.io.FileWriter(file, true); + fw.write(name + ":" + price + + ":" + cmd2 + "\n"); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void removeItem(int index) { + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.util.List lines = + new java.util.ArrayList<>(); + java.io.BufferedReader br = + new java.io.BufferedReader( + new java.io.FileReader( + file)); + String line; + int i = 0; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty() + || line.startsWith("#")) { + lines.add(line); + continue; + } + if (i != index) { + lines.add(line); + } + i++; + } + br.close(); + java.io.PrintWriter pw = + new java.io.PrintWriter(file); + for (String l : lines) pw.println(l); + pw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void handleClick(Player p, int slot) { + int points = getPoints(p.getName()); + switch (slot) { + case 10: + if (points >= 100) { + plugin.getDb().deductPoints( + p.getName(), 100); + if (plugin.getEconomy() != null) { + plugin.getEconomy() + .depositPlayer(p, 1000); + } + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "100")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 12: + if (points >= 50) { + plugin.getDb().deductPoints( + p.getName(), 50); + activateCY(p.getName(), 9, 0); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "50")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 14: + if (points >= 20) { + plugin.getDb().deductPoints( + p.getName(), 20); + p.setLevel(p.getLevel() + 10); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "20")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 31: + if (cyInstance != null) { + p.openInventory( + createCYShopGUI(p)); + } + break; + } + } + + public void handleCYClick(Player p, int slot) { + if (slot < 0 || slot >= cyItems.size() + || slot >= 45) return; + Map item = cyItems.get(slot); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + if (stock == -1) { + p.sendMessage("§c该商品已下架"); + return; + } + int points = getPoints(p.getName()); + if (points < price) { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + return; + } + + plugin.getDb().deductPoints(p.getName(), price); + int actDays = lifetime ? 0 : days; + activateCY(p.getName(), slots, actDays); + + String tl = lifetime + ? "终身" : (days + "天"); + p.sendMessage("§a购买成功!获得 " + + tl + " +" + slots + "格 背包空间"); + } + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src/main/java/Sdf1_login/QuestStorage.java b/backup/src/main/java/Sdf1_login/QuestStorage.java new file mode 100644 index 0000000..45dbabf --- /dev/null +++ b/backup/src/main/java/Sdf1_login/QuestStorage.java @@ -0,0 +1,138 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class QuestStorage { + + private Connection conn; + + public QuestStorage(File dataFolder) { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + dataFolder, + "quest_data.db"); + conn = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getPath()); + initTable(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void initTable() + throws SQLException { + Statement stmt = + conn.createStatement(); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY " + + "(player, condition_key))"); + stmt.close(); + } + + /** + * 标记条件已完成(value = "1") + */ + public void setCondition( + String player, + String condition) { + setConditionRaw( + player, condition, "1"); + } + + /** + * 写入任意值(计数器等) + */ + public void setConditionRaw( + String player, + String condition, + String value) { + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO " + + "quest_conditions " + + "(player, " + + "condition_key, " + + "value, timestamp) " + + "VALUES " + + "(?, ?, ?, ?)"); + ps.setString(1, player); + ps.setString(2, condition); + ps.setString(3, value); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 查询条件值 + */ + public Object getCondition( + String player, + String condition) { + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value FROM " + + "quest_conditions " + + "WHERE player = ? " + + "AND " + + "condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + String val = + rs.getString("value"); + rs.close(); + ps.close(); + // 尝试解析为数字 + try { + if (val.contains(".")) { + return Double.parseDouble( + val); + } + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backup/src/main/java/Sdf1_login/QuestTracker.java b/backup/src/main/java/Sdf1_login/QuestTracker.java new file mode 100644 index 0000000..228c1ac --- /dev/null +++ b/backup/src/main/java/Sdf1_login/QuestTracker.java @@ -0,0 +1,3252 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final QuestStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new QuestStorage( + plugin.getDataFolder()); + + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 岩岸/沙滩 + BIOME_CATEGORY.put("minecraft:stony_shore", + "石岸"); +// 海岸 + BIOME_CATEGORY.put("minecraft:mushroom_fields", + "蘑菇岛"); +// 苍白之园 + BIOME_CATEGORY.put( + "minecraft:pale_garden", "森林"); +// 末地小型岛屿 + BIOME_CATEGORY.put( + "minecraft:small_end_islands", "末地"); +// 主世界结构归类 + BIOME_CATEGORY.put("minecraft:village", "平原"); + BIOME_CATEGORY.put( + "minecraft:desert_pyramid", "沙漠"); + BIOME_CATEGORY.put( + "minecraft:jungle_pyramid", "森林"); + BIOME_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + + + } + + // 所有群系大类列表 + private static final String[] + ALL_CATEGORIES = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "石岸", "蘑菇岛", "地下", + "下界", "末地" + }; + + // ===== 结构映射 ===== + private static final Map + STRUCTURE_CATEGORY = + new HashMap<>(); + static { + // 主世界结构 + STRUCTURE_CATEGORY.put( + "minecraft:village", "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:desert_pyramid", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:jungle_pyramid", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + STRUCTURE_CATEGORY.put( + "minecraft:igloo", "雪原"); + STRUCTURE_CATEGORY.put( + "minecraft:mansion", "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:pillager_outpost", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:trail_ruins", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:trial_chambers", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:buried_treasure", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:fossil", "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "地下"); + // 下界结构 + STRUCTURE_CATEGORY.put( + "minecraft:nether_fortress", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:bastion_remnant", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:nether_fossil", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:basalt_pillars", + "下界"); + } + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + CATEGORY_KEYWORDS.put("石岸", "石岸"); + CATEGORY_KEYWORDS.put("沙滩", "石岸"); + CATEGORY_KEYWORDS.put("海岸", "石岸"); + + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 优先用世界名判断维度 + if (worldName != null) { + String w = + worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + + // 用群系映射表 + if (rawKey != null) { + String lower = + rawKey.toLowerCase(); + // 带 minecraft: 前缀的直接查 + String cat = + BIOME_CATEGORY.get(lower); + if (cat != null) + return cat; + + // 不带前缀的尝试补全 + String prefixed = + "minecraft:" + lower; + cat = BIOME_CATEGORY.get(prefixed); + if (cat != null) + return cat; + + // 模糊匹配群系名 + for (Map.Entry entry + : BIOME_CATEGORY.entrySet()) { + if (entry.getKey() + .contains(lower) + || lower.contains( + entry.getKey() + .replace( + "minecraft:", + ""))) { + return entry.getValue(); + } + } + } + + return "主世界"; + } + + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API或债券 + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage("§c无可领取的奖励"); + return; + } + + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) { + player.sendMessage( + "§7[可选奖励] 需完成所有可选条件: " + rt); + continue; + } + } + + // 按 + 分割子项 + String[] parts = rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } + } + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory().addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); + } + + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + public String getProgress( + String player, String rawCond) { + if (player == null || rawCond == null) + return "§7未知"; + String key = stripOptional(rawCond); + + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + + cur + "/1"; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "VILLAGER_TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + + } + + // ===== 击杀泛解析(支持中文列表) ===== +// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等 + if (key.contains("击杀") && key.contains("只")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + String mobPart = key; + java.util.regex.Matcher killM = + java.util.regex.Pattern + .compile("击杀\\d+只(.+)") + .matcher(key); + if (killM.find()) { + mobPart = killM.group(1).trim(); + } + + String[] mobNames = mobPart + .split("[、,,,]+"); + int totalKills = 0; + StringBuilder names = new StringBuilder(); + for (String name : mobNames) { + String trimmed = name.trim(); + if (trimmed.isEmpty()) continue; + String entityType = + chineseMobToEntity(trimmed); + if (!entityType.isEmpty()) { + totalKills += getCounter( + player, + "KILL:" + entityType); + if (names.length() > 0) + names.append("/"); + names.append(trimmed); + } + } + return "§7击杀" + names + ": §e" + + totalKills + "/" + needed; + } + +// 单一KILL:xxx:N 格式(原有逻辑保留) + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + int needed = parseInt(p[2]); + int cur = getCounter(player, + "KILL:" + p[1]); + return "§7击杀" + p[1] + ": §e" + + cur + "/" + needed; + } + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + // ===== 泛解析:击杀类进度 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < mobTypes.size(); i++) { + total += getCounter(player, + "KILL:" + mobTypes.get(i)); + if (i > 0) names.append("/"); + names.append(reverseMobName( + mobTypes.get(i))); + } + return "§7击杀" + names + ": §e" + + total + "/" + needed; + } + } + + // ===== 泛解析:村民交易进度 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + // ===== 泛解析:采集/挖矿进度 ===== + if (key.contains("采集") + || key.contains("挖")) { + int needed = + parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < matTypes.size(); i++) { + total += getCounter( + player, + "MINE:" + matTypes + .get(i)); + if (i > 0) + names.append("/"); + names.append( + translateMaterialName( + matTypes + .get(i))); + } + return "§7采集" + names + + ": §e" + total + + "/" + needed; + } + } + + // ===== 泛解析:合成进度 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "CRAFT"); + return "§7合成: §e" + cur + "/" + needed; + } + + // ===== 泛解析:钓鱼进度 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "FISH"); + return "§7钓鱼: §e" + cur + "/" + needed; + } + + return "§7" + key; + } + private String translateMaterialName( + String name) { + switch (name) { + case "COAL_ORE": + return "煤矿石"; + case "DEEPSLATE_COAL_ORE": + return "深层煤矿石"; + case "IRON_ORE": + return "铁矿石"; + case "DEEPSLATE_IRON_ORE": + return "深层铁矿石"; + case "COPPER_ORE": + return "铜矿石"; + case "DEEPSLATE_COPPER_ORE": + return "深层铜矿石"; + case "GOLD_ORE": + return "金矿石"; + case "DEEPSLATE_GOLD_ORE": + return "深层金矿石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "DEEPSLATE_DIAMOND_ORE": + return "深层钻石矿石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "DEEPSLATE_EMERALD_ORE": + return "深层绿宝石矿石"; + case "LAPIS_ORE": + return "青金石矿石"; + case "DEEPSLATE_LAPIS_ORE": + return "深层青金石矿石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "DEEPSLATE_REDSTONE_ORE": + return "深层红石矿石"; + case "NETHER_GOLD_ORE": + return "下界金矿石"; + case "NETHER_QUARTZ_ORE": + return "下界石英矿石"; + case "ANCIENT_DEBRIS": + return "远古残骸"; + case "COAL": + return "煤炭"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "RAW_COPPER": + return "粗铜"; + case "DIAMOND": + return "钻石"; + case "EMERALD": + return "绿宝石"; + case "LAPIS_LAZULI": + return "青金石"; + case "REDSTONE": + return "红石"; + case "QUARTZ": + return "石英"; + default: + return name; + } + } + + /** + * 中文物种名 → Bukkit EntityType 名 + * 支持击杀泛解析的模糊匹配 + */ + private String chineseMobToEntity(String cn) { + String n = cn.toLowerCase().trim(); + + // 直接别名表 + Map mobMap = new HashMap<>(); + mobMap.put("骷髅", "SKELETON"); + mobMap.put("僵尸", "ZOMBIE"); + mobMap.put("苦力怕", "CREEPER"); + mobMap.put("蜘蛛", "SPIDER"); + mobMap.put("洞穴蜘蛛", "CAVE_SPIDER"); + mobMap.put("小白", "SKELETON"); + mobMap.put("末影人", "ENDERMAN"); + mobMap.put("enderman", "ENDERMAN"); + mobMap.put("猪灵", "PIGLIN"); + mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE"); + mobMap.put("恶魂", "GHAST"); + mobMap.put("凋灵骷髅", "WITHER_SKELETON"); + mobMap.put("烈焰人", "BLAZE"); + mobMap.put("史莱姆", "SLIME"); + mobMap.put("岩浆怪", "MAGMA_CUBE"); + mobMap.put("女巫", "WITCH"); + mobMap.put("蠹虫", "SILVERFISH"); + mobMap.put("潜影贝", "SHULKER"); + mobMap.put("守卫者", "GUARDIAN"); + mobMap.put("远古守卫者", "ELDER_GUARDIAN"); + mobMap.put("溺尸", "DROWNED"); + mobMap.put("僵尸村民", "ZOMBIE_VILLAGER"); + mobMap.put("尸壳", "HUSK"); + mobMap.put("流浪者", "STRAY"); + mobMap.put("幻翼", "PHANTOM"); + mobMap.put("掠夺者", "PILLAGER"); + mobMap.put("卫道士", "VINDICATOR"); + mobMap.put("唤魔者", "EVOKER"); + mobMap.put("恼鬼", "VEX"); + mobMap.put("劫掠兽", "RAVAGER"); + mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY"); + mobMap.put("凋灵", "WITHER"); + mobMap.put("末影龙", "ENDER_DRAGON"); + + // 精确匹配 + if (mobMap.containsKey(n)) { + return mobMap.get(n); + } + + // 模糊匹配(包含关系) + for (Map.Entry en + : mobMap.entrySet()) { + if (n.contains(en.getKey()) + || en.getKey().contains(n)) { + return en.getValue(); + } + } + + // 尝试直接用英文名 + try { + return org.bukkit.entity.EntityType + .valueOf(n.toUpperCase() + .replace(" ", "_")) + .name(); + } catch (Exception e) { + return ""; + } + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + // 结构检测(覆盖纯生物群系分类) + // 地下检测:Y低于表面则归类为地下 +// 覆盖废弃矿井/试炼密室/要塞/远古城市等 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡") + || !condKey + .contains("群系")) + continue; + String target = + extractBiome(condKey); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + /* + * 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录 + */ + public String checkInAtPosition( + String player, String worldName, + double x, double y, double z) { + + List history = + loadCheckInPositions(player); + for (double[] pos : history) { + double dist = Math.sqrt( + Math.pow(x - pos[0], 2) + + Math.pow(y - pos[1], 2) + + Math.pow(z - pos[2], 2)); + if (dist <= 32) { + return "§c距离上次打卡点太近(" + + String.format( + "%.0f", dist) + + "格)"; + } + } + + // ===== 获取群系和分类 ===== + String biome = "minecraft:plains"; + String category = "平原"; + try { + org.bukkit.World w = + Bukkit.getWorld(worldName); + if (w != null) { + org.bukkit.Location loc = + new org.bukkit.Location( + w, + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + biome = loc.getBlock() + .getBiome().getKey() + .toString(); + category = mapBiomeCategory( + biome, worldName); + + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + + if ("下界".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.NETHER) { + category = "下界"; + } else if ("末地".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.THE_END) { + category = "末地"; + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 群系读取失败: " + + ex.getMessage()); + } + + plugin.getLogger().info( + "[QuestTracker] 打卡: " + + player + + " world=" + worldName + + " biome=" + biome + + " cat=" + category + + " pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + + saveCheckInPosition( + player, x, y, z); + recordCheckIn( + player, biome, category, + worldName, x, y, z); + List matched = + checkInBiome( + player, biome, + worldName, x, y, z); + + StringBuilder sb = new StringBuilder(); + sb.append("§a打卡成功!群系: §e") + .append(category); + if (!matched.isEmpty()) { + sb.append("\n§a完成任务条件:"); + for (String m : matched) { + String qn = + findQuestName(m); + sb.append("\n§7• "); + if (qn != null) { + sb.append("§e[") + .append(qn) + .append("§7] §f"); + } + sb.append(m); + } + } + return sb.toString(); + } + + + /** + * 根据条件文本查找所属任务名 + */ + private String findQuestName( + String condition) { + for (String cat : Arrays.asList( + "新人任务", + "主线任务", + "支线任务")) { + for (QuestFile qf : + getQuests(cat)) { + if (qf.conditions.contains( + condition)) { + return qf.displayName; + } + } + } + return null; + } + + + /** + * 加载玩家所有历史打卡坐标 + * 格式: x1,y1,z1;x2,y2,z2;... + */ + private List loadCheckInPositions( + String player) { + List list = new ArrayList<>(); + Object val = storage.getCondition( + player, "checkin_all_pos"); + if (val == null) return list; + String data = val.toString(); + if (data.isEmpty()) return list; + String[] records = data.split(";"); + for (String r : records) { + String[] parts = r.split(","); + if (parts.length >= 3) { + try { + list.add(new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2])}); + } catch (NumberFormatException e) { + // 跳过损坏数据 + } + } + } + return list; + } + + /** + * 保存打卡坐标(追加到已有记录) + */ + private void saveCheckInPosition( + String player, double x, + double y, double z) { + List list = + loadCheckInPositions(player); + list.add(new double[]{x, y, z}); + StringBuilder sb = new StringBuilder(); + for (double[] pos : list) { + if (sb.length() > 0) sb.append(";"); + sb.append(pos[0]).append(",") + .append(pos[1]).append(",") + .append(pos[2]); + } + storage.setConditionRaw( + player, + "checkin_all_pos", + sb.toString()); + } + // ========== 中文生物名 → EntityType 映射 ========== + private static final Map + MOB_NAME_MAP = new HashMap<>(); + static { + MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER"); + MOB_NAME_MAP.put("骷髅", "SKELETON"); + MOB_NAME_MAP.put("僵尸", "ZOMBIE"); + MOB_NAME_MAP.put("苦力怕", "CREEPER"); + MOB_NAME_MAP.put("蜘蛛", "SPIDER"); + MOB_NAME_MAP.put("末影人", "ENDERMAN"); + MOB_NAME_MAP.put("女巫", "WITCH"); + MOB_NAME_MAP.put("烈焰人", "BLAZE"); + MOB_NAME_MAP.put("僵尸猪灵", + "ZOMBIFIED_PIGLIN"); + MOB_NAME_MAP.put("猪灵", "PIGLIN"); + MOB_NAME_MAP.put("恶魂", "GHAST"); + MOB_NAME_MAP.put("凋灵骷髅", + "WITHER_SKELETON"); + MOB_NAME_MAP.put("史莱姆", "SLIME"); + MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE"); + MOB_NAME_MAP.put("溺尸", "DROWNED"); + MOB_NAME_MAP.put("尸壳", "HUSK"); + MOB_NAME_MAP.put("流髑", "STRAY"); + MOB_NAME_MAP.put("幻翼", "PHANTOM"); + MOB_NAME_MAP.put("守卫者", "GUARDIAN"); + MOB_NAME_MAP.put("远古守卫者", + "ELDER_GUARDIAN"); + MOB_NAME_MAP.put("掠夺者", "PILLAGER"); + MOB_NAME_MAP.put("卫道士", "VINDICATOR"); + MOB_NAME_MAP.put("唤魔者", "EVOKER"); + MOB_NAME_MAP.put("恼鬼", "VEX"); + MOB_NAME_MAP.put("劫掠兽", "RAVAGER"); + MOB_NAME_MAP.put("潜影贝", "SHULKER"); + MOB_NAME_MAP.put("末影螨", "ENDERMITE"); + MOB_NAME_MAP.put("凋灵", "WITHER"); + MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON"); + MOB_NAME_MAP.put("监守者", "WARDEN"); + MOB_NAME_MAP.put("嗅探兽", "SNIFFER"); + MOB_NAME_MAP.put("僵尸村民", + "ZOMBIE_VILLAGER"); + } + + /** + * 从条件文本中提取所有匹配的生物类型 + * 按名称长度降序匹配,避免短名误匹配 + */ + private List extractMobTypes( + String text) { + List types = new ArrayList<>(); + List> sorted = + new ArrayList<>( + MOB_NAME_MAP.entrySet()); + sorted.sort((a, b) -> + b.getKey().length() + - a.getKey().length()); + String remaining = text; + for (Map.Entry entry + : sorted) { + if (remaining.contains( + entry.getKey())) { + if (!types.contains( + entry.getValue())) { + types.add(entry.getValue()); + } + remaining = remaining.replace( + entry.getKey(), ""); + } + } + return types; + } + + /** + * EntityType → 中文名 + */ + private String reverseMobName( + String entityType) { + for (Map.Entry entry + : MOB_NAME_MAP.entrySet()) { + if (entry.getValue() + .equals(entityType)) { + return entry.getKey(); + } + } + return entityType; + } + + /** + * 从条件文本中提取矿物名 + * "采集任意矿物(煤炭除外)1组" + * → [COAL_ORE, IRON_ORE, GOLD_ORE, ...] + */ + private List extractMaterialTypes( + String text) { + List types = new ArrayList<>(); + // 排除列表 + List excludes = + new ArrayList<>(); + if (text.contains("煤炭除外")) { + excludes.add("COAL"); + excludes.add("COAL_ORE"); + excludes.add("DEEPSLATE_COAL_ORE"); + } + // 通配符:"矿物" "矿石" + if (text.contains("矿物") + || text.contains("矿石")) { + String[] ores = { + "COAL_ORE", + "DEEPSLATE_COAL_ORE", + "IRON_ORE", + "DEEPSLATE_IRON_ORE", + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE", + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE", + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE", + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE", + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE", + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE", + "NETHER_GOLD_ORE", + "NETHER_QUARTZ_ORE", + "ANCIENT_DEBRIS" + }; + for (String ore : ores) { + if (!excludes.contains(ore)) { + types.add(ore); + } + } + return types; + } + // 单个矿物名 + if (text.contains("煤炭")) + types.add("COAL"); + if (text.contains("铁矿") + || text.contains("铁锭")) + types.add("RAW_IRON"); + if (text.contains("金矿") + || text.contains("金锭")) + types.add("RAW_GOLD"); + if (text.contains("钻石")) + types.add("DIAMOND"); + if (text.contains("绿宝石")) + types.add("EMERALD"); + if (text.contains("青金石")) + types.add("LAPIS_LAZULI"); + if (text.contains("红石")) + types.add("REDSTONE"); + if (text.contains("铜")) + types.add("RAW_COPPER"); + if (text.contains("下界石英") + || text.contains("石英")) + types.add("QUARTZ"); + return types; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + */ + private String extractBiome(String text) { + if (text == null) return ""; + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点"))) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; + } + return getCounter( + player, "biome_ALL") >= 1; + } + + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "VILLAGER_TRADE") + >= needed; + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "REPORT_VILLAGE") + >= needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return streak >= needed; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + // ===== 泛解析:击杀类中文条件 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + for (String mobType : mobTypes) { + total += getCounter(player, + "KILL:" + mobType); + } + return total >= needed; + } + } + + // ===== 泛解析:村民交易条件 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "TRADE") + >= needed; + } + + // ===== 泛解析:采集/挖矿类条件 ===== + if (key.contains("采集") || key.contains("挖")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + for (String mat : matTypes) { + total += getCounter(player, + "MINE:" + mat); + } + return total >= needed; + } + } + + // ===== 泛解析:合成类条件 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "CRAFT") + >= needed; + } + + // ===== 泛解析:钓鱼类条件 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "FISH") + >= needed; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public void onVillagerTrade(String player) { + String key = "VILLAGER_TRADE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/backup/src/main/java/Sdf1_login/RadioDownloadListener.java b/backup/src/main/java/Sdf1_login/RadioDownloadListener.java new file mode 100644 index 0000000..d3e744f --- /dev/null +++ b/backup/src/main/java/Sdf1_login/RadioDownloadListener.java @@ -0,0 +1,144 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player + .PlayerResourcePackStatusEvent; + +import java.util.UUID; +import java.util.concurrent + .ConcurrentHashMap; + +public class RadioDownloadListener + implements Listener { + + private final Main plugin; + private final java.util.logging.Logger log; + + private final ConcurrentHashMap + failCount = new ConcurrentHashMap<>(); + private final ConcurrentHashMap + blocked = new ConcurrentHashMap<>(); + + private static final int MAX_FAILED = 3; + + public RadioDownloadListener(Main plugin) { + this.plugin = plugin; + this.log = plugin.getLogger(); + } + + public void onPlayerQuit(UUID uuid) { + failCount.remove(uuid); + blocked.remove(uuid); + } + + @EventHandler + public void onStatus( + PlayerResourcePackStatusEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + String status = + e.getStatus().name(); + + log.info("[Radio] === 资源包状态 ==="); + log.info("[Radio] 玩家: " + + p.getName()); + log.info("[Radio] UUID: " + uuid); + log.info("[Radio] 状态: " + + status); + log.info("[Radio] 原始状态枚举: " + + e.getStatus().getClass() + .getSimpleName() + + "." + + e.getStatus().name()); + + if (blocked.containsKey(uuid)) { + log.info("[Radio] 已熔断,跳过: " + + p.getName()); + return; + } + + switch (status) { + case "SUCCESS": + log.info("[Radio] " + + p.getName() + + " ✓ 资源包加载成功"); + failCount.remove(uuid); + blocked.remove(uuid); + break; + + case "ACCEPTED": + log.info("[Radio] " + + p.getName() + + " 客户端已接受," + + "等待下载..."); + break; + + case "DECLINED": + log.warning("[Radio] " + + p.getName() + + " ✗ 拒绝了资源包"); + log.warning("[Radio] " + + "可能原因: " + + "1.客户端设置禁用资源包" + + " 2.已缓存拒绝记录" + + " 3.SSL证书不匹配"); + handleFailure(p, uuid); + break; + + case "FAILED_DOWNLOAD": + log.warning("[Radio] " + + p.getName() + + " ✗ 下载失败"); + log.warning("[Radio] " + + "可能原因: " + + "1.URL无法访问" + + " 2.SSL证书错误" + + " 3.网络超时" + + " 4.文件不存在(404)"); + handleFailure(p, uuid); + break; + + default: + log.info("[Radio] " + + p.getName() + + " 未知状态: " + + status); + break; + } + } + + private void handleFailure(Player p, UUID uuid) { + int failNum = failCount.getOrDefault(uuid, 0) + 1; + failCount.put(uuid, failNum); + + log.warning("[Radio] " + p.getName() + + " 失败次数: " + failNum + "/" + MAX_FAILED); + + if (failNum >= MAX_FAILED) { + blocked.put(uuid, true); + log.severe("[Radio] " + p.getName() + + " 已达最大失败次数,停止重试," + + "玩家继续留在服务器"); + // 不踢出,只记录熔断,玩家正常游玩 + p.sendMessage("§e§l[Radio] 资源包加载失败" + + ",已取消下载资源包,部分画面音频可能出现问题" + + "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员"); + + return; + } +// 打了包没测试 + p.sendMessage("§e§l[Radio] 下载失败," + + "重试... (" + failNum + "/" + MAX_FAILED + ")"); + + // 5秒后重试 + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline() && plugin.radio != null) { + plugin.radio.sendResourcePack(p); + } + }, 100L); + } +} \ No newline at end of file diff --git a/backup/src/main/java/Sdf1_login/RadioManager.java b/backup/src/main/java/Sdf1_login/RadioManager.java new file mode 100644 index 0000000..6fe5eda --- /dev/null +++ b/backup/src/main/java/Sdf1_login/RadioManager.java @@ -0,0 +1,1040 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.zip.*; +import java.util.Collections; +import org.bukkit.NamespacedKey; +import java.util.Set; + + +public class RadioManager { + + private final Main plugin; + private File radioDir; + private File oggDir; + private File packDir; + private BukkitTask mainRadioTask; + private final Map loginPlayers = + new ConcurrentHashMap<>(); + private String externalUrl = ""; + private int httpPort = 443; + private final Set pendingPack = + Collections.newSetFromMap( + new ConcurrentHashMap<>()); + private String lastPlayedSound = ""; + + private int radioIndex = 0; + + // ========== 登录阶段推送(管理员手动触发) ========== + public void startLoginRadio(Player p) { + loginPlayers.put(p.getName(), true); + } + + public void stopLoginRadio(Player p) { + loginPlayers.remove(p.getName()); + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + } + + private final List mainFiles = new ArrayList<>(); + private final List loginFiles = new ArrayList<>(); + + public RadioManager(Main plugin) { + this.plugin = plugin; + this.radioDir = new File( + plugin.getDataFolder(), "radio"); + this.oggDir = new File( + plugin.getDataFolder(), "radio/ogg"); + this.packDir = new File( + plugin.getDataFolder(), "radiopack"); + radioDir.mkdirs(); + oggDir.mkdirs(); + packDir.mkdirs(); + loadRadioConfig(); + } + // ========== 读取 radio_config.txt ========== + private void loadRadioConfig() { + File cfg = new File( + plugin.getDataFolder(), + "radio_config.txt"); + + // 不存在则创建默认配置 + if (!cfg.exists()) { + createRadioConfig(cfg); + plugin.getLogger().info( + "[Radio] 已生成 radio_config.txt"); + } + + // 读取 + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(cfg), + java.nio.charset + .StandardCharsets + .UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("#") + || line.trim().isEmpty()) + continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq) + .trim(); + String val = line.substring(eq + 1) + .trim(); + if (key.equals("resource-pack-url")) { + externalUrl = val; + } else if (key.equals("http-port")) { + try { + httpPort = Integer.parseInt( + val); + } catch (NumberFormatException ignored) { + } + } + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 读取radio_config.txt失败: " + + e.getMessage()); + } + + // 443是HTTPS端口,内嵌HTTP不能用 + if (httpPort == 443) httpPort = 8080; + + plugin.getLogger().info( + "[Radio] 配置: externalUrl=[" + + externalUrl + "] httpPort=" + + httpPort); + } + + private void createRadioConfig(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + java.nio.charset + .StandardCharsets + .UTF_8))) { + pw.println("# ============ Sdf1 Radio Config ============"); + pw.println("#"); + pw.println("# resource-pack-url"); + pw.println("# 填外部地址(HTTP或HTTPS均可)"); + pw.println("# 例: https://example.com/radiopack.zip"); + pw.println("# 留空 = 使用内置HTTP服务"); + pw.println("#"); + pw.println("# http-port"); + pw.println("# 内置HTTP端口(仅resource-pack-url为空时生效)"); + pw.println("#"); + pw.println("# ============ Settings ==============="); + pw.println(); + pw.println("resource-pack-url="); + pw.println("http-port=8080"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 创建radio_config.txt失败: " + + e.getMessage()); + } + } + + // ========== 初始化 ========== + public void init() { + loadRadioConfig(); + convertNonOgg(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); // 启动HTTP + plugin.getLogger().info( + "[Radio] 初始化完成: 全服=" + + mainFiles.size() + + " 登录=" + loginFiles.size()); + } + + + // ========== 转换非ogg文件 ========== + private void convertNonOgg() { + convertDir(radioDir); + convertDir(new File(radioDir, "login")); + convertDir(new File(radioDir, "reg")); + } + + private void convertDir(File src) { + if (!src.exists() || !src.isDirectory()) return; + File[] files = src.listFiles((d, n) -> { + String l = n.toLowerCase(); + return l.endsWith(".mp3") + || l.endsWith(".wav") + || l.endsWith(".flac"); + }); + if (files == null) return; + + for (File f : files) { + String oggName = f.getName() + .replaceAll("\\.(mp3|wav|flac)$", + ".ogg"); + File oggFile = new File(src, oggName); + if (oggFile.exists() + && oggFile.lastModified() + >= f.lastModified()) { + continue; + } + // 调用ffmpeg转换 + if (convertWithFfmpeg(f, oggFile)) { + plugin.getLogger().info( + "[Radio] 转换成功: " + + f.getName()); + } else { + plugin.getLogger().warning( + "[Radio] 转换失败: " + + f.getName() + + " (需要安装ffmpeg)"); + } + } + } + + private boolean convertWithFfmpeg( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-c:a", "libvorbis", + "-q:a", "5", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean finished = + p.waitFor(30, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + return false; + } + } + + // ========== 加载ogg文件列表 ========== + private void loadAllOggs() { + mainFiles.clear(); + loginFiles.clear(); + loadOggsFlat(radioDir, mainFiles); + loadOggsFlat( + new File(radioDir, "login"), + loginFiles); + loadOggsFlat( + new File(radioDir, "reg"), + loginFiles); + } + + private void loadOggsFlat( + File dir, List list) { + if (!dir.exists() || !dir.isDirectory()) + return; + File[] files = dir.listFiles( + (d, n) -> n.toLowerCase() + .endsWith(".ogg")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) list.add(f); + } + /** + * 解析 pack.png:优先读取插件目录, + * 没有则搜索图片并用ffmpeg转码 + */ + private File resolvePackIcon() { + File dataDir = plugin.getDataFolder(); + File packPng = new File(dataDir, "pack.png"); + + if (packPng.exists() + && packPng.length() > 0) { + plugin.getLogger().info( + "[Radio] 找到 pack.png (" + + packPng.length() + + " bytes)"); + return packPng; + } + + // 搜索可转换的图片文件 + String[] exts = {"png", "jpg", "jpeg", + "bmp", "webp", "tiff", "gif"}; + File[] found = dataDir.listFiles( + (dir, name) -> { + String low = name.toLowerCase(); + for (String ext : exts) { + if (low.endsWith("." + ext) + && !name.equals( + "pack.png")) { + return true; + } + } + return false; + }); + + if (found != null && found.length > 0) { + File src = found[0]; + plugin.getLogger().info( + "[Radio] 找到图片: " + + src.getName() + + " → 转码为 pack.png"); + if (convertToPackPng(src, packPng)) { + return packPng; + } + plugin.getLogger().warning( + "[Radio] 转码失败"); + return null; + } + + plugin.getLogger().warning( + "[Radio] 未找到 pack.png 或可转换图片"); + return null; + } + + /** + * ffmpeg 将任意图片转为 256x256 PNG + */ + private boolean convertToPackPng( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-vf", + "scale='min(256,iw)'" + + ":'min(256,ih)':" + + "force_original_aspect_ratio" + + "=decrease," + + "pad=256:256:" + + "(ow-iw)/2:" + + "(oh-ih)/2:" + + "color=0x00000000", + "-c:v", "png", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean ok = p.waitFor( + 30, TimeUnit.SECONDS); + if (!ok) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] ffmpeg转码失败: " + + e.getMessage()); + return false; + } + } + + // ========== 资源包构建 ========== + private void buildResourcePack() { + deleteDir(packDir); + packDir.mkdirs(); + + List allOggs = new ArrayList<>(); + collectOggs(radioDir, allOggs); + + if (allOggs.isEmpty()) { + plugin.getLogger().warning( + "[Radio] radio/ 目录下" + + "无 .ogg 文件,跳过资源包"); + return; + } + + // 正确路径:assets/minecraft/sounds/custom/ + File soundsDir = new File(packDir, + "assets/minecraft/sounds/custom"); + soundsDir.mkdirs(); + + // 生成 sounds.json + StringBuilder json = + new StringBuilder("{\n"); + for (int i = 0; i < allOggs.size(); i++) { + File ogg = allOggs.get(i); + String simpleName = + ogg.getName() + .replace(".ogg", ""); + File dest = new File(soundsDir, + simpleName + ".ogg"); + try { + Files.copy(ogg.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception e) { + // ignore + } + // 音频事件名: custom.xxx + // 文件路径: custom/xxx + json.append(" \"custom.") + .append(simpleName) + .append("\": {\n"); + json.append(" \"sounds\": [{\n"); + json.append(" \"name\":" + + " \"custom/") + .append(simpleName) + .append("\",\n"); + json.append( + " \"stream\": true\n"); + json.append(" }]\n"); + json.append(" }"); + if (i < allOggs.size() - 1) + json.append(","); + json.append("\n"); + } + json.append("}"); + + // sounds.json 放在 assets/minecraft/ 下 + try { + Files.write(new File(packDir, + "assets/minecraft/sounds.json") + .toPath(), + json.toString() + .getBytes("UTF-8")); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] sounds.json写入失败"); + } +// sounds.json 写入后... + +// ===== 新增:打包 pack.png 作为资源包图标 ===== + File iconPng = resolvePackIcon(); + if (iconPng != null && iconPng.exists()) { + try { + Files.copy(iconPng.toPath(), + new File(packDir, "pack.png") + .toPath(), + StandardCopyOption + .REPLACE_EXISTING); + plugin.getLogger().info( + "[Radio] pack.png 已加入资源包"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] pack.png复制失败: " + + e.getMessage()); + } + } + + // pack.mcmeta(pack_format=54) + try { + Files.write(new File(packDir, + "pack.mcmeta").toPath(), + ("{\n \"pack\": {\n" + + " \"pack_format\": 54,\n" + + " \"description\":" + + " \"§d§l草原探险服务器资源包,请勿拒绝资源包\"\n }\n}") + .getBytes("UTF-8")); + } catch (Exception e) { + // ignore + } + + try { + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + packZip(packDir, zipFile); + plugin.getLogger().info( + "[Radio] 资源包生成: " + + allOggs.size() + " 个音频"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] zip打包失败: " + + e.getMessage()); + } + } + + private void deleteDir(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) deleteDir(f); + else f.delete(); + } + } + dir.delete(); + } + + private void collectOggs( + File dir, List list) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) + collectOggs(f, list); + else if (f.getName().endsWith(".ogg")) + list.add(f); + } + } + + private void packZip(File root, + File zipFile) throws Exception { + try (ZipOutputStream zos = + new ZipOutputStream( + new FileOutputStream( + zipFile))) { + zipDir(root, root, zos); + } + } + + private void zipDir(File root, + File current, + ZipOutputStream zos) + throws Exception { + File[] files = current.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + zipDir(root, f, zos); + } else { + String path = root.toURI() + .relativize(f.toURI()) + .getPath(); + zos.putNextEntry(new ZipEntry(path)); + zos.write(Files.readAllBytes( + f.toPath())); + zos.closeEntry(); + } + } + } + + // ========== 资源包发送 ========== + + /** + * 发送资源包(带限流,不禁重试) + */ + private final Map lastSendTime = + new ConcurrentHashMap<>(); + + public void sendResourcePack(Player p) { + UUID uuid = p.getUniqueId(); + + // 最短间隔 5秒(不是10秒的全锁死) + Long last = lastSendTime.get(uuid); + if (last != null + && System.currentTimeMillis() - last + < 5000L) { + plugin.getLogger().info( + "[Radio] 跳过(5秒内): " + + p.getName()); + return; + } + lastSendTime.put(uuid, + System.currentTimeMillis()); + + String url = getResourcePackUrl(); + if (url == null || url.isEmpty()) { + plugin.getLogger().warning( + "[Radio] URL为空,跳过: " + + p.getName()); + return; + } + + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) { + plugin.getLogger().warning( + "[Radio] radiopack.zip不存在," + + "跳过: " + p.getName()); + return; + } + + try { + byte[] data = + Files.readAllBytes(zip.toPath()); + String hash = sha1(data); + long fileSize = zip.length(); + + plugin.getLogger().info( + "[Radio] 发送: " + p.getName() + + " url=" + url + + " size=" + fileSize + + " hash=" + hash); + + sendPackWithHash(p, url, hash); + + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] 发送异常: " + + p.getName()); + e.printStackTrace(); + } + } + + + + private void sendPackWithHash( + Player p, + String url, + String hash) { + + UUID uuid = UUID.fromString( + "a1b2c3d4-e5f6-7890-abcd-" + + "ef1234567890"); + + // 尝试5参 + Component + try { + Object component = + net.kyori.adventure.text + .Component.text( + "§d§l请下载自定义资源包"); + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + net.kyori.adventure + .text.Component + .class, + boolean.class) + .invoke( + p, uuid, url, + hash, component, + true); + plugin.getLogger().info( + "[Radio] 成功(5参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试5参 + String描述 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + String.class, + boolean.class) + .invoke( + p, uuid, url, + hash, + "Sdf1 Radio", + true); + plugin.getLogger().info( + "[Radio] 成功(5参String): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试3参 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class) + .invoke( + p, uuid, url, + hash); + plugin.getLogger().info( + "[Radio] 成功(3参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // ★ 全部失败 → 通知玩家 + 熔断 + plugin.getLogger().severe( + "[Radio] 所有签名失败: " + + p.getName() + + " 客户端版本: " + + p.getProtocolVersion()); + p.sendMessage( + "§e§l[Radio] §f资源包发送失败," + + "§7部分功能可能异常"); + p.sendMessage( + "§7可尝试: §e/rejoin §7或重启客户端"); + } + + + + private void sendPackFromLocal( + Player p, String url) { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) return; + try { + byte[] data = + Files.readAllBytes( + zip.toPath()); + String hash = sha1(data); + sendPackWithHash(p, url, hash); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 降级发送失败"); + } + } + + // 从URL下载字节 + private byte[] downloadBytes(String urlStr) + throws Exception { + java.net.URL url = + new java.net.URL(urlStr); + java.net.HttpURLConnection conn = + (java.net.HttpURLConnection) + url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + try (java.io.InputStream is = + conn.getInputStream()) { + return is.readAllBytes(); + } + } + + // SHA-1计算 + private String sha1(byte[] data) + throws Exception { + java.security.MessageDigest md = + java.security.MessageDigest + .getInstance("SHA-1"); + md.update(data); + byte[] hash = md.digest(); + StringBuilder sb = + new StringBuilder(); + for (byte b : hash) + sb.append(String.format( + "%02x", b)); + return sb.toString(); + } + + public void clearPending(UUID uuid) { + lastSendTime.remove(uuid); + } + + + + // ========== 内嵌HTTP服务 ========== + private com.sun.net.httpserver.HttpServer + httpServer; + + private void startHttpServer() { + // 如果配置了外部地址则不启动 + if (plugin.getConfig2() != null + && !plugin.getConfig2() + .broadcastServerUrl + .isEmpty()) { + return; + } + try { + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + httpServer = + com.sun.net.httpserver + .HttpServer.create( + new java.net + .InetSocketAddress( + port), 0); + httpServer.createContext( + "/radiopack.zip", + exchange -> { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (zip.exists()) { + byte[] data = + Files.readAllBytes( + zip.toPath()); + exchange.getResponseHeaders() + .set("Content-Type", + "application/zip"); + exchange.getResponseHeaders() + .set("Access-Control" + + "-Allow-Origin", + "*"); + exchange.sendResponseHeaders( + 200, data.length); + exchange.getResponseBody() + .write(data); + exchange.getResponseBody() + .close(); + } else { + byte[] empty = + "not found".getBytes(); + exchange.sendResponseHeaders( + 404, empty.length); + exchange.getResponseBody() + .write(empty); + exchange.getResponseBody() + .close(); + } + }); + httpServer.setExecutor(null); + httpServer.start(); + plugin.getLogger().info( + "[Radio] HTTP服务: http://0.0.0.0:" + + port + "/radiopack.zip"); + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] HTTP启动失败: " + + e.getMessage()); + } + } + + private void stopHttpServer() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + } + + public String getResourcePackUrl() { + // 有外部配置 → 随机生成URL + if (externalUrl != null + && !externalUrl.trim().isEmpty()) { + return generateRandomUrl(externalUrl); + } + // 无配置 → 用内置HTTP + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + return "http://127.0.0.1:" + + port + "/radiopack.zip"; + } + + + /** + * 从配置的域名列表中随机选一个 + * 生成随机、带时间戳的完整资源包URL + * + * 规则: + * *.example.com + * → 随机子域+时间戳 + * 例: a3f7k2.example.com/pack.zip?t=xxx&v=b9c1e4 + * + * cdn.example.com (无*) + * → 直接使用+时间戳 + * 例: cdn.example.com/pack.zip?t=xxx + */ + private String generateRandomUrl( + String raw) { + String[] parts = raw.split("\\|"); + String pick = + parts[(int)(Math.random() + * parts.length)].trim(); + + // 去掉协议 https:// http:// + String cleaned = pick + .replaceFirst("^https?://", ""); + // 去掉路径 /path/a?v=1 + if (cleaned.contains("/")) { + cleaned = cleaned.substring( + 0, cleaned.indexOf('/')); + } + + // 随机子域 + String hex = Long.toHexString( + Double.doubleToLongBits( + Math.random())); + String rand = hex.length() > 6 + ? hex.substring(2, 8) : hex; + rand = rand.replaceAll( + "[^a-z0-9]", "x"); + + String host; + if (cleaned.startsWith("*")) { + host = rand + + cleaned.substring(1); + } else { + host = cleaned; + } + + String ts = String.valueOf( + System.currentTimeMillis()); + String token = Integer.toHexString( + (int)(Math.random() + * 0xFFFFFF)); + + String url = "https://" + host + + "/radiopack.zip" + + "?t=" + ts + + "&v=" + token; + + plugin.getLogger().info( + "[Radio] 生成URL: " + + pick + " → " + url); + return url; + } + + + + // ========== 全服广播 ========== + public void startMainRadio() { + stopMainRadio(); + if (mainFiles.isEmpty()) { + plugin.getLogger().warning( + "[Radio] 无 .ogg 文件"); + return; + } + plugin.getLogger().info( + "[Radio] 启动全服广播 (" + + mainFiles.size() + " 个文件)"); + mainRadioTask = new BukkitRunnable() { + @Override + public void run() { + playNext(); + } + }.runTaskTimer(plugin, 100L, 2400L); + } + + private void playNext() { + if (mainFiles.isEmpty()) return; + File file = mainFiles.get( + radioIndex % mainFiles.size()); + String name = file.getName() + .replace(".ogg", ""); + lastPlayedSound = "custom." + name; + String key = "minecraft:" + lastPlayedSound; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + radioIndex++; + } + + // ========== 按名称播放 ========== + public void playToAll(String name) { + String key = findSongKey(name); + if (key == null) return; + lastPlayedSound = "custom." + name; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + } + + + public void playToPlayer( + Player p, String name) { + String key = findSongKey(name); + if (key == null) { + p.sendMessage("§c未找到: " + name); + return; + } + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + + public void playToLoginPlayers(String name) { + String key = findSongKey(name); + if (key == null) return; + for (String pName + : loginPlayers.keySet()) { + Player lp = Bukkit.getPlayer(pName); + if (lp != null && lp.isOnline()) { + lp.playSound(lp.getLocation(), + key, + SoundCategory.RECORDS, + 1.5f, 1.0f); + } + } + } + + + private String findSongKey(String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + return "minecraft:custom." + name; + } + } + plugin.getLogger().warning( + "[Radio] 未找到歌曲: " + name); + return null; + } + + + public boolean hasSong(String name) { + return findSongKey(name) != null; + } + + // ========== 手动播放 ========== + public boolean playSpecific( + Player sender, String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + String key = "sdf1radio:" + name; + for (Player p + : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + + name); + return true; + } + } + if (sender != null) + sender.sendMessage("§c未找到: " + name); + return false; + } + public void stopMainRadio() { + if (mainRadioTask != null) { + mainRadioTask.cancel(); + mainRadioTask = null; + } + if (lastPlayedSound != null + && !lastPlayedSound.isEmpty()) { + for (Player p : Bukkit.getOnlinePlayers()) { + p.stopSound(lastPlayedSound, + SoundCategory.RECORDS); + } + lastPlayedSound = ""; + } + } + +// 在 init() 或 registerEvents 中保存引用: +// this.listener = new RadioDownloadListener(plugin); +private RadioDownloadListener radioListener; + + + + // ========== 重载 ========== + public void reload() { + stopMainRadio(); + stopAllLoginRadio(); + stopHttpServer(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); + loadRadioConfig(); + } +} \ No newline at end of file diff --git a/backup/src/main/java/Sdf1_login/RefundRecord.java b/backup/src/main/java/Sdf1_login/RefundRecord.java new file mode 100644 index 0000000..c0a3070 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/RefundRecord.java @@ -0,0 +1,62 @@ +package Sdf1_login; + +import org.bukkit.Material; +import java.util.UUID; + +public class RefundRecord { + public final UUID playerUUID; + public final String playerName; + public final String itemId; + public final String itemName; + public final Material material; + public final Material shulkerMaterial; + public int amount; + public int totalPaid; + public final long timestamp; + + // 普通购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, int amount, + int totalPaid, long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = null; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + // 盒装购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, + Material shulkerMat, + int amount, int totalPaid, + long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = shulkerMat; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + public boolean isExpired() { + return System.currentTimeMillis() + - timestamp > 300000L; + } + + public int getSecondsLeft() { + long left = 300000L + - (System.currentTimeMillis() + - timestamp); + return (int) Math.max(0, left / 1000); + } +} diff --git a/backup/src/main/java/Sdf1_login/RiskControlManager.java b/backup/src/main/java/Sdf1_login/RiskControlManager.java new file mode 100644 index 0000000..4ddedcc --- /dev/null +++ b/backup/src/main/java/Sdf1_login/RiskControlManager.java @@ -0,0 +1,248 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警 + */ +public class RiskControlManager { + + private final Main plugin; + + // 每个玩家名 -> 连续失败次数 + private final Map failAttempts = + new ConcurrentHashMap<>(); + + // 每个IP -> 关联的可疑玩家列表 + private final Map> ipSuspicious = + new ConcurrentHashMap<>(); + + // 每个玩家 -> 上次验证时间(用于冷却) + private final Map verifyCooldown = + new ConcurrentHashMap<>(); + + // 配置项 + private static final int MAX_FAIL_ATTEMPTS = 5; + private static final int MAX_ACCOUNTS_PER_IP = 5; + private static final long COOLDOWN_MS = 60000L; + + public RiskControlManager(Main plugin) { + this.plugin = plugin; + } + + /** + * 记录一次失败的登录尝试 + * + * @param playerName 玩家名 + * @return 剩余尝试次数 + */ + public int recordFailAttempt(String playerName) { + int current = failAttempts + .getOrDefault(playerName, 0) + 1; + failAttempts.put(playerName, current); + return Math.max(0, MAX_FAIL_ATTEMPTS - current); + } + + /** + * 重置指定玩家的失败计数(登录成功时调用) + */ + public void resetFailAttempts(String playerName) { + failAttempts.remove(playerName); + } + + /** + * 获取指定玩家的失败次数 + */ + public int getFailAttempts(String playerName) { + return failAttempts + .getOrDefault(playerName, 0); + } + + /** + * 判断玩家是否已被锁定(失败次数达到上限) + */ + public boolean isLocked(String playerName) { + return failAttempts + .getOrDefault(playerName, 0) + >= MAX_FAIL_ATTEMPTS; + } + + /** + * 检查IP是否达到注册上限 + * 直接查数据库,不依赖 DatabaseManager 的方法 + */ + public boolean canRegisterFromIP(String ip) { + if (ip == null || ip.isEmpty()) return true; + try { + java.sql.Connection conn = + plugin.getDb().getDb(); + if (conn == null) return true; + java.sql.PreparedStatement ps = + conn.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address = ?"); + ps.setString(1, ip); + java.sql.ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count < 5; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + + /** + * 记录IP关联的可疑行为 + */ + public void recordSuspiciousIP(String ip, + String playerName) { + if (ip == null || ip.isEmpty()) return; + ipSuspicious + .computeIfAbsent(ip, + k -> ConcurrentHashMap.newKeySet()) + .add(playerName); + } + + /** + * 获取某IP关联的所有可疑玩家 + */ + public Set getSuspiciousAccounts(String ip) { + return ipSuspicious + .getOrDefault(ip, Collections.emptySet()); + } + + /** + * 检查是否在验证冷却期内 + */ + public boolean isOnCooldown(String playerName) { + Long last = verifyCooldown.get(playerName); + if (last == null) return false; + return System.currentTimeMillis() - last + < COOLDOWN_MS; + } + + /** + * 设置验证冷却时间 + */ + public void setCooldown(String playerName) { + verifyCooldown.put(playerName, + System.currentTimeMillis()); + } + + /** + * 当检测到可疑登录时触发报警 + * - 全服广播 + * - 有邮箱则发邮件 + * - 记录到数据库 + */ + public void triggerAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + + // 记录到数据库 + plugin.getDb().recordSecurityAlert( + name, suspiciousIP); + + // 记录IP可疑行为 + recordSuspiciousIP(suspiciousIP, name); + + // 全服广播 + String serverMsg = "§c§l[安全警告] §e" + name + + " §f的账号疑似遭到 §c" + suspiciousIP + + " §f的盗号尝试, 请各位玩家注意账号安全!"; + Bukkit.broadcastMessage(serverMsg); + + // 有邮箱则发邮件 + String emailAddr = (String) plugin.getDb() + .getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String subject = + "[安全警告] 您的账号疑似遭到盗号尝试"; + String body = "尊敬的玩家 " + name + + ":\n\n" + + "您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + suspiciousIP + + " 的盗号尝试。\n\n" + + "建议您:\n" + + "1. 立即修改密码" + + "(使用 /sdf1_login pw)\n" + + "2. 如非本人操作," + + "请联系管理员处理\n\n" + + "--Sdf1_login 安全系统"; + plugin.getEmail().sendTempPassword( + emailAddr, name, body); + } + } + + /** + * 处理登录失败时的风控逻辑 + * + * @param player 玩家 + * @param suspiciousIP 可疑来源IP + * @return true=已被锁定(应踢出), false=仍可继续尝试 + */ + public boolean handleLoginFailure(Player player, + String suspiciousIP) { + String name = player.getName(); + int remaining = recordFailAttempt(name); + + if (remaining <= 0) { + // 达到上限,触发报警 + triggerAlert(player, suspiciousIP); + player.kickPlayer( + "§c§l[风控] §f登录失败次数过多," + + " 账号已被临时锁定, 请联系管理员"); + return true; + } + + // 未达上限,提示剩余次数 + player.sendMessage( + "§c§l[风控] §f密码错误! " + + "剩余" + remaining + "次机会"); + return false; + } + + /** + * 登录成功后重置风控状态 + */ + public void onLoginSuccess(String playerName) { + resetFailAttempts(playerName); + } + + /** + * 清理过期的冷却记录(可由定时任务调用) + */ + public void cleanup() { + long now = System.currentTimeMillis(); + verifyCooldown.entrySet().removeIf( + e -> now - e.getValue() > COOLDOWN_MS * 10); + } + + /** + * 获取所有玩家的失败尝试记录(调试用) + */ + public Map getAllFailAttempts() { + return Collections.unmodifiableMap(failAttempts); + } + + /** + * 清除所有风控数据(管理员调试用) + */ + public void clearAll() { + failAttempts.clear(); + ipSuspicious.clear(); + verifyCooldown.clear(); + } +} diff --git a/backup/src/main/java/Sdf1_login/SalesStatsManager.java b/backup/src/main/java/Sdf1_login/SalesStatsManager.java new file mode 100644 index 0000000..9d06a22 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/SalesStatsManager.java @@ -0,0 +1,170 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class SalesStatsManager { + + private final Main plugin; + + private final Map sessionSales = + new ConcurrentHashMap<>(); + private int sessionRevenue = 0; + private int sessionOrders = 0; + private int sessionItems = 0; + private int sessionRefunds = 0; + private int sessionRefundAmount = 0; + private long sessionStart = System.currentTimeMillis(); + + // 状态 + private boolean waitingForEmpty = false; + private boolean hasReported = false; + private BukkitRunnable waitTask = null; + + public SalesStatsManager(Main plugin) { + this.plugin = plugin; + startMonitor(); + } + + // ★ 每10秒检测一次 + private void startMonitor() { + new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 有玩家在线 → 重置等待状态 + if (waitingForEmpty) { + // 刚有人进来了,清除已汇报标记 + // 下次没人时会重新汇报 + } + waitingForEmpty = false; + hasReported = false; + cancelWaitTask(); + } else if (!waitingForEmpty && !hasReported) { + // 没人了,且还没汇报过 → 开始1分钟倒计时 + waitingForEmpty = true; + startWaitCountdown(); + } + } + }.runTaskTimer(plugin, 200L, 200L); + } + + // ★ 1分钟等待 + private void startWaitCountdown() { + cancelWaitTask(); + waitTask = new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 等待期间有人进来 → 取消,等下次没人 + waitingForEmpty = false; + hasReported = false; + waitTask = null; + cancel(); + return; + } + + // 1分钟没人 → 汇报一次 + if (!hasReported) { + hasReported = true; + report(); + } + waitingForEmpty = false; + waitTask = null; + cancel(); + } + }; + waitTask.runTaskLater(plugin, 1200L); // 60秒 + } + + private void cancelWaitTask() { + if (waitTask != null) { + waitTask.cancel(); + waitTask = null; + } + } + + // ===== 记录 ===== + + public void recordSale(String itemName, int price) { + sessionSales.merge(itemName, 1, Integer::sum); + sessionRevenue += price; + sessionOrders++; + sessionItems++; + } + + public void recordRefund(int amount) { + sessionRefunds++; + sessionRefundAmount += amount; + } + + // ===== 汇报 ===== + + public void report() { + if (sessionItems == 0 && sessionRefunds == 0) { + // plugin.getLogger().info("[Sales] 无数据,跳过"); + return; + } + + long minutes = + (System.currentTimeMillis() - sessionStart) + / 60000; + if (minutes < 1) minutes = 1; + + StringBuilder sb = new StringBuilder(); + sb.append("§6§l========== 销量汇总 =========="); + sb.append("\n§7时长: §f").append(minutes).append("分钟"); + sb.append("\n§7订单: §f").append(sessionOrders).append("笔"); + sb.append("\n§7商品: §f").append(sessionItems).append("件"); + sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚"); + sb.append("\n§7退款: §c").append(sessionRefunds) + .append("次(§c").append(sessionRefundAmount) + .append("枚§7)"); + sb.append("\n§6§l------------------------------"); + + if (!sessionSales.isEmpty()) { + sb.append("\n§7销量排行:"); + List> sorted = + new ArrayList<>(sessionSales.entrySet()); + sorted.sort((a, b) -> + b.getValue() - a.getValue()); + int rank = 1; + for (Map.Entry e : sorted) { + if (rank > 10) break; + sb.append("\n§e").append(rank).append(". ") + .append(e.getKey()) + .append(" §7x").append(e.getValue()); + rank++; + } + } + sb.append("\n§6§l=============================="); + + String msg = sb.toString(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.isOp()) { + for (String line : msg.split("\n")) + p.sendMessage(line); + } + } + plugin.getLogger().info("[Sales] 汇报完成"); + resetSession(); + } + + public void resetSession() { + sessionSales.clear(); + sessionRevenue = 0; + sessionOrders = 0; + sessionItems = 0; + sessionRefunds = 0; + sessionRefundAmount = 0; + sessionStart = System.currentTimeMillis(); + } +} diff --git a/backup/src/main/java/Sdf1_login/ShopCategory.java b/backup/src/main/java/Sdf1_login/ShopCategory.java new file mode 100644 index 0000000..715e9ad --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ShopCategory.java @@ -0,0 +1,32 @@ +package Sdf1_login; + +import java.util.ArrayList; +import java.util.List; + +public class ShopCategory { + private final String name; + private final String fileName; + private final List items; + + public ShopCategory(String name, String fileName) { + this.name = name; + this.fileName = fileName; + this.items = new ArrayList<>(); + } + + public String getName() { return name; } + public String getFileName() { return fileName; } + public List getItems() { return items; } + + public ShopItem getItem(String id) { + for (ShopItem item : items) { + if (item.getId().equalsIgnoreCase(id)) + return item; + } + return null; + } + + public void addItem(ShopItem item) { + items.add(item); + } +} diff --git a/backup/src/main/java/Sdf1_login/ShopItem.java b/backup/src/main/java/Sdf1_login/ShopItem.java new file mode 100644 index 0000000..d336c7c --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ShopItem.java @@ -0,0 +1,49 @@ +package Sdf1_login; + +import org.bukkit.Material; + +public class ShopItem { + private final String id; + private final String displayName; + private final Material material; + private final int buyPrice; + private final int sellPrice; + private int stock; + private int hourlySales; + private int totalSales; + public ShopItem(String id, String displayName, + Material material, int buyPrice, + int sellPrice, int stock, + int hourlySales, int totalSales) { + this.id = id; + this.displayName = displayName; + this.material = material; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.stock = stock; + this.hourlySales = hourlySales; + this.totalSales = totalSales; + } + + public int getEffectiveSellPrice() { + if (sellPrice <= 0) return 0; + return sellPrice; + } + + public String getId() { return id; } + public String getDisplayName() { return displayName; } + public Material getMaterial() { return material; } + public int getBuyPrice() { return buyPrice; } + public int getSellPrice() { return sellPrice; } + public int getStock() { return stock; } + public int getHourlySales() { return hourlySales; } + public int getTotalSales() { return totalSales; } + + public void setStock(int s) { this.stock = s; } + public void setHourlySales(int s) { this.hourlySales = s; } + public void setTotalSales(int s) { this.totalSales = s; } + public void addSales(int n) { + this.hourlySales += n; + this.totalSales += n; + } +} diff --git a/backup/src/main/java/Sdf1_login/ShopManager.java b/backup/src/main/java/Sdf1_login/ShopManager.java new file mode 100644 index 0000000..cdcc045 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/ShopManager.java @@ -0,0 +1,2610 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Base64; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.inventory.meta.PotionMeta; + + + + +public class ShopManager implements Listener { + + private final Main plugin; + private final List categories = + new ArrayList<>(); + private final List refundRecords = + new ArrayList<>(); + private final Map activeDiscount = + new HashMap<>(); + private final Map viewCat = + new HashMap<>(); + private final Map viewPage = + new HashMap<>(); + private final Map> shoppingCarts = + new HashMap<>(); + private final Set cartModeEnabled = + new HashSet<>(); + private final java.util.Random rng = + new java.util.Random(); + private final Map> pendingCheckout = + new HashMap<>(); + private final Set couponListening = + new HashSet<>(); + private final Map pendingCouponCode = + new HashMap<>(); + // ===== 物品NBT数据 ===== + private final Map itemNbtData = + new HashMap<>(); + private final Map itemStackCache = + new HashMap<>(); + // 替换字段 + private final Map couponApplyTime = new HashMap<>(); + + + + private static final Material[] SHULKER_COLORS = { + Material.SHULKER_BOX, + Material.WHITE_SHULKER_BOX, + Material.ORANGE_SHULKER_BOX, + Material.MAGENTA_SHULKER_BOX, + Material.LIGHT_BLUE_SHULKER_BOX, + Material.YELLOW_SHULKER_BOX, + Material.LIME_SHULKER_BOX, + Material.PINK_SHULKER_BOX, + Material.GRAY_SHULKER_BOX, + Material.LIGHT_GRAY_SHULKER_BOX, + Material.CYAN_SHULKER_BOX, + Material.PURPLE_SHULKER_BOX, + Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, + Material.RED_SHULKER_BOX, + Material.BLACK_SHULKER_BOX + }; + private static final Map EFFECT_MAP = new HashMap<>(); + static { + // ===== 基础效果 ===== + EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); + EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); + EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); + EFFECT_MAP.put("速度", PotionEffectType.SPEED); + EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); // 1.21+ 改名 + EFFECT_MAP.put("治疗", PotionEffectType.INSTANT_HEALTH); // 1.21+ 改名 + EFFECT_MAP.put("治愈", PotionEffectType.INSTANT_HEALTH); + EFFECT_MAP.put("伤害", PotionEffectType.INSTANT_DAMAGE); // 1.21+ 改名 + EFFECT_MAP.put("剧毒", PotionEffectType.POISON); + EFFECT_MAP.put("中毒", PotionEffectType.POISON); + EFFECT_MAP.put("回复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("再生", PotionEffectType.REGENERATION); + EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("抗性", PotionEffectType.RESISTANCE); // 1.21+ 改名 + EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); + EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); + EFFECT_MAP.put("急迫", PotionEffectType.HASTE); // 1.21+ 改名 + EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); // 1.21+ 改名 + EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); + EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); // ★ 补充 + EFFECT_MAP.put("迟缓", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); + EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); + EFFECT_MAP.put("幸运", PotionEffectType.LUCK); + EFFECT_MAP.put("不祥", PotionEffectType.UNLUCK); + EFFECT_MAP.put("发光", PotionEffectType.GLOWING); + EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); + EFFECT_MAP.put("生命加成", PotionEffectType.HEALTH_BOOST); + EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); + EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); + EFFECT_MAP.put("海豚关怀", PotionEffectType.DOLPHINS_GRACE); + EFFECT_MAP.put("跳跃", PotionEffectType.JUMP_BOOST); + // ★ 神龟药水是双效果,特殊处理,这里只做名称匹配入口 + EFFECT_MAP.put("神龟", PotionEffectType.RESISTANCE); + } + + public static class CartItem { + String itemId; + String displayName; + Material material; + int unitPrice; + int quantity; + } + + + private static final int PAGE_SIZE = 28; + private static final int[] ITEM_SLOTS = { + 10,11,12,13,14,15,16, + 19,20,21,22,23,24,25, + 28,29,30,31,32,33,34, + 37,38,39,40,41,42,43 + }; + + public ShopManager(Main plugin) { + this.plugin = plugin; + loadCategories(); + startHourlyTask(); + loadItemNbtData(); + startCleanupTask(); + } + // ===== 切换购物车模式 ===== + // ===== 切换购物车模式 ===== + public boolean toggleCartMode(Player p) { + UUID uuid = p.getUniqueId(); + if (cartModeEnabled.contains(uuid)) { + cartModeEnabled.remove(uuid); + p.sendMessage("§7购物车模式已关闭,恢复正常购买"); + return false; + } else { + cartModeEnabled.add(uuid); + p.sendMessage("§a购物车模式已开启,点击商品加入购物车"); + return true; + } + } + + public boolean isCartMode(Player p) { + return cartModeEnabled.contains(p.getUniqueId()); + } + private String itemToBase64(ItemStack item) { + try { + ByteArrayOutputStream os = + new ByteArrayOutputStream(); + BukkitObjectOutputStream bos = + new BukkitObjectOutputStream(os); + bos.writeObject(item); + bos.close(); + return Base64.getEncoder() + .encodeToString(os.toByteArray()); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToItem(String b64) { + try { + byte[] data = + Base64.getDecoder().decode(b64); + ByteArrayInputStream is = + new ByteArrayInputStream(data); + BukkitObjectInputStream bis = + new BukkitObjectInputStream(is); + ItemStack item = + (ItemStack) bis.readObject(); + bis.close(); + return item; + } catch (Exception e) { + return null; + } + } + + private void loadItemNbtData() { + itemNbtData.clear(); + itemStackCache.clear(); + File f = new File(plugin.getDataFolder(), + "shop/item_nbt.properties"); + if (!f.exists()) return; + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + 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 id = + line.substring(0, eq).trim(); + String b64 = + line.substring(eq + 1).trim(); + itemNbtData.put(id, b64); + ItemStack stack = base64ToItem(b64); + if (stack != null) { + itemStackCache.put(id, stack); + } + } + br.close(); + // plugin.getLogger().info("[Shop] NBT数据: " + // + itemStackCache.size() + "条"); + } catch (Exception ignored) {} + } + + private void saveItemNbtData() { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, "item_nbt.properties"); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 商品完整数据(自动生成)"); + for (Map.Entry e + : itemNbtData.entrySet()) { + pw.println(e.getKey() + "=" + + e.getValue()); + } + pw.flush(); + pw.close(); + } catch (Exception ignored) {} + } + + // ===== 加入/移除购物车 ===== + public boolean addToCart(Player p, ShopItem item) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts + .computeIfAbsent( + uuid, k -> new ArrayList<>()); + // ★ 始终用原价,折扣在结算时统一应用 + int price = item.getBuyPrice(); + for (CartItem ci : cart) { + if (ci.itemId.equals(item.getId())) { + ci.quantity += 1; + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + } + CartItem ci = new CartItem(); + ci.itemId = item.getId(); + ci.displayName = item.getDisplayName(); + ci.material = item.getMaterial(); + ci.unitPrice = price; + ci.quantity = 1; + cart.add(ci); + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + + + public int getCartCount(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int c = 0; + for (CartItem ci : cart) c += ci.quantity; + return c; + } + + public int getCartTotal(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int t = 0; + for (CartItem ci : cart) + t += ci.unitPrice * ci.quantity; + return t; + } + + public void cartAddOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + cart.get(index).quantity += 1; + } + + public void cartRemoveOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + CartItem ci = cart.get(index); + ci.quantity -= 1; + if (ci.quantity <= 0) cart.remove(index); + } + + public void clearCart(Player p) { + shoppingCarts.remove(p.getUniqueId()); + } + + // ===== 打开购物车页面 ===== + public void openCart(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§a§l购物车"); + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + for (int i = 0; i < cart.size() && i < 25; i++) { + CartItem ci = cart.get(i); + ItemStack is = new ItemStack(ci.material); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + ci.displayName + + " §7x" + ci.quantity); + im.setLore(java.util.Arrays.asList( + "§7单价: §f" + ci.unitPrice + "枚", + "§7小计: §6" + + (ci.unitPrice * ci.quantity) + + "枚", + "", + "§a左键+1 §c右键-1")); + is.setItemMeta(im); + } + g.setItem(10 + i, is); + } + if (cart.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7购物车为空")); + } + int total = getCartTotal(p); + int bal = plugin.getBonds().getBonds(p.getName()); + g.setItem(49, mkItem(Material.EMERALD, + "§6§l结算: §e" + total + " §6枚债券", + "§7余额: §f" + bal + "枚", + bal >= total ? "§a余额充足" : "§c余额不足", + "", "§a点击结算")); + g.setItem(48, mkItem( + Material.LIME_STAINED_GLASS_PANE, + "§7清空购物车")); + g.setItem(53, mkItem(Material.ARROW, "§7返回商店")); + + p.openInventory(g); + } + + // ===== 购物车点击处理 ===== + public boolean handleCartClick(Player p, int raw, + boolean left, + boolean right) { + if (raw == 53) { openShopMain(p); return true; } + if (raw == 48) { + clearCart(p); + p.sendMessage("§7购物车已清空"); + openCart(p); + return true; + } + if (raw == 49) { + checkoutCartInit(p); + return true; + } + if (raw >= 10 && raw < 35) { + int idx = raw - 10; + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + if (idx < cart.size()) { + if (right) cartRemoveOne(p, idx); + else cartAddOne(p, idx); + openCart(p); + } + return true; + } + return true; + } + + // ===== 结算入口:判断打包方式 ===== + private void checkoutCartInit(Player p) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts.get(uuid); + if (cart == null || cart.isEmpty()) { + p.sendMessage("§c购物车为空"); + return; + } + int total = getCartTotal(p); + int bal = plugin.getBonds() + .getBonds(p.getName()); + if (bal < total) { + p.sendMessage("§c余额不足,需要 §e" + + total + "§c枚"); + return; + } + int totalQty = 0; + for (CartItem ci : cart) totalQty += ci.quantity; + if (cart.size() == 1 && totalQty <= 64) { + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + deductAndGive(p, cart, + (int) Math.round(total * factor), + false, null, "", 0, + ecoPct, couponPct); + return; + } + pendingCheckout.put(uuid, + new ArrayList<>(cart)); + openPackingConfirm(p, total); + } + + + + // ===== 执行扣款+发货 ===== + private boolean deductAndGive(Player p, + List cart, int baseTotal, + boolean useShulker, Material color, + String packNote, int packFee, + int ecoPct, int couponPct) { + + UUID uuid = p.getUniqueId(); + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + String[] names = new String[cart.size()]; + int[] subs = new int[cart.size()]; + for (int i = 0; i < cart.size(); i++) { + CartItem ci = cart.get(i); + names[i] = ci.displayName; + subs[i] = ci.unitPrice * ci.quantity; + } + + if (baseTotal < originalTotal) { + double ratio = + (double) baseTotal / originalTotal; + int sum = 0; + for (int i = 0; i < subs.length; i++) { + if (i == subs.length - 1) { + subs[i] = baseTotal - sum; + } else { + subs[i] = (int) (subs[i] * ratio); + sum += subs[i]; + } + } + } + + boolean ok = plugin.getBonds().checkoutCart( + p.getName(), names, subs); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + if (packFee > 0) { + plugin.getBonds().deductBonds( + p.getName(), packFee, + "shop_packing", "", + "商店系统", "打包费"); + } + + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + if (si != null) { + if (si.getStock() >= 0) + si.setStock(si.getStock() + - ci.quantity); + si.addSales(ci.quantity); + } + } + + int newBal = plugin.getBonds() + .getBonds(p.getName()); + + + // ===== 优惠券码 ===== + String usedCouponCode = + pendingCouponCode.get(uuid); + + // ===== 记录订单 ===== + OrderManager.OrderRecord latestOrder = null; + try { + List orderItems = + new ArrayList<>(); + for (CartItem ci : cart) { + orderItems.add( + new OrderManager.OrderItem( + ci.displayName, + ci.material.name(), + ci.unitPrice, + ci.unitPrice, + ci.quantity)); + } + String dType = "none"; + String cCode = ""; + if (usedCouponCode != null) { + dType = "coupon"; + cCode = usedCouponCode; + } else if (ecoPct > 0) { + dType = "eco"; + } + plugin.getOrderManager().recordOrder( + p, orderItems, originalTotal, + baseTotal + packFee, + originalTotal - baseTotal, + dType, cCode, packFee, + useShulker ? (packFee > 0 + ? "custom" : "default") + : "none", + color != null ? color.name() : ""); + // 获取最新订单 + synchronized (plugin.getOrderManager() + .getOrders()) { + List all = + plugin.getOrderManager() + .getOrders(); + if (!all.isEmpty()) + latestOrder = + all.get(all.size() - 1); + } + // 散装打印小票 + if (!useShulker && latestOrder != null) { + plugin.getOrderManager() + .sendReceiptChat(p, latestOrder); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + // + ex.getMessage()); + } + +// ===== 发货 ===== + if (!useShulker) { + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + ItemStack give = (si != null) + ? getShopStack(si, ci.quantity) + : new ItemStack(ci.material, ci.quantity); + HashMap left2 = + p.getInventory().addItem(give); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } else { + List shulkers = + packShulkers(cart, color); + + // ★ 给潜影盒打订单ID标签 + if (latestOrder != null) { + for (ItemStack sh : shulkers) { + ItemMeta shm = sh.getItemMeta(); + if (shm != null) { + List lore = shm.getLore() != null + ? new ArrayList<>(shm.getLore()) + : new ArrayList<>(); + lore.add("§8#" + latestOrder.orderId); + shm.setLore(lore); + sh.setItemMeta(shm); + } + } + } + + // 小票书塞入 + if (!shulkers.isEmpty() + && latestOrder != null) { + ItemStack book = plugin.getOrderManager() + .createReceiptBook(latestOrder); + plugin.getOrderManager() + .addBookToShulker(shulkers.get(0), book); + } + // 发货 + for (ItemStack sh : shulkers) { + HashMap left2 = + p.getInventory().addItem(sh); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } + + + // ===== 退款记录 ===== + long now = System.currentTimeMillis(); + if (!useShulker) { + for (CartItem ci : cart) { + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + ci.itemId, ci.displayName, + ci.material, null, + ci.quantity, + ci.unitPrice * ci.quantity, + now)); + } + } else { + String summary = ""; + for (CartItem ci : cart) { + if (!summary.isEmpty()) summary += "+"; + summary += ci.displayName + + "x" + ci.quantity; + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + "cart_" + now, summary, + Material.SHULKER_BOX, color, + 1, baseTotal, now)); + } + + // ===== 核销优惠券 ===== + if (usedCouponCode != null) { + activeDiscount.remove(uuid); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCouponCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCouponCode + + " §7已核销"); + } + + shoppingCarts.remove(uuid); + pendingCheckout.remove(uuid); + return true; + } + + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + markShulkerUsed(item); + } + + // ★ 公共方法:标记潜影盒为已使用 + private void markShulkerUsed(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + // shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + @EventHandler + public void onPlayerDrop(org.bukkit.event.player.PlayerDropItemEvent e) { + ItemStack item = e.getItemDrop().getItemStack(); + if (item != null && item.getType().name().contains("SHULKER")) { + markShulkerUsed(item); + } + } + + + // ===== 潠影盒打包(指定颜色)===== + private List packShulkers( + List items, Material color) { + List result = new ArrayList<>(); + List all = new ArrayList<>(); + for (CartItem ci : items) { + int rem = ci.quantity; + while (rem > 0) { + int sz = Math.min(rem, 64); + all.add(new ItemStack(ci.material, sz)); + rem -= sz; + } + } + while (!all.isEmpty()) { + ItemStack shulker = new ItemStack(color); + org.bukkit.inventory.meta.BlockStateMeta meta = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (meta != null) { + org.bukkit.block.BlockState state = + meta.getBlockState(); + if (state instanceof org.bukkit.block.Container) { + org.bukkit.block.Container c = + (org.bukkit.block.Container) state; + List remain = new ArrayList<>(); + for (ItemStack stack : all) { + HashMap left = + c.getInventory().addItem(stack); + for (ItemStack l : left.values()) + remain.add(l); + } + all = remain; + meta.setBlockState(c); + shulker.setItemMeta(meta); + } + } + result.add(shulker); + } + + return result; + } + + // ===== 打包确认GUI ===== + public void openPackingConfirm(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l选择包装方式"); + g.setItem(10, mkItem(Material.CHEST, + "§a§l不打包", + "§7直接放入背包", + "§7适合少量物品")); + g.setItem(13, mkItem(Material.SHULKER_BOX, + "§6§l默认打包", + "§7随机颜色潜影盒", + "§7加收 §e5 §6枚债券")); + g.setItem(16, mkItem(Material.ENDER_CHEST, + "§b§l自选颜色", + "§7选择你喜欢的潜影盒颜色", + "§7加收 §e5 §6枚债券")); + g.setItem(22, mkItem(Material.PAPER, + "§7消费金额: §e" + baseTotal + "枚")); + + p.openInventory(g); + } + + public boolean handlePackingConfirm( + Player p, int raw) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + + if (raw == 10) { + ecoPct = 2; + } + + double factor = 1.0; + if (ecoPct > 0) + factor *= (100 - ecoPct) / 100.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + originalTotal * factor); + + int totalQty = 0; + for (CartItem ci : cart) + totalQty += ci.quantity; + boolean multiAndBig = (cart.size() > 1 + && originalTotal >= 100); + + if (raw == 10) { + deductAndGive(p, cart, discounted, + false, null, "", 0, + ecoPct, couponPct); + return true; + } + if (raw == 13) { + Material c = Material.SHULKER_BOX; + if (multiAndBig) { + deductAndGive(p, cart, discounted, + true, c, "", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, + true, c, "默认打包", 5, + ecoPct, couponPct); + } + return true; + } + if (raw == 16) { + pendingCheckout.put(uuid, cart); + openColorSelect(p, discounted); + return true; + } + return true; + } + + + // ===== 自选颜色GUI ===== + public void openColorSelect(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 54, "§b§l选择潜影盒颜色"); + String[] colorNames = { + "白色","橙色","品红","淡蓝", + "黄色","淡绿","粉色","灰色", + "淡灰","青色","紫色","蓝色", + "棕色","绿色","红色","黑色","原色" + }; + for (int i = 0; i < SHULKER_COLORS.length; i++) { + ItemStack is = new ItemStack(SHULKER_COLORS[i]); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + colorNames[i] + + "§6潜影盒"); + im.setLore(java.util.Arrays.asList( + "§7加收 §e5 §6枚债券", + "", + "§a点击选择")); + is.setItemMeta(im); + } + g.setItem(i, is); + } + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(45, mkItem(Material.PAPER, + "§7消费: §e" + baseTotal + + "枚 + §e5§6打包费")); + + p.openInventory(g); + } + + public boolean handleColorSelect( + Player p, int raw) { + if (raw == 49) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.get(uuid); + if (cart != null) { + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + openPackingConfirm(p, total); + } + return true; + } + if (raw < 0 || raw >= SHULKER_COLORS.length) + return false; + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + total * factor); + + Material selectedColor = SHULKER_COLORS[raw]; + if (selectedColor == Material.SHULKER_BOX) { + deductAndGive(p, cart, discounted, true, + Material.SHULKER_BOX, + "原色打包", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, true, + selectedColor, "自选颜色", 5, + ecoPct, couponPct); + } + return true; + } + + + + // ===== 加载分类 ===== + + public void loadCategories() { + // plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); // 过滤调试日志 + categories.clear(); + viewCat.clear(); // ★ 清缓存 + viewPage.clear(); + + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + + convertTxtToMd(dir); + + File[] allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + if (allMd == null || allMd.length == 0) { + createDefaultCategories(dir); + allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + } + if (allMd == null) { + // plugin.getLogger().warning( + // "[Shop] shop目录为空"); + return; + } + + /* plugin.getLogger().info("[Shop] === 开始加载 ==="); + plugin.getLogger().info("[Shop] 扫描到 " + + allMd.length + " 个md文件"); +*/ + for (File f : allMd) { + /* plugin.getLogger().info("[Shop] 读取: " + + f.getName());*/ + ShopCategory cat = parseFile(f); + if (cat != null) { + /* plugin.getLogger().info("[Shop] 分类: " + + cat.getName() + + " 商品数: " + + cat.getItems().size());*/ + if (!cat.getItems().isEmpty()) { + categories.add(cat); + } + } + } + + int totalItems = 0; + for (ShopCategory c : categories) + totalItems += c.getItems().size(); + + plugin.getLogger().info("[Shop] === 加载完成 ===" + + " 分类=" + categories.size() + + " 商品=" + totalItems); + } + + +// ===== txt → md 自动迁移 ===== + + private void convertTxtToMd(File dir) { + File[] txtFiles = dir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("coupons.txt")); + if (txtFiles == null) return; + for (File txt : txtFiles) { + String mdName = txt.getName() + .replace(".txt", ".md"); + File mdFile = new File(dir, mdName); + if (mdFile.exists()) continue; + + try { + List lines = new ArrayList<>(); + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(txt), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) + lines.add(line); + br.close(); + + String catName = txt.getName() + .replace(".txt", ""); + List out = new ArrayList<>(); + out.add("# " + catName); + out.add(""); + out.add("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + out.add("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + + for (String l : lines) { + l = l.trim(); + if (l.isEmpty() || l.startsWith("#")) + continue; + // 旧格式: ID | 名称 | 材质 | ... + // 保持原样转换为 md 表格行 + out.add("| " + l.replace("|", "|") + + " |"); + } + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(mdFile), + StandardCharsets.UTF_8)); + for (String o : out) + pw.println(o); + pw.flush(); + pw.close(); + + txt.delete(); + // plugin.getLogger().info( + // "[Shop] 已迁移: " + // + txt.getName() + // + " → " + mdName); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 迁移失败: " + // + txt.getName()); + } + } + } + +// ===== 解析 md 文件 ===== + + private ShopCategory parseFile(File f) { + String name = f.getName() + .replace(".md", ""); + ShopCategory cat = + new ShopCategory(name, f.getName()); + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + boolean headerPassed = false; + boolean separatorPassed = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + // 空行跳过 + if (line.isEmpty()) continue; + + // 标题行:# 分类名 + if (line.startsWith("#")) { + String t = line.replaceFirst( + "^#+\\s*", "").trim(); + if (!t.isEmpty()) { + // 更新分类名(用文件内标题) + // cat.name 是 final,不改 + } + continue; + } + + // 表格分隔行:| --- | --- | + if (line.contains("---")) { + separatorPassed = true; + continue; + } + + // 表头行:| ID | 品名 | ... + if (line.startsWith("|")) { + if (!separatorPassed) { + headerPassed = true; + continue; + } + // 数据行 + ShopItem item = + parseTableRow(line); + if (item != null) + cat.addItem(item); + } + } + br.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 解析失败: " + f.getName() + // + " 原因: " + e.getMessage()); + return null; + } + return cat; + } + +// ===== 解析表格行 ===== + + private ShopItem parseTableRow(String row) { + // plugin.getLogger().info("[Shop] 行: " + row); // 过滤调试日志 + + String trimmed = row.trim(); + if (trimmed.startsWith("|")) + trimmed = trimmed.substring(1); + if (trimmed.endsWith("|")) + trimmed = trimmed.substring(0, + trimmed.length() - 1); + + String[] p = trimmed.split("\\|"); + // plugin.getLogger().info("[Shop] 列数=" + p.length); // 过滤调试日志 + + if (p.length < 6) { + // plugin.getLogger().warning( + // "[Shop] ✗ 列数不足, 跳过"); + return null; + } + + String rawId = p[0].trim(); + String rawName = p[1].trim(); + String rawMat = p[2].trim().toUpperCase(); + String rawBuy = p[3].trim(); + String rawSell = p[4].trim(); + String rawStock = p[5].trim(); + + // plugin.getLogger().info("[Shop] ID=" + // + rawId + " 名=" + rawName + // + " 材质=" + rawMat + // + " 购=" + rawBuy + " 售=" + rawSell + // + " 库=" + rawStock); // 过滤调试日志 + + // 检查材质是否存在 + Material mat = Material.matchMaterial(rawMat); + if (mat == null) { + // plugin.getLogger().warning( + // "[Shop] ✗ 无效材质: " + rawMat + // + " (行: " + row + ")"); + return null; + } + + try { + ShopItem item = new ShopItem( + rawId, rawName, mat, + Integer.parseInt(rawBuy), + Integer.parseInt(rawSell), + Integer.parseInt(rawStock), + p.length > 6 + ? Integer.parseInt( + p[6].trim()) : 0, + p.length > 7 + ? Integer.parseInt( + p[7].trim()) : 0); + // plugin.getLogger().info("[Shop] ✓ 解析成功: " + rawId); // 过滤调试日志 + return item; + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] ✗ 数字解析失败: " + // + e.getMessage()); + return null; + } + } + + +// ===== 保存为 md ===== + + public void saveCategory(ShopCategory cat) { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, cat.getFileName()); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# " + cat.getName()); + pw.println(""); + pw.println("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + pw.println("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + for (ShopItem item : cat.getItems()) { + pw.println("| " + + item.getId() + + " | " + item.getDisplayName() + + " | " + item.getMaterial().name() + + " | " + item.getBuyPrice() + + " | " + item.getSellPrice() + + " | " + item.getStock() + + " | " + item.getHourlySales() + + " | " + item.getTotalSales() + + " |"); + } + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 保存失败: " + // + cat.getFileName()); + } + } + +// ===== 创建示例分类 ===== + + private void createDefaultCategories(File dir) { + writeMd(dir, "建筑材料.md", + "# 建筑材料\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| STONE | 石头 | STONE" + + " | 10 | -1 | -1 | 0 | 0 |\n" + + "| COBBLESTONE | 圆石 | COBBLESTONE" + + " | 5 | -1 | -1 | 0 | 0 |\n" + + "| OAK_PLANKS | 橡木木板 | OAK_PLANKS" + + " | 8 | 3 | -1 | 0 | 0 |\n" + + "| BRICKS | 砖块 | BRICKS" + + " | 12 | -1 | -1 | 0 | 0 |\n" + + "| GLASS | 玻璃 | GLASS" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK" + + " | 20 | 8 | -1 | 0 | 0 |\n"); + + writeMd(dir, "装备工具.md", + "# 装备工具\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| IRON_PICKAXE | 铁镐 | IRON_PICKAXE" + + " | 50 | 20 | -1 | 0 | 0 |\n" + + "| DIAMOND_PICKAXE | 钻石镐 | DIAMOND_PICKAXE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| IRON_SWORD | 铁剑 | IRON_SWORD" + + " | 40 | 16 | -1 | 0 | 0 |\n" + + "| DIAMOND_SWORD | 钻石剑 | DIAMOND_SWORD" + + " | 400 | 160 | -1 | 0 | 0 |\n" + + "| ELYTRA | 鞘翅 | ELYTRA" + + " | 2000 | 800 | -1 | 0 | 0 |\n"); + + writeMd(dir, "食物补给.md", + "# 食物补给\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| COOKED_BEEF | 熟牛排 | COOKED_BEEF" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| GOLDEN_APPLE | 金苹果 | GOLDEN_APPLE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| BREAD | 面包 | BREAD" + + " | 8 | 3 | -1 | 0 | 0 |\n"); + + // plugin.getLogger().info( + // "[Shop] 已创建3个示例分类(md)"); + } + + private void writeMd(File dir, String name, + String content) { + File f = new File(dir, name); + if (f.exists()) return; + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.print(content); + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 创建失败: " + name); + } + } + + /** + * 从商品名称+材质智能推断完整物品 + * + * "夜视药水" + POTION + * → PotionEffect(NIGHT_VISION, 600, 0) + * + * "喷溅型治疗药水" + SPLASH_POTION + * → PotionEffect(HEALING, 600, 0) + * + * "长效夜视之箭" + TIPPED_ARROW + * → PotionEffect(NIGHT_VISION, 1800, 0) + * + * "强效力量喷溅药水" + SPLASH_POTION + * → PotionEffect(STRENGTH, 600, 1) + */ + private ItemStack inferFromName( + String name, Material mat) { + // 只处理药水和箭 + if (mat != Material.POTION + && mat != Material.SPLASH_POTION + && mat != Material.LINGERING_POTION + && mat != Material.TIPPED_ARROW) { + return null; + } + + // 1. 找效果类型 + PotionEffectType effectType = null; + for (Map.Entry e + : EFFECT_MAP.entrySet()) { + if (name.contains(e.getKey())) { + effectType = e.getValue(); + break; + } + } + if (effectType == null) return null; + + // 2. 找时长 + int duration = 600; + if (name.contains("长效") || name.contains("长时") + || name.contains("持久") || name.contains("长命")) { + duration = 1800; + } +// ★ 瞬间效果药水:duration = 1 + if (name.contains("治疗") || name.contains("治愈") + || name.contains("伤害")) { + duration = 1; + } + + // 3. 找等级 + int amplifier = 0; + if (name.contains("强效") + || name.contains("强化") + || name.contains("加强")) { + amplifier = 1; + } + + // 4. 构造完整物品 + try { + ItemStack item = new ItemStack(mat); + PotionMeta meta = + (PotionMeta) item.getItemMeta(); + + // ★ 神龟药水:双效果 + if (name.contains("神龟")) { + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.RESISTANCE, + duration, amplifier + 2), + true); + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.SLOWNESS, + duration, amplifier + 3), + true); + } else { + // 普通药水:单效果 + meta.addCustomEffect( + new PotionEffect(effectType, + duration, amplifier), + true); + } + + item.setItemMeta(meta); + return item; + } catch (Exception e) { + return null; + } + } + + public void saveAll() { + for (ShopCategory cat : categories) + saveCategory(cat); + } + private ItemStack getShopStack( + ShopItem item, int amount) { + // 优先级1: 手动捕获的NBT数据 + ItemStack cached = + itemStackCache.get(item.getId()); + if (cached != null) { + ItemStack copy = cached.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级2: 根据名称智能推断 + ItemStack inferred = inferFromName( + item.getDisplayName(), + item.getMaterial()); + if (inferred != null) { + ItemStack copy = inferred.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级3: 纯材质(默认) + return new ItemStack( + item.getMaterial(), amount); + } + + // ===== 购买 ===== + + public boolean buyItem(Player p, ShopItem item, + int amount) { + // plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); + // plugin.getLogger().info("buyItem被调用"); + if (item == null) return false; + if (item.getStock() >= 0 + && item.getStock() < amount) { + p.sendMessage("§c库存不足,剩余: " + + item.getStock()); + return false; + } + if (p.getInventory().firstEmpty() == -1 + && !p.getInventory().contains( + item.getMaterial(), 1)) { + p.sendMessage("§c背包已满"); + return false; + } + + int unit = item.getBuyPrice(); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int total = Math.max(0, + unit * amount * (100 - disc) / 100); + + int balance = plugin.getBonds() + .getBonds(p.getName()); + if (balance < total) { + p.sendMessage("§c债券不足,需要 §e" + + total + "§c枚,当前 §e" + + balance + "§c枚"); + return false; + } + + boolean ok = plugin.getBonds().deductBonds( + p.getName(), total, + "shop_buy", item.getId(), + "商店系统", "购买" + + item.getDisplayName()); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + ItemStack give = getShopStack(item, amount); + HashMap left = + p.getInventory().addItem(give); + for (ItemStack drop : left.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + + if (item.getStock() >= 0) + item.setStock( + item.getStock() - amount); + item.addSales(amount); + + mergeOrAddRefund(p, item, amount, total); + Long applyTime = couponApplyTime.get(p.getUniqueId()); + if (applyTime != null + && System.currentTimeMillis() - applyTime > 300000L) { + // 超过5分钟,自动失效 + pendingCouponCode.remove(p.getUniqueId()); + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + p.sendMessage("§7优惠券已过期,请重新输入"); + disc = 0; // 重置折扣 + } + + + String discMsg = disc > 0 + ? " §7(优惠" + disc + "%)" : ""; + p.sendMessage("§a购买成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a-" + total + "枚债券" + + discMsg); + if (disc > 0) + activeDiscount.remove( + p.getUniqueId()); + // ★ 直购成功后核销优惠券 + String usedCode = pendingCouponCode + .remove(p.getUniqueId()); + if (usedCode != null) { + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCode + + " §7已核销"); + couponApplyTime.remove(p.getUniqueId()); + } + // ★ 记录订单(包裹try-catch,失败不影响购买) + try { + String usedCouponCode = + pendingCouponCode.get(p.getUniqueId()); + List orderItems = + new ArrayList<>(); + orderItems.add(new OrderManager.OrderItem( + item.getDisplayName(), + item.getMaterial().name(), + item.getBuyPrice(), + item.getBuyPrice(), amount)); + plugin.getOrderManager().recordOrder( + p, orderItems, + item.getBuyPrice() * amount, total, + item.getBuyPrice() * amount - total, + usedCouponCode != null ? "coupon" : "none", + usedCouponCode != null ? usedCouponCode : "", + 0, "none", ""); + // 打印小票 + List all = + plugin.getOrderManager().getOrders(); + if (!all.isEmpty()) { + plugin.getOrderManager().sendReceiptChat( + p, all.get(all.size() - 1)); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + ex.getMessage()); + } + + return true; + } + + + public void handleRefundItemClick(Player p, int raw) { + if (raw < 10 || raw >= 45) return; + int target = raw - 10; + int cnt = 0; + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { it.remove(); continue; } + if (cnt == target) { + requestRefund(p, r); + return; + } + cnt++; + } + } + } + + + // ===== 出售 ===== + + public boolean sellItem(Player p, ShopItem item, + int amount) { + if (item == null) return false; + + // ★ 先判断售价,禁止出售的不收物品 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return false; + } + + if (!p.getInventory().containsAtLeast( + getShopStack(item, 1), amount)) { + p.sendMessage("§c你没有足够的 §e" + + item.getDisplayName()); + return false; + } + + int total = item.getEffectiveSellPrice() * amount; + + p.getInventory().removeItem( + getShopStack(item, amount)); + plugin.getBonds().addBonds( + p.getName(), total, "shop_sell", + item.getId(), "商店系统", + "出售" + item.getDisplayName()); + if (item.getStock() >= 0) + item.setStock(item.getStock() + amount); + p.sendMessage("§a出售成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a+" + total + "枚债券"); + return true; + } + + // ===== 退款合并:30秒内同商品合并为一个订单 ===== + private boolean mergeOrAddRefund(Player p, ShopItem item, + int amount, int total) { + long now = System.currentTimeMillis(); + synchronized (refundRecords) { + for (int i = refundRecords.size() - 1; i >= 0; i--) { + RefundRecord r = refundRecords.get(i); + boolean same = r.playerUUID.equals(p.getUniqueId()) + && r.itemId.equals(item.getId()) + && (now - r.timestamp) < 30000L; + if (same) { + // final 字段不能改,只能删旧建新 + int newAmount = r.amount + amount; + int newTotal = r.totalPaid + total; + refundRecords.remove(i); + refundRecords.add(new RefundRecord( + r.playerUUID, r.playerName, + r.itemId, r.itemName, + r.material, newAmount, + newTotal, now)); + return true; + } + } + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + item.getId(), item.getDisplayName(), + item.getMaterial(), amount, total, now)); + return false; + } + + + + // ===== 退款 ===== + + public boolean requestRefund(Player p, RefundRecord rec) { + if (rec == null) return false; + if (!rec.playerUUID.equals(p.getUniqueId())) { + p.sendMessage("§c这不是你的购买记录"); + return false; + } + if (rec.isExpired()) { + p.sendMessage("§c已超过5分钟退款期限"); + synchronized (refundRecords) { + refundRecords.remove(rec); + } + return false; + } + + // 检测物品/潜影盒是否还在 + synchronized (refundRecords) { + if (rec.shulkerMaterial != null) { + int boxCount = countShulkers(p, rec.shulkerMaterial); + if (boxCount < 1) { + p.sendMessage("§c潜影盒已移出背包,无法退款"); + refundRecords.remove(rec); + return false; + } + } else { + int have = countDirect(p, rec.material); + if (have < rec.amount) { + p.sendMessage("§c物品已转移或消耗,无法退款"); + refundRecords.remove(rec); + return false; + } + } + } + + // 退款:扣物品/潜影盒 + if (rec.shulkerMaterial != null) { + // 盒装:扣一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == rec.shulkerMaterial) { + p.getInventory().clear(i); + break; + } + } + } else { + // 普通:扣商品 + p.getInventory().removeItem( + new ItemStack(rec.material, rec.amount)); + } + + // 退款:加债券 + plugin.getBonds().addBonds( + p.getName(), rec.totalPaid, + "refund", rec.itemId, + "商店系统", "退款" + rec.itemName); + + // 恢复库存 + ShopItem si = findItemById(rec.itemId); + if (si != null && si.getStock() >= 0) + si.setStock(si.getStock() + rec.amount); + + synchronized (refundRecords) { + refundRecords.remove(rec); + } + + p.sendMessage("§a退款成功: §e" + rec.itemName + + " §a+" + rec.totalPaid + "枚债券"); + return true; + } + + + + + public void setPlayerDiscount(Player p, + int pct) { + activeDiscount.put( + p.getUniqueId(), pct); + p.sendMessage("§a已设置 " + pct + + "% 折扣,下次购买生效"); + } + + // ===== 管理员操作 ===== + + public boolean addItemToCategory( + String catName, String id, + String displayName, Material mat, + int buyPrice, int sellPrice, + int stock) { + ShopCategory cat = + findCategory(catName); + if (cat == null) return false; + if (cat.getItem(id) != null) return false; + cat.addItem(new ShopItem(id, displayName, + mat, buyPrice, sellPrice, + stock, 0, 0)); + saveCategory(cat); + return true; + } + + public boolean removeItem(String id) { + for (ShopCategory cat : categories) { + ShopItem item = cat.getItem(id); + if (item != null) { + cat.getItems().remove(item); + saveCategory(cat); + return true; + } + } + return false; + } + + public boolean setStock(String id, int stock) { + ShopItem item = findItemById(id); + if (item == null) return false; + item.setStock(stock); + for (ShopCategory cat : categories) { + if (cat.getItem(id) != null) { + saveCategory(cat); + return true; + } + } + return false; + } + + public ShopCategory findCategory(String n) { + for (ShopCategory c : categories) + if (c.getName().equals(n)) return c; + return null; + } + + public ShopItem findItemById(String id) { + for (ShopCategory c : categories) { + ShopItem item = c.getItem(id); + if (item != null) return item; + } + return null; + } + + public List getCategories() { + return categories; + } + + // ===== 命令处理 ===== + + public boolean handleCommand( + CommandSender s, String[] a) { + if (a.length <= 1) { + if (s instanceof Player) { + openShopMain((Player) s); + } else { + s.sendMessage("§c仅玩家可使用"); + } + return true; + } + + String sub = a[1].toLowerCase(); + + if (sub.equals("reload")) { + loadCategories(); + s.sendMessage("§a商店已重载,共 " + + categories.size() + + " 个分类"); + return true; + } + + if (sub.equals("add")) { + if (a.length < 7) { + s.sendMessage( + "§e用法: shop add <分类>" + + " <名称> <材质>" + + " <购入价>" + + " [售出价-1自动]" + + " [库存-1无限]"); + return true; + } + Material mat; + try { + mat = Material.valueOf( + a[5].toUpperCase()); + } catch (Exception e) { + s.sendMessage( + "§c无效材质: " + a[5]); + return true; + } + int price = Integer.parseInt(a[6]); + int sell = a.length > 7 + ? Integer.parseInt(a[7]) : -1; + int stock = a.length > 8 + ? Integer.parseInt(a[8]) : -1; + if (addItemToCategory(a[2], a[3], + a[4], mat, price, sell, stock)) { + s.sendMessage("§a已添加: " + + a[4] + " → " + a[2]); + } else { + s.sendMessage("§c添加失败" + + "(分类不存在或ID重复)"); + } + return true; + } + + if (sub.equals("remove")) { + if (a.length < 3) { + s.sendMessage( + "§e用法: shop remove "); + return true; + } + if (removeItem(a[2])) { + s.sendMessage( + "§a已移除: " + a[2]); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("set")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop set " + + " <库存数(-1无限)>"); + return true; + } + int stock = Integer.parseInt(a[3]); + if (setStock(a[2], stock)) { + s.sendMessage("§a已设置 " + + a[2] + " 库存为 " + + stock); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("pro")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop pro" + + " <玩家名> <折扣%>"); + return true; + } + Player t = + Bukkit.getPlayerExact(a[2]); + if (t == null) { + s.sendMessage("§c玩家不在线"); + return true; + } + int pct = Integer.parseInt(a[3]); + setPlayerDiscount(t, pct); + s.sendMessage("§a已对 " + a[2] + + " 设置 " + pct + "% 折扣"); + return true; + } + + + s.sendMessage("§e商店子命令:" + + " reload / add / remove" + + " / set / pro "); + return true; + } + @EventHandler(priority = EventPriority.LOW) + public void onCouponChat( + org.bukkit.event.player.AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + if (!couponListening.contains(uuid)) return; + e.setCancelled(true); + couponListening.remove(uuid); + String code = e.getMessage().trim(); + if (code.isEmpty()) { + p.sendMessage("§c已取消输入"); + return; + } + // ★ 预处理:标准化输入 + String normalized = normalizeCouponInput(code); + if (normalized.isEmpty()) { + p.sendMessage("§c优惠券码无效(输入为空或仅含特殊字符)"); + couponListening.remove(uuid); + return; + } + + p.sendMessage("§7正在验证优惠券: §e" + code); + + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "validateCoupon", + String.class); + Object result = m.invoke(sdf1, normalized); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception ex) { + p.sendMessage("§c验证异常: " + + ex.getMessage()); + return; + } + if (discount <= 0) { + p.sendMessage("§c优惠券无效或已使用"); + return; + } + int offPct = 100 - discount; + int current = activeDiscount + .getOrDefault(uuid, 0); + if (offPct > current) { + pendingCouponCode.put(uuid, normalized); + activeDiscount.put(uuid, offPct); + couponApplyTime.put(uuid, System.currentTimeMillis()); + String zhe; + if (discount % 10 == 0) + zhe = (discount / 10) + "折"; + else + zhe = (discount / 10.0) + "折"; + p.sendMessage("§6§l[优惠券] §a验证通过!"); + p.sendMessage("§7折扣: §e" + zhe + + " §7(减" + offPct + "%)"); + p.sendMessage("§7结算时自动核销"); + final UUID u = uuid; + Bukkit.getScheduler().runTask(plugin, () -> { + Player pl = Bukkit.getPlayer(u); + if (pl != null && pl.isOnline()) { + openShopMain(pl); + } + }); + } else { + String curZhe; + if ((100 - current) % 10 == 0) + curZhe = ((100 - current) / 10) + "折"; + else + curZhe = ((100 - current) / 10.0) + "折"; + p.sendMessage("§c当前已有更优折扣: " + + curZhe + ",本次不生效"); + } + } + + /** + * 优惠券输入标准化 + * 全角→半角、去分隔符、转小写 + * "AAA-BBB-CCCC" → "aaaabbbbcccc" + * "AAA-BBB" → "aaabbb" + * "aaa_bbb_cccc" → "aaabbbcccc" + */ + private String normalizeCouponInput(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // 全角数字→半角 + if (c >= '\uFF10' && c <= '\uFF19') { + sb.append((char)(c - 0xFF10 + '0')); + } + // 全角字母→半角 + else if (c >= '\uFF21' && c <= '\uFF3A') { + sb.append((char)(c - 0xFF21 + 'A')); + } + else if (c >= '\uFF41' && c <= '\uFF5A') { + sb.append((char)(c - 0xFF41 + 'a')); + } + // ★ 保留中文字符(Unicode CJK统一汉字范围) + else if ((c >= '\u4E00' && c <= '\u9FFF') + || (c >= '\u3400' && c <= '\u4DBF')) { + sb.append(c); + } + // 保留半角字母和数字 + else if ((c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')) { + sb.append(c); + } + // 分隔符(- _ 空格等)丢弃 + } + return sb.toString().toLowerCase(); + } + + + + + + private String formatZhe(int discount) { + if (discount % 10 == 0) { + return (discount / 10) + "折"; + } + return (discount / 10.0) + "折"; + } + + public boolean redeemCoupon(Player p, String code) { + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + Object result = m.invoke(sdf1, code); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception e) { + return false; + } + if (discount <= 0) return false; + int offPct = 100 - discount; + int cur = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (offPct > cur) { + activeDiscount.put(p.getUniqueId(), offPct); + } + return true; + } + + /** + * 商店主界面点击"优惠券"按钮后调用 + * 开启聊天监听,等待玩家输入优惠券码 + */ + public void openCouponInput(Player p) { + UUID uuid = p.getUniqueId(); + // 先取消已有的待核销优惠券 + pendingCouponCode.remove(uuid); + couponListening.add(uuid); + // 关闭UI + Bukkit.getScheduler().runTask(plugin, () -> { + if (p.isOnline()) p.closeInventory(); + }); + p.sendMessage("§6§l[优惠券] §e请在聊天栏输入优惠券码"); + p.sendMessage("§715秒内输入,过期自动取消"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (couponListening.remove(uuid)) { + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§c优惠券输入已超时取消"); + } + } + }, 300L); + } + + + // ===== GUI: 主界面 ===== + + public void openShopMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l商店"); + int si = 0; + for (ShopCategory cat : categories) { + if (si >= ITEM_SLOTS.length) break; + ItemStack is = new ItemStack( + Material.CHEST); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l" + cat.getName()); + im.setLore(Arrays.asList( + "§7商品: §f" + + cat.getItems().size() + + " 种", + "", + "§a点击进入")); + is.setItemMeta(im); + } + g.setItem(ITEM_SLOTS[si], is); + si++; + } + // ★ 改为跳转订单中心 + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看订单记录、申请退款", + "", "§a点击进入")); + + // 在 openShopMain 方法中,原来的 slot 49/53 附近加: + + g.setItem(48, mkItem( + isCartMode(p) + ? Material.LIME_WOOL + : Material.GRAY_WOOL, + isCartMode(p) + ? "§a§l购物车模式 §7[开]" + : "§7§l购物车模式 §7[关]", + isCartMode(p) + ? "§a当前: 点击商品加入购物车" + : "§7当前: 直接购买", + "§7点击切换")); + g.setItem(50, mkItem(Material.CHEST, + "§6§l购物车 §7[" + getCartCount(p) + "件]", + "§7总价: §6" + getCartTotal(p) + "枚", + "", + "§a点击打开购物车")); + + // 在现有按钮之后,p.openInventory(g) 之前加: + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看购物订单记录", + "", "§a点击进入")); + + // 优惠券按钮 - slot 47 + String discStatus = "§7"; + int curDisc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (curDisc > 0) { + discStatus = "§a当前: " + formatZhe(100 - curDisc); + } + g.setItem(47, mkItem(Material.NAME_TAG, + "§6§l优惠券", + discStatus, + "", + "§a点击输入优惠券码", + "§7一次性使用,核销后失效")); + + g.setItem(53, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + // ===== GUI: 分类 ===== + public void openCategory(Player p, + ShopCategory cat, + int page) { + // ★ 每次重新获取最新对象 ★ + ShopCategory fresh = null; + for (ShopCategory c : categories) { + if (c.getName().equals(cat.getName())) { + fresh = c; + break; + } + } + if (fresh == null) { + openShopMain(p); + return; + } + cat = fresh; + viewCat.put(p.getUniqueId(), cat); + + int total = (int) Math.ceil( + (double) cat.getItems().size() + / PAGE_SIZE); + if (total < 1) total = 1; + if (page < 0) page = 0; + if (page >= total) page = total - 1; + viewPage.put(p.getUniqueId(), page); + + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l" + cat.getName()); + + + int start = page * PAGE_SIZE; + int end = Math.min(start + PAGE_SIZE, + cat.getItems().size()); + + for (int i = start; i < end; i++) { + int idx = i - start; + if (idx >= ITEM_SLOTS.length) break; + + // ★ item 保留原名,代表 ShopItem 数据 + ShopItem item = cat.getItems().get(i); + + // ★ is 是带完整元数据的 ItemStack(用于GUI图标) + ItemStack is = getShopStack(item, 1); + ItemMeta im = is.getItemMeta(); + + if (im != null) { + im.setDisplayName("§e§l" + + item.getDisplayName()); + List lore = new ArrayList<>(); + lore.add("§7购入: §a" + + item.getBuyPrice() + + "§7枚债券"); + lore.add("§7售出: §e" + + item.getEffectiveSellPrice() + + "§7枚债券"); + if (item.getStock() >= 0) + lore.add("§7库存: §f" + + item.getStock()); + else + lore.add("§7库存: §a无限"); + lore.add(""); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (disc > 0) { + int dis = item.getBuyPrice() + * (100 - disc) / 100; + lore.add("§7优惠价: §6" + + dis + "枚" + + " §7(-" + disc + "%)"); + } + lore.add("§a左键买1 §6右键卖1"); + lore.add("§aShift+左键买64" + + " §6Shift+右键卖64"); + im.setLore(lore); + is.setItemMeta(im); + } + + // ★ 用 is(ItemStack)放入GUI格子 + g.setItem(ITEM_SLOTS[idx], is); + } + + + g.setItem(4, mkItem(Material.BOOK, + "§e§l" + cat.getName(), + "§7共 " + cat.getItems().size() + + " 种商品")); + g.setItem(48, page > 0 + ? mkItem(Material.ARROW, + "§7上一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(49, mkItem(Material.PAPER, + "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 + ? mkItem(Material.ARROW, + "§7下一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, + "§7返回商店")); + + p.openInventory(g); + } + + // ===== GUI: 退款 ===== + + public void openRefundPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l退款中心"); + + // 位移检测:比对上次记录的背包内容 + validateRefunds(p); + + // 过期清理 + synchronized (refundRecords) { + refundRecords.removeIf(RefundRecord::isExpired); + } + + int idx = 0; + synchronized (refundRecords) { + for (RefundRecord r : refundRecords) { + if (idx >= 45) break; + if (!r.playerUUID.equals(p.getUniqueId())) continue; + if (r.isExpired()) continue; + + // 盒装用潜影盒图标,普通用商品图标 + Material iconMat = (r.shulkerMaterial != null) + ? r.shulkerMaterial : r.material; + + ItemStack is = new ItemStack(iconMat); + ItemMeta im = is.getItemMeta(); + if (im != null) { + int secs = r.getSecondsLeft(); + im.setDisplayName("§e" + r.itemName + + (r.shulkerMaterial != null + ? " §7[盒装]" : " x" + r.amount)); + + List lore = new ArrayList<>(); + lore.add("§7订单: §f#" + r.timestamp); + lore.add("§7支付: §a" + r.totalPaid + + "§7枚债券"); + + if (r.shulkerMaterial != null) { + int boxCount = countShulkers(p, + r.shulkerMaterial); + lore.add("§7潜影盒: §f" + + (boxCount >= 1 + ? "§a在背包中" + : "§c已移出")); + } else { + int have = countDirect(p, r.material); + lore.add("§7物品: §f" + have + + "/" + r.amount); + } + + lore.add("§7剩余: §e" + secs + "秒"); + lore.add(""); + lore.add("§c点击退款"); + im.setLore(lore); + } + is.setItemMeta(im); + g.setItem(10 + idx, is); + idx++; + } + } + + if (idx == 0) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无可退款记录")); + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + + p.openInventory(g); + } + // 统计背包中某物品数量 + private int countInInventory(Player p, Material mat) { + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == mat) { + count += is.getAmount(); + } + } + // 潜影盒也要检查里面 + for (ItemStack is : p.getInventory().getContents()) { + if (is != null + && is.getType().name() + .endsWith("_SHULKER_BOX")) { + count += is.getAmount(); + } + } + return count; + } + + // 只统计背包中直接可见的数量(不含盒内) + // 新代码:增加按完整物品匹配的方法 + private int countDirect(Player p, Material mat) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.getType() == mat) + count += is.getAmount(); + } + return count; + } + + private int countDirectByStack(Player p, + ItemStack target) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.isSimilar(target)) + count += is.getAmount(); + } + return count; + } + // 统计背包中潜影盒数量 + private int countShulkers(Player p, Material shulker) { + if (shulker == null) return Integer.MAX_VALUE; + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == shulker) + count += is.getAmount(); + } + return count; + } + + // 退款检测:位移 OR 消耗 → 不能退 + private void validateRefunds(Player p) { + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { + it.remove(); + continue; + } + + if (r.shulkerMaterial != null) { + // 盒装:检测潜影盒是否还在背包 + int now = countShulkers( + p, r.shulkerMaterial); + if (now < 1) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 潜影盒已移出背包,无法退款"); + } + } else { + // 普通:检测商品是否还在背包 + int now = countDirect(p, r.material); + if (now < r.amount) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 物品已被使用或移出,无法退款"); + } + } + } + } + } + + + public int getPlayerPage(java.util.UUID uuid) { + return viewPage.getOrDefault(uuid, 0); + } + + public ShopCategory getPlayerCategory(java.util.UUID uuid) { + return viewCat.get(uuid); + } + + // ===== 供GUIManager调用:商店主界面点击 ===== + public boolean handleMainShopClick(Player p, int raw) { + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true; } + + if (raw == 48) { + toggleCartMode(p); + openShopMain(p); + return true; + } + if (raw == 50) { + openCart(p); + return true; + } + if (raw == 53) { + plugin.getGui().openMain(p); + return true; + } + + // ★ 新增:优惠券按钮 + if (raw == 47) { + openCouponInput(p); + return true; + } + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true;} + int idx = slotToIndex(raw); + if (idx < 0 || idx >= categories.size()) + return true; + openCategory(p, categories.get(idx), 0); + return true; + } + + // ===== 供GUIManager调用:退款中心点击 ===== + public boolean handleRefundClick(Player p, int raw) { + if (raw == 49) { openShopMain(p); return true; } + return true; + } + + // ===== 供GUIManager调用:分类页点击 ===== + public boolean handleCategoryClick(Player p, String title, + int raw, boolean left, + boolean shift) { + // plugin.getLogger().info("[Shop] ★ handleCategoryClick raw=" + raw); + + ShopManager sm = this; + ShopCategory match = null; + for (ShopCategory c : categories) { + if (title.equals("§6§l" + c.getName())) { + match = c; + break; + } + } + if (match == null) { + // plugin.getLogger().info("[Shop] ★ 匹配失败, title=[" + title + "]"); + return false; + } + // plugin.getLogger().info("[Shop] ★ 匹配到: " + match.getName()); + + viewCat.put(p.getUniqueId(), match); + + if (raw == 53) { openShopMain(p); return true; } + int pg = getPlayerPage(p.getUniqueId()); + if (raw == 48) { openCategory(p, match, pg - 1); return true; } + if (raw == 50) { openCategory(p, match, pg + 1); return true; } + + int itemIdx = slotToIndex(raw); + if (itemIdx < 0) { + // plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); + return true; + } + int actual = itemIdx + pg * PAGE_SIZE; + if (actual < 0 || actual >= match.getItems().size()) { + // plugin.getLogger().info("[Shop] ★ 超出范围 actual=" + actual + " size=" + match.getItems().size()); + return true; + } + ShopItem si = match.getItems().get(actual); + // plugin.getLogger().info("[Shop] ★ 准备购买: " + si.getId() + " left=" + left); + + int amount = shift ? 64 : 1; + if (left) { + if (shift && isCartMode(p)) { addToCart(p, si); } + else if (shift) { buyItem(p, si, 64); } + else if (isCartMode(p)) { addToCart(p, si); } + else { buyItem(p, si, 1); } + } else { + if (shift) { sellAllOf(p, si); } + else { sellItem(p, si, 1); } + } + openCategory(p, match, pg); + return true; + } + + // ===== 批量出售某商品全部数量 ===== + private void sellAllOf(Player p, ShopItem item) { + if (item == null) return; + // ★ 补上禁止出售检查 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return; + } + if (item == null) return; + Material mat = item.getMaterial(); + int total = 0; + int count = 0; + // 遍历背包清点 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + total += is.getAmount(); + count++; + } + } + if (total <= 0) { + p.sendMessage("§c你没有可出售的 §e" + + item.getDisplayName()); + return; + } + // 清除背包中该物品 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + } + } + int money = item.getEffectiveSellPrice() * total; + plugin.getBonds().addBonds( + p.getName(), money, "shop_sell", + item.getId(), "商店系统", + "批量出售" + item.getDisplayName() + + " x" + total); + if (item.getStock() >= 0) + item.setStock(item.getStock() + total); + p.sendMessage("§a批量出售: §e" + + item.getDisplayName() + + " x" + total + + " §a+" + money + "枚债券"); + } + + + // ===== 定时 ===== + + private void startHourlyTask() { + new BukkitRunnable() { + @Override + public void run() { + // ★ 汇报交给 SalesStatsManager + if (plugin.getSalesStats() != null) { + plugin.getSalesStats().report(); + } + // 重置小时销量 + for (ShopCategory cat : categories) + for (ShopItem item : cat.getItems()) + item.setHourlySales(0); + saveAll(); + } + }.runTaskTimer(plugin, 72000L, 72000L); + } + + + private void startCleanupTask() { + new BukkitRunnable() { + @Override + public void run() { + synchronized (refundRecords) { + refundRecords.removeIf( + RefundRecord::isExpired); + } + } + }.runTaskTimer(plugin, 1200L, 1200L); + } + + // ===== 工具 ===== + + private int slotToIndex(int slot) { + for (int i = 0; i < ITEM_SLOTS.length; i++) + if (ITEM_SLOTS[i] == slot) return i; + return -1; + } + + private ItemStack mkItem(Material mat, + String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src/main/java/Sdf1_login/TicketManager.java b/backup/src/main/java/Sdf1_login/TicketManager.java new file mode 100644 index 0000000..c21edc2 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/TicketManager.java @@ -0,0 +1,1644 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TicketManager { + + private final Main plugin; + + public static final String T_TICKET_MAIN = + "§6§l工单中心"; + public static final String T_TICKET_CREATE = + "§e§l创建工单"; + public static final String T_TICKET_MY = + "§b§l我的工单"; + public static final String T_TICKET_ADMIN = + "§c§l工单管理"; + public static final String T_TICKET_DETAIL = + "§a§l工单详情"; + + public static final String TYPE_BUG = + "bug反馈"; + public static final String TYPE_HELP = + "求助"; + public static final String TYPE_REPORT = + "举报"; + public static final String TYPE_APPLY = + "申请"; + public static final String TYPE_OTHER = + "其他"; + public static final String TYPE_DELETE = + "删号"; + public static final String TYPE_PASSWORD = + "修改密码"; + + + + private static final Map + PRICES = new LinkedHashMap<>(); + static { + PRICES.put(TYPE_BUG, 10); + PRICES.put(TYPE_HELP, 5); + PRICES.put(TYPE_REPORT, 8); + PRICES.put(TYPE_APPLY, 15); + PRICES.put(TYPE_OTHER, 5); + PRICES.put(TYPE_DELETE, 0); + PRICES.put(TYPE_PASSWORD, 0); + } + + + public TicketManager(Main plugin) { + this.plugin = plugin; + } + + // ========== GUI ========== + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_MAIN); + fillBg(g); + g.setItem(10, mk(Material.PAPER, + "§e创建工单", + "§7提交bug、求助、举报等")); + g.setItem(12, mk(Material.BOOK, + "§b我的工单", + "§7查看已提交的工单")); + g.setItem(14, mk(Material.CHEST, + "§6工单记录", + "§7查看历史工单")); + if (adm(p)) { + g.setItem(16, mk(Material.REDSTONE, + "§c工单管理", + "§7管理员专用")); + } + if (plugin.getDb() + .isServiceProvider( + p.getName())) { + g.setItem(18, mk(Material.EMERALD, + "§a服务商面板", + "§7接单、处理工单")); + } + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openCreate(Player p) { + Inventory g = Bukkit.createInventory( + null, 36, T_TICKET_CREATE); + fillBg(g); + g.setItem(10, mk(Material.BOOK, + "§cbug反馈")); + g.setItem(11, mk(Material.EMERALD, + "§a求助")); + g.setItem(12, mk(Material.BLAZE_POWDER, + "§6举报")); + g.setItem(13, mk(Material.NAME_TAG, + "§e申请")); + g.setItem(14, mk(Material.PAPER, + "§7其他")); + g.setItem(20, mk(Material.BARRIER, + "§c申请删号", + "§7申请删除账号")); + g.setItem(21, mk(Material.NETHER_WART, + "§d申请改密", + "§7请求管理员协助改密")); + g.setItem(31, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyTickets(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_MY); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByPlayer( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openDetail(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_DETAIL); + fillBg(g); + + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = sv(t, "requester"); + String title = sv(t, "title"); + String desc = sv(t, "description"); + String assigned = sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + int confirmed = nv(t, "admin_confirmed"); + + g.setItem(10, mk(Material.PAPER, + "§e§l" + (title.isEmpty() + ? type : title), + "§7类型: §f" + type, + "§7状态: " + stxt(status), + "§7提交者: §f" + requester, + "§7奖励: §e" + reward + " 积分")); + + g.setItem(12, mk(Material.BOOK, + "§7详细描述", + desc.isEmpty() ? "§7无描述" + : "§7" + (desc.length() > 40 + ? desc.substring(0, 40) + + "..." : desc))); + + if (!assigned.isEmpty()) { + g.setItem(14, mk(Material.PLAYER_HEAD, + "§7处理人: §f" + assigned, + confirmed == 1 + ? "§7评分: §e" + score + : "§7处理中")); + } else { + g.setItem(14, mk(Material.BARRIER, + "§7未分配")); + } + + // 驳回原因 + if ("closed".equals(status)) { + String reason = sv(t, "reject_reason"); + if (!reason.isEmpty()) { + g.setItem(13, mk(Material.BARRIER, + "§c驳回原因", + "§7" + reason)); + } + } + + // 回复按钮(非关闭/已完结状态) + if (!"closed".equals(status) + && !"withdrawn".equals(status) + && !"resolved".equals(status)) { + g.setItem(15, mk( + Material.WRITABLE_BOOK, + "§b回复工单", + "§7向对方发送消息")); + g.setItem(16, mk(Material.BOOK, + "§7查看回复记录")); + } + + // ===== 管理员按钮 ===== + if (adm(p)) { + if ("pending".equals(status)) { + // 待分配:可分配给服务商 + g.setItem(19, mk(Material.EMERALD, + "§a分配工单", + "§7指定服务商处理")); + // 也可自己处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && !assigned.equals("admin") + && !assigned.isEmpty()) { + // 服务商在处理:可取消指派收回 + g.setItem(19, mk(Material.BARRIER, + "§c取消指派", + "§7取消服务商指派,收回工单")); + // 也可自己接手处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && assigned.equals("admin")) { + // 管理员自己在处理:可标记完结 + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + if ("replied".equals(status) + && assigned.equals("admin")) { + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + // 驳回(非已关闭/已完结状态) + if (!"closed".equals(status) + && !"resolved".equals(status) + && !"withdrawn".equals(status)) { + g.setItem(21, mk(Material.RED_DYE, + "§c驳回工单", + "§7驳回并关闭此工单")); + } + } + + // ===== 服务商按钮 ===== + if (plugin.getDb() + .isServiceProvider(p.getName()) + && !TYPE_DELETE.equals(type) + && !TYPE_PASSWORD.equals(type)) { + if ("pending".equals(status) + && assigned.isEmpty()) { + // 待分配无人:抢单 + g.setItem(19, mk( + Material.GOLD_INGOT, + "§6抢单", + "§7接取此工单")); + } + if (assigned.equals(p.getName()) + && ("processing".equals(status) + || "replied".equals(status))) { + // 我在处理:标记完结/上报异常 + g.setItem(19, mk( + Material.LIME_DYE, + "§a标记完结", + "§7上报处理完成")); + g.setItem(20, mk( + Material.REDSTONE, + "§c上报异常", + "§7报告处理中的问题")); + } + } + + g.setItem(22, mk(Material.ARROW, + "§7返回")); + + ItemStack info = g.getItem(10); + if (info != null) { + ItemMeta im = info.getItemMeta(); + if (im != null) { + im.setCustomModelData(tid); + info.setItemMeta(im); + } + } + p.openInventory(g); + } + + + public void openAdminList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_ADMIN); + fillBg(g); + g.setItem(45, mk(Material.EMERALD, + "§a邀请服务商")); + g.setItem(46, mk(Material.BARRIER, + "§c移除服务商")); + g.setItem(47, mk(Material.CLOCK, + "§e服务商列表")); + g.setItem(48, mk(Material.NETHER_STAR, + "§e直接派单", + "§7管理员直接创建并派单")); + List> all = + new ArrayList<>(); + all.addAll(plugin.getDb() + .getTicketsByStatus("pending")); + all.addAll(plugin.getDb() + .getTicketsByStatus("processing")); + all.addAll(plugin.getDb() + .getTicketsByStatus("replied")); + all.addAll(plugin.getDb() + .getTicketsByStatus("completed")); + all.addAll(plugin.getDb() + .getTicketsByStatus("resolved")); + all.addAll(plugin.getDb() + .getTicketsByStatus("closed")); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + + public void openProviderPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§d§l服务商面板"); + fillBg(g); + g.setItem(10, mk(Material.CHEST, + "§e抢单大厅", + "§7查看可接工单")); + g.setItem(12, mk(Material.CLOCK, + "§b我的工单", + "§7我正在处理的工单")); + g.setItem(14, mk(Material.PAPER, + "§6历史工单", + "§7已完成的工单")); + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openGrabList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l抢单大厅"); + fillBg(g); + List> pending = + plugin.getDb() + .getTicketsByStatus( + "pending"); + List> list = + new ArrayList<>(); + for (Map t : pending) { + if (sv(t, "assigned_to").isEmpty()) { + list.add(t); + } + } + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyAssigned(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l我处理中的工单"); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByProvider( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + // ========== 业务 ========== + + public int submitTicket(Player p, + String type, String title, + String desc) { + int price = PRICES + .getOrDefault(type, 5); + int id = plugin.getDb().createTicket( + type, "pending", "normal", + p.getName(), title, desc, + price, ""); + if (id > 0) { + if (TYPE_DELETE.equals(type) + || TYPE_PASSWORD.equals(type)) { + plugin.getDb().updateTicketField( + id, "assigned_to", "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage( + "§a§l[工单] §f工单 #" + + id + " 已提交," + + "等待管理员处理"); + } else { + p.sendMessage( + "§a§l[工单] §f创建成功!" + + " #§e" + id + + " 奖励: §e" + + price + "积分"); + } + notifyAdmins(p.getName(), id); + } else { + p.sendMessage("§c创建失败"); + } + return id; + } + + + public void adminCreateAndAssign( + Player p, String type, + String title, String desc, + String provider, int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + int id = plugin.getDb().createTicket( + type, "processing", "normal", + p.getName(), title, desc, + price, ""); + if (id <= 0) { + p.sendMessage("§c创建失败"); + return; + } + plugin.getDb().assignTicket( + id, provider); + if (price > 0) { + plugin.getDb().updateTicketField( + id, "reward_amount", price); + } + p.sendMessage("§a工单 #" + id + + " 已创建并分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f管理员指派工单 #" + + id + " 给您"); + playAssignedSound(tp); + } + } + + public void grabTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!"pending".equals( + sv(t, "status"))) { + p.sendMessage("§c该工单已被接单"); + return; + } + if (!sv(t, "assigned_to").isEmpty()) { + p.sendMessage("§c该工单已被分配"); + return; + } + plugin.getDb().assignTicket( + tid, p.getName()); + p.sendMessage("§a抢单成功!工单 #" + tid); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f您的工单 #" + + tid + " 已被 " + + p.getName() + + " 接单"); + } + } + + public void assignToProvider(Player p, + int tid, String provider, + int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + plugin.getDb().assignTicket( + tid, provider); + if (price > 0) { + plugin.getDb().updateTicketStatus( + tid, "processing"); + plugin.getDb().updateTicketField( + tid, "reward_amount", price); + } + p.sendMessage("§a工单 #" + tid + + " 已分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f您有新工单: #" + + tid); + playAssignedSound(tp); + } + } + + public void confirmAndScore(Player p, + int tid, int score) { + if (score < 1 || score > 5) { + p.sendMessage("§c评分1-5"); + return; + } + plugin.getDb().confirmTicket( + tid, score); + p.sendMessage("§a工单 #" + tid + + " 已确认,评分: " + score); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已确认完结"); + } + } + settleTicket(tid); + } + + public void completeAsProvider(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "assigned_to") + .equals(p.getName())) { + p.sendMessage("§c这不是你的工单"); + return; + } + plugin.getDb().completeTicket(tid); + p.sendMessage("§a工单 #" + tid + + " 已标记完结,等待确认"); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已处理完成"); + } + } + + public void rejectTicket(Player p, + int tid, String reason) { + plugin.getDb().updateTicketStatus( + tid, "closed"); + plugin.getDb().updateTicketField( + tid, "reject_reason", reason); + p.sendMessage("§c工单 #" + tid + + " 已驳回并关闭"); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 被驳回: " + + reason); + } + } + } + + public void replyTicket(Player p, + int tid, String message) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + String status = sv(t, "status"); + if ("closed".equals(status) + || "withdrawn".equals(status) + || "resolved".equals(status)) { + p.sendMessage("§c该工单已关闭"); + return; + } + plugin.getDb().addTicketReply( + tid, p.getName(), message); + p.sendMessage("§a回复成功"); + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + String sender = p.getName(); + if (!requester.equals(sender)) { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + if (!assigned.isEmpty() + && !assigned.equals(sender)) { + Player ap = Bukkit.getPlayer( + assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + } + + public void showReplies(Player p, + int tid) { + List> replies = + plugin.getDb() + .getTicketReplies(tid); + if (replies.isEmpty()) { + p.sendMessage("§7暂无回复"); + return; + } + p.sendMessage("§e§l=== 回复记录 ==="); + SimpleDateFormat sdf = + new SimpleDateFormat( + "MM-dd HH:mm"); + for (Map r : replies) { + String sender = sv(r, "sender"); + String msg2 = sv(r, "message"); + long time = ((Number) r.get( + "create_time")).longValue(); + p.sendMessage("§7[" + sdf.format( + new Date(time)) + "] §f" + + sender + "§7: " + msg2); + } + p.sendMessage( + "§e§l================="); + } + + public void withdrawTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "requester") + .equals(p.getName()) + && !adm(p)) { + p.sendMessage("§c无权操作"); + return; + } + plugin.getDb().updateTicketStatus( + tid, "withdrawn"); + p.sendMessage("§a工单 #" + tid + + " 已撤回"); + } + + public void showProviderList(Player p) { + List> list = + plugin.getDb() + .getAllServiceProviders(); + if (list.isEmpty()) { + p.sendMessage("§7暂无服务商"); + return; + } + p.sendMessage( + "§e§l=== 服务商列表 ==="); + for (Map pro : list) { + p.sendMessage("§7- §f" + + pro.get("player_name") + + " §7角色: §e" + + pro.get("role")); + } + p.sendMessage( + "§e§l==================="); + } + + public void autoProcessCompleted() { + long timeoutMs = 10 * 60 * 1000L; + + // 自动处理删号工单(5分钟自动通过) + List> deleteTickets = + plugin.getDb() + .getTicketsByStatus( + "processing"); + for (Map t : deleteTickets) { + String dtype = sv(t, "type"); + if (!TYPE_DELETE.equals(dtype)) continue; + int did = nv(t, "id"); + int dconf = nv(t, "admin_confirmed"); + // 已经处理过了跳过 + if (dconf != 0) continue; + String dasg = sv(t, "assigned_to"); + // 只处理分配给admin的 + if (!"admin".equals(dasg)) continue; + long createTime = ((Number) t.get( + "create_time")).longValue(); + if (System.currentTimeMillis() + - createTime >= 5 * 60 * 1000L) { + processDeleteAccount(did, t); + } + } + + + // 已完结未确认→10分钟自动完结 + List> list = + plugin.getDb() + .getUnconfirmedCompleted( + timeoutMs); + for (Map t : list) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 已自动完结"); + String assigned = + sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) + pp.sendMessage( + "§e§l[工单] §f工单 #" + + id + + " 已自动完结"); + } + settleTicket(id); + } + + // 已回复但10分钟无互动→自动完结 + List> stale = + plugin.getDb() + .getRepliedStaleTickets( + timeoutMs); + for (Map t : stale) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 超时自动完结"); + settleTicket(id); + } + } + + + + // ========== GUI点击 ========== + public boolean handleClick(Player p, + String title, int slot) { + + if (title.equals(T_TICKET_MAIN)) { + if (slot == 10) openCreate(p); + else if (slot == 12) + openMyTickets(p); + else if (slot == 14) + openMyTickets(p); + else if (slot == 16) { + if (adm(p)) openAdminList(p); + else p.sendMessage("§c权限不足"); + } + else if (slot == 18) { + if (plugin.getDb() + .isServiceProvider(p.getName())) { + openProviderPanel(p); + } else { + p.sendMessage("§c权限不足"); + } + } + else if (slot == 22) + plugin.getGui().openMain(p); + return true; + } + + if (title.equals(T_TICKET_CREATE)) { + if (slot == 31) { + openMain(p); + return true; + } + String type = null; + if (slot == 10) type = TYPE_BUG; + else if (slot == 11) type = TYPE_HELP; + else if (slot == 12) type = TYPE_REPORT; + else if (slot == 13) type = TYPE_APPLY; + else if (slot == 14) type = TYPE_OTHER; + else if (slot == 20) type = TYPE_DELETE; + else if (slot == 21) type = TYPE_PASSWORD; + if (type != null) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_TITLE; + plugin.getChatInput() + .getState(p) + .ticketType = type; + if (TYPE_DELETE.equals(type)) { + p.sendMessage("§e请描述删号原因:"); + } else if (TYPE_PASSWORD.equals(type)) { + p.sendMessage("§e请描述需要协助的内容:"); + } else { + p.sendMessage("§e请输入工单标题:"); + } + } + return true; + } + + + if (title.equals(T_TICKET_MY)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals(T_TICKET_DETAIL)) { + return handleClickDetail(p, slot); + } + + if (title.equals(T_TICKET_ADMIN)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot == 45) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_INVITE_PROVIDER; + p.sendMessage("§e输入服务商玩家名:"); + return true; + } + if (slot == 46) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REMOVE_PROVIDER; + p.sendMessage("§e输入要移除的服务商:"); + return true; + } + if (slot == 47) { + showProviderList(p); + return true; + } + if (slot == 48) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ADMIN_TYPE; + p.sendMessage("§e选择工单类型:"); + p.sendMessage("§7bug/求助/举报/申请/其他"); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§d§l服务商面板")) { + if (slot == 22) { + plugin.getGui().openMain(p); + return true; + } + if (slot == 10) openGrabList(p); + else if (slot == 12) openMyAssigned(p); + else if (slot == 14) openMyTickets(p); + return true; + } + + if (title.equals("§6§l抢单大厅")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§b§l我处理中的工单")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + return false; + } + + private boolean handleClickDetail(Player p, + int slot) { + if (slot == 22) { + openMyTickets(p); + return true; + } + int id = gtFromDetail(p); + if (id <= 0) return true; + Map t = + plugin.getDb().getTicket(id); + if (t == null) return true; + + String st = sv(t, "status"); + int conf = nv(t, "admin_confirmed"); + String asg = sv(t, "assigned_to"); + + // 回复按钮 + if (slot == 15) { + if (!"closed".equals(st) + && !"withdrawn".equals(st) + && !"resolved".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPLY; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入回复内容:"); + } + return true; + } + + // 查看回复记录 + if (slot == 16) { + showReplies(p, id); + return true; + } + + // slot 19 + if (slot == 19) { + // 管理员:分配工单 + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ASSIGN; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入: 服务商名 价格"); + return true; + } + // 管理员:取消指派(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + cancelTicket(p, id); + openDetail(p, id); + return true; + } + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); + return true; + } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; + } + } + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && "pending".equals(st) + && asg.isEmpty()) { + grabTicket(p, id); + openDetail(p, id); + return true; + } + // 服务商:标记完结 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + completeAsProvider(p, id); + openDetail(p, id); + return true; + } + return true; + } + + // slot 20 + if (slot == 20) { + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:自己接手(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 服务商:上报异常 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPORT_ISSUE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入异常描述:"); + return true; + } + return true; + } + + // slot 21:驳回 + if (slot == 21) { + if (adm(p) && !"closed".equals(st) + && !"resolved".equals(st) + && !"withdrawn".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REJECT; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入驳回原因:"); + } + return true; + } + + return true; + } + + // ========== 聊天输入 ========== + + public boolean handleChatInput(Player p, + String msg, + ChatInputManager.InputType type) { + switch (type) { + case TICKET_TITLE: { + plugin.getChatInput() + .getState(p) + .ticketTitle = msg; + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return true; + } + case TICKET_DESC: { + String tt = plugin.getChatInput() + .getState(p).ticketType; + String tl = plugin.getChatInput() + .getState(p).ticketTitle; + plugin.getChatInput().reset(p); + submitTicket(p, tt, tl, msg); + return true; + } + case TICKET_SCORE: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + Map tk = + plugin.getDb() + .getTicket(tid2); + String st = tk != null + ? sv(tk, "status") + : ""; + if ("completed".equals(st)) { + plugin.getChatInput() + .reset(p); + confirmAndScore( + p, tid2, sc); + } else { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c工单尚未处理完成"); + } + } catch ( + NumberFormatException e) { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return true; + } + case TICKET_REJECT: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + plugin.getChatInput().reset(p); + rejectTicket(p, tid2, msg); + return true; + } + case TICKET_ASSIGN: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + plugin.getChatInput().reset(p); + assignToProvider(p, tid2, + prov, price); + runSync(() -> + openDetail(p, tid2)); + return true; + } + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return true; + } + plugin.getDb() + .addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return true; + } + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .isServiceProvider( + tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return true; + } + plugin.getDb() + .removeServiceProvider( + tgt); + p.sendMessage( + "§a已移除 " + tgt); + return true; + } + default: + return false; + } + } + + // ========== 私有 ========== + + private void settleTicket(int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) return; + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + ConfigManager cfg = + plugin.getConfig2(); + if (reward > 0) { + plugin.getDb().addPoints( + requester, reward); + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 积分奖励"); + } + } + if (!assigned.isEmpty()) { + int pts = (int) (score + * cfg.providerPointsPerScore); + if (pts > 0) { + plugin.getDb().addPoints( + assigned, pts); + } + double money = + cfg.baseEconomyReward + + score + * cfg.providerEconomyPerScore; + boolean ok = false; + try { + if (plugin.getEconomy() != null) { + OfflinePlayer op = + Bukkit.getOfflinePlayer( + assigned); + plugin.getEconomy() + .depositPlayer( + op, money); + ok = true; + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 经济失败: " + + e.getMessage()); + } + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (pts > 0) + pp.sendMessage("§7积分: §e" + + pts); + if (ok) + pp.sendMessage("§7经济: §a$" + + String.format( + "%.0f", money)); + } + } + } + + private void notifyAdmins(String creator, + int tid) { + String tag = + plugin.getConfig2().adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§e§l[工单] §f" + creator + + " 提交了新工单 #" + + tid); + } + } + } + + private void playAssignedSound(Player p) { + for (int i = 0; i < 3; i++) { + final int d = i * 15; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline()) { + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + }, d); + } + } + + private void runSync(Runnable task) { + Bukkit.getScheduler() + .runTask(plugin, task); + } + + private boolean adm(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory g) { + ItemStack glass = mk( + Material.GRAY_STAINED_GLASS_PANE, + " "); + for (int i = 0; i < g.getSize(); i++) { + if (g.getItem(i) == null + || g.getItem(i).getType() + == Material.AIR) { + g.setItem(i, glass); + } + } + } + + private void fillList(Inventory g, + List> list) { + if (list.isEmpty()) { + g.setItem(22, mk(Material.BARRIER, + "§7暂无工单")); + return; + } + int slot = 0; + for (Map t : list) { + if (slot >= 45) break; + g.setItem(slot, makeItem(t)); + slot++; + } + } + /** + * 执行删号流程 + */ + /** + * 执行删号流程 + */ + public void processDeleteAccount(int tid, + Map t) { + String requester = sv(t, "requester"); + plugin.getDb().updateTicketStatus( + tid, "resolved"); + plugin.getDb().updateTicketField(tid, + "admin_confirmed", 1); + Player target = + Bukkit.getPlayer(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + // 延迟执行删号 + final String name = requester; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + plugin.getDb().deleteUser(name); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 删号工单已执行"); + }, 100L); + } + + /** + * 管理员手动批准删号 + */ + public void approveDeleteTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!TYPE_DELETE.equals(sv(t, "type"))) { + p.sendMessage("§c这不是删号工单"); + return; + } + p.sendMessage("§a工单 #" + tid + + " 已批准,正在执行删号..."); + processDeleteAccount(tid, t); + } + + /** + * 管理员取消工单(服务商异常后) + */ + /** + * 管理员取消工单(解除服务商绑定,管理员可自行处理) + */ + public void cancelTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + // 解除服务商绑定,状态回退到pending + plugin.getDb().updateTicketField( + tid, "assigned_to", ""); + plugin.getDb().updateTicketStatus( + tid, "pending"); + p.sendMessage("§a工单 #" + tid + + " 已取消指派,回到待分配状态"); + // 通知原服务商 + String assigned = sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player ap = Bukkit.getPlayer(assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 已被管理员取消指派"); + } + } + } + + + /** + * 服务商查看历史工单 + */ + public void openProviderHistory(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l我的历史工单"); + fillBg(g); + List> all = + plugin.getDb() + .getTicketsByProviderAll( + p.getName()); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private ItemStack makeItem( + Map t) { + int id = nv(t, "id"); + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = + sv(t, "requester"); + String title = sv(t, "title"); + int reward = nv(t, "reward_amount"); + Material mat; + switch (status) { + case "processing": + mat = Material.YELLOW_DYE; + break; + case "replied": + mat = Material.CYAN_DYE; + break; + case "completed": + mat = Material.LIME_DYE; + break; + case "resolved": + mat = Material.LIME_DYE; + break; + case "closed": + mat = Material.GRAY_DYE; + break; + case "rejected": + mat = Material.RED_DYE; + break; + case "withdrawn": + mat = Material.GRAY_DYE; + break; + default: + mat = Material.ORANGE_DYE; + break; + } + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName("§e#" + id + + " " + (title.isEmpty() + ? type : title)); + List lore = + new ArrayList<>(); + lore.add("§7类型: §f" + type); + lore.add("§7状态: " + + stxt(status)); + lore.add("§7提交者: §f" + + requester); + lore.add("§7奖励: §e" + reward + + " 积分"); + lore.add(""); + lore.add("§7点击查看详情"); + im.setLore(lore); + im.setCustomModelData(id); + item.setItemMeta(im); + } + return item; + } + + private int gt(ItemStack item) { + if (item == null) return -1; + ItemMeta im = item.getItemMeta(); + if (im == null) return -1; + if (im.hasCustomModelData()) + return im.getCustomModelData(); + return -1; + } + + private int gtFromDetail(Player p) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(10); + return gt(it); + } + + private String stxt(String status) { + switch (status) { + case "pending": + return "§e待处理"; + case "processing": + return "§b处理中"; + case "replied": + return "§9已回复"; + case "completed": + return "§a待确认"; + case "resolved": + return "§a已完结"; + case "closed": + return "§7已关闭"; + case "rejected": + return "§c已驳回"; + case "withdrawn": + return "§7已撤回"; + default: + return "§7" + status; + } + } + + private String sv(Map m, + String key) { + Object v = m.get(key); + return v != null + ? String.valueOf(v) : ""; + } + + private int nv(Map m, + String key) { + Object v = m.get(key); + return v instanceof Number + ? ((Number) v).intValue() : 0; + } + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) { + if (!l.isEmpty()) + list.add(l); + } + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src/main/java/Sdf1_login/UpdateChecker.java b/backup/src/main/java/Sdf1_login/UpdateChecker.java new file mode 100644 index 0000000..c5c01bb --- /dev/null +++ b/backup/src/main/java/Sdf1_login/UpdateChecker.java @@ -0,0 +1,523 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = + "ypsdf1"; + private static final String GITHUB_REPO = + "sdf1_login"; + private static final String GITEE_OWNER = + "nihaoshidifu"; + private static final String GITEE_REPO = + "sdf1_login"; + + + // API 地址 + private static final String GITHUB_API = + "https://api.github.com/repos/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + + "/tags"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + + GITEE_OWNER + "/" + + GITEE_REPO + + "/tags"; + + // 仓库页面链接(给用户看的) + private static final String GITHUB_LINK = + "https://github.com/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + + GITEE_OWNER + "/" + + GITEE_REPO + "/releases"; + + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 3000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = + false; + private volatile int failCount = 0; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = + false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = + plugin.getDescription() + .getVersion(); + this.disabledFile = new File( + plugin.getDataFolder(), + "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = + new BufferedReader( + new FileReader( + disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = + "true".equals( + line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = + new PrintWriter( + new FileWriter( + disabledFile))) { + pw.println(autoCheckDisabled + ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) return; + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doCheck(null, true)); + } + + /** + * /sdf1_login update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + * 只有拿到版号才解除熔断 + */ + public void checkUpdate( + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> + doCheck(sender, + false)); + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, + boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled + || circuitTripped) { + return; + } + } + + // 通道1:GitHub + CheckResult gh = + tryChannel(GITHUB_API, + "GitHub", GITHUB_LINK); + + // 通道2:Gitee + CheckResult ge = + tryChannel(GITEE_API, + "Gitee", GITEE_LINK); + + // 选择成功的结果 + CheckResult chosen = null; + String failMsg = null; + + if (gh.isSuccess()) { + chosen = gh; + } else if (ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + // 拼接两个通道的失败原因 + StringBuilder sb = + new StringBuilder(); + if (gh.error != null) { + sb.append("§7GitHub: §c") + .append(gh.error); + } + if (ge.error != null) { + if (sb.length() > 0) + sb.append("\n"); + sb.append("§7Gitee: §c") + .append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_login] 更新检测" + + "已熔断,连续失败" + + failCount + "次"); + } + if (sender != null) { + final String msg = + sb.toString(); + final boolean tripped = + circuitTripped; + final int fc = failCount; + Bukkit.getScheduler() + .runTask(plugin, () -> { + sender.sendMessage( + "§c§l[更新] " + + "检测失败"); + sender.sendMessage(msg); + sender.sendMessage( + "§e失败次数: " + + fc + "/" + + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage( + "§c已熔断," + + "请重启服务器" + + "重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_login] 远程版本信号" + + " \"" + chosen.version + + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler() + .runTask(plugin, () -> + sender.sendMessage( + "§e[更新] 远程已" + + "禁用自动" + + "更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + Bukkit.getScheduler() + .runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage( + "§e[更新] 检测成功" + + " via: §a" + src); + sender.sendMessage( + "§e[更新] 本地版本:" + + " §f" + + currentVersion); + sender.sendMessage( + "§e[更新] 远程版本:" + + " §f" + rv); + + if (isNewer(rv, + currentVersion)) { + sender.sendMessage( + "§c[更新] 发现新" + + "版本: " + rv); + sender.sendMessage( + "§a请前往仓库下载" + + "更新:"); + sender.sendMessage( + "§a" + lnk); + } else { + sender.sendMessage( + "§a[更新] 已是" + + "最新版本"); + } + + // 显示另一通道的失败原因 + // (仅信息,不影响结果) + CheckResult other = + (src.equals("GitHub")) + ? ge : gh; + if (!other.isSuccess() + && other.error != null) { + sender.sendMessage( + "§7[更新] " + + other.source + + "不可用: " + + other.error); + } + }); + } + + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal( + String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) + || "关闭".equals(v) + || "false".equals(v) + || "禁用".equals(v); + } + + // ========== HTTP 请求(带超时) ========== + + // ========== 检测结果 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, + String source, + String link, + String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + +// ========== HTTP 请求 ========== + + private CheckResult tryChannel( + String apiUrl, + String sourceName, + String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) + url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; " + + "Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty( + "Accept", + "*/*"); + conn.setRequestProperty( + "Accept-Language", + "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": HTTP " + code); + } + + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn + .getInputStream())); + StringBuilder sb = + new StringBuilder(); + String line; + while ((line = reader.readLine()) + != null) { + sb.append(line); + } + reader.close(); + + String tag = + parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 无法解析版本号"); + } + + return new CheckResult(tag, + sourceName, link, null); + + } catch (java.net + .SocketTimeoutException e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 连接被拒绝"); + } catch (java.net + .UnknownHostException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": " + + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + + + /** + * 从 JSON 中提取 tag_name + */ + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + // 尝试 tag_name(releases格式) + int idx = + json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + // 尝试 "name"(tags格式) + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") + || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + + // ========== 版本比较 ========== + + private boolean isNewer(String remote, + String local) { + String[] r = remote.split("\\."); + String[] l = local.split("\\."); + int len = Math.max(r.length, l.length); + for (int i = 0; i < len; i++) { + int ri = i < r.length + ? parseNum(r[i]) : 0; + int li = i < l.length + ? parseNum(l[i]) : 0; + if (ri > li) return true; + if (ri < li) return false; + } + return false; + } + + private int parseNum(String s) { + try { + return Integer.parseInt( + s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/backup/src/main/java/Sdf1_login/UserGroupManager.java b/backup/src/main/java/Sdf1_login/UserGroupManager.java new file mode 100644 index 0000000..adaf907 --- /dev/null +++ b/backup/src/main/java/Sdf1_login/UserGroupManager.java @@ -0,0 +1,370 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.util.*; +import java.util.Base64; +import java.util.concurrent.ConcurrentHashMap; + +public class UserGroupManager { + + private final Main plugin; + private Connection db; + private final Map groupConfigs = new ConcurrentHashMap<>(); + + public UserGroupManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadGroupConfigs(); + startExpiryChecker(); + } + + // ===== DB ===== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(plugin.getDataFolder(), "usergroup.db"); + db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_config (" + + "group_name TEXT PRIMARY KEY," + + "display_name TEXT DEFAULT ''," + + "display_color TEXT DEFAULT '§f'," + + "display_emoji TEXT DEFAULT ''," + + "priority INTEGER DEFAULT 0," + + "is_permanent INTEGER DEFAULT 1," + + "duration_minutes INTEGER DEFAULT 0," + + "pack_url TEXT DEFAULT ''," + + "pack_hash TEXT DEFAULT ''," + + "pack_prompt TEXT DEFAULT 'Custom Resource Pack'," + + "pack_size INTEGER DEFAULT 0)"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_member (" + + "player_name TEXT NOT NULL," + + "group_name TEXT NOT NULL," + + "added_by TEXT DEFAULT 'system'," + + "added_time INTEGER DEFAULT 0," + + "expiry_time INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, group_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e); + } + } + + // ===== 配置加载 ===== + + public void loadGroupConfigs() { + groupConfigs.clear(); + File dir = new File(plugin.getDataFolder(), "用户组"); + if (!dir.exists()) dir.mkdirs(); + File[] files = dir.listFiles((d, n) -> n.endsWith(".txt")); + if (files == null) return; + + for (File f : files) { + try { + List lines = java.nio.file.Files.readAllLines( + f.toPath(), StandardCharsets.UTF_8); + UserGroupConfig cfg = parseGroupConfig(lines); + if (cfg != null) { + groupConfigs.put(cfg.name, cfg); + saveGroupConfigToDB(cfg); + } + } catch (Exception e) { + plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName()); + } + } + } + + private UserGroupConfig parseGroupConfig(List lines) { + UserGroupConfig cfg = new UserGroupConfig(); + boolean inBlock = false; + + for (String raw : lines) { + String l = raw; + + // 块注释 + if (l.contains("")) { inBlock = false; continue; } + if (l.contains("/*")) { inBlock = true; continue; } + if (l.contains("*/")) { inBlock = false; continue; } + if (inBlock) continue; + + // 行注释 + l = l.trim(); + if (l.isEmpty()) continue; + if (l.startsWith("#") || l.startsWith("//")) continue; + + // 行内注释 + int h = l.indexOf('#'); + int s = l.indexOf("//"); + if (h >= 0) l = l.substring(0, h).trim(); + if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim(); + if (l.isEmpty()) continue; + + // 解析键值对 + String key = l; + String val = ""; + int idx = l.indexOf('\uff1a'); + if (idx < 0) idx = l.indexOf(':'); + if (idx >= 0) { + key = l.substring(0, idx).trim(); + val = l.substring(idx + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) + val = val.substring(1, val.length() - 1); + } + + switch (key) { + case "组名": + case "name": cfg.name = val; break; + case "显示名": + case "display": cfg.displayName = val; break; + case "颜色": + case "color": cfg.displayColor = val; break; + case "符号": + case "emoji": cfg.displayEmoji = val; break; + case "优先级": + case "priority": + try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "类型": + case "type": + cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent"); + break; + case "时长": + case "duration": + try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "资源包URL": + case "pack_url": cfg.packUrl = val; break; + case "资源包Hash": + case "pack_hash": cfg.packHash = val; break; + case "资源包提示": + case "pack_prompt": cfg.packPrompt = val; break; + case "资源包大小": + case "pack_size": + try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "本地文件": + case "local_file": cfg.localFile = val; break; + } + } + + if (cfg.name == null || cfg.name.isEmpty()) return null; + return cfg; + } + + private void saveGroupConfigToDB(UserGroupConfig cfg) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_config " + + "(group_name, display_name, display_color, display_emoji," + + " priority, is_permanent, duration_minutes," + + " pack_url, pack_hash, pack_prompt, pack_size)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, cfg.name); + ps.setString(2, cfg.displayName); + ps.setString(3, cfg.displayColor); + ps.setString(4, cfg.displayEmoji); + ps.setInt(5, cfg.priority); + ps.setInt(6, cfg.permanent ? 1 : 0); + ps.setInt(7, cfg.durationMinutes); + ps.setString(8, cfg.packUrl); + ps.setString(9, cfg.packHash); + ps.setString(10, cfg.packPrompt); + ps.setInt(11, cfg.packSize); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 玩家操作 ===== + + public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) { + UserGroupConfig cfg = groupConfigs.get(groupName); + if (cfg == null) return false; + long now = System.currentTimeMillis(); + long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000; + if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000; + else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_member " + + "(player_name, group_name, added_by, added_time, expiry_time)" + + " VALUES (?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, groupName); + ps.setString(3, addedBy); + ps.setLong(4, now); + ps.setLong(5, expiry); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + Player p = plugin.getServer().getPlayerExact(player); + if (p != null) sendResourcePack(p); + return true; + } + + public boolean removePlayer(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + int rows = ps.executeUpdate(); + ps.close(); + return rows > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public List> getPlayerGroups(String player) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM user_group_member WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + long now = System.currentTimeMillis(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("group_name", rs.getString("group_name")); + row.put("added_by", rs.getString("added_by")); + long expiry = rs.getLong("expiry_time"); + row.put("expiry_time", expiry); + row.put("expired", expiry > 0 && expiry <= now); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public UserGroupConfig getHighestGroup(String player) { + List> groups = getPlayerGroups(player); + long now = System.currentTimeMillis(); + UserGroupConfig best = null; + for (Map g : groups) { + long expiry = (long) g.get("expiry_time"); + if (expiry > 0 && expiry <= now) continue; + String name = (String) g.get("group_name"); + UserGroupConfig cfg = groupConfigs.get(name); + if (cfg != null && (best == null || cfg.priority > best.priority)) { + best = cfg; + } + } + return best; + } + + public boolean isPlayerInGroup(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + boolean exists = ps.executeQuery().next(); + ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ===== 过期自动移除 ===== + + private void startExpiryChecker() { + new BukkitRunnable() { + @Override + public void run() { + checkExpiry(); + } + }.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查 + } + + private void checkExpiry() { + try { + long now = System.currentTimeMillis(); + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, group_name FROM user_group_member " + + "WHERE expiry_time > 0 AND expiry_time <= ?"); + ps.setLong(1, now); + ResultSet rs = ps.executeQuery(); + List expired = new ArrayList<>(); + while (rs.next()) { + expired.add(new String[]{ + rs.getString("player_name"), + rs.getString("group_name")}); + } + rs.close(); ps.close(); + + for (String[] e : expired) { + PreparedStatement del = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + del.setString(1, e[0]); + del.setString(2, e[1]); + del.executeUpdate(); + del.close(); + plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 资源包分发 ===== + + public void sendResourcePack(Player p) { + UserGroupConfig cfg = getHighestGroup(p.getName()); + if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return; + + String url = cfg.packUrl; + String prompt = cfg.packPrompt != null + ? cfg.packPrompt : "Custom Resource Pack"; + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + try { + p.setResourcePack(url, new byte[0], prompt, false); + plugin.getLogger().info("[UserGroup] 资源包已发送: " + + p.getName() + " -> " + cfg.displayName); + } catch (Exception e) { + plugin.getLogger().warning( + "[UserGroup] 资源包发送失败: " + e.getMessage()); + } + }, 40L); + } + + public Map getGroupConfigs() { + return groupConfigs; + } + + // ===== 配置类 ===== + + public static class UserGroupConfig { + public String name; + public String displayName = ""; + public String displayColor = "§f"; + public String displayEmoji = ""; + public int priority = 0; + public boolean permanent = true; + public int durationMinutes = 0; + public String packUrl = ""; + public String packHash = ""; + public String packPrompt = "Custom Resource Pack"; + public int packSize = 0; + public String localFile = ""; + } +} diff --git a/backup/src/main/java/Sdf1_login/VerificationManager.java b/backup/src/main/java/Sdf1_login/VerificationManager.java new file mode 100644 index 0000000..3fc4ceb --- /dev/null +++ b/backup/src/main/java/Sdf1_login/VerificationManager.java @@ -0,0 +1,158 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.function.Consumer; + +public class VerificationManager { + + private final Main plugin; + + public VerificationManager(Main plugin) { + this.plugin = plugin; + } + + public boolean isBedrockPlayer(Player p) { + String uuid = p.getUniqueId().toString(); + return uuid.contains("f851ba4") + || uuid.startsWith("00000000-0000-0000"); + } + + public boolean isOnlineMode() { + return Bukkit.getServer().getOnlineMode(); + } + + public void verifyPremiumAsync(Player player, + Consumer callback) { + Thread t = new Thread(() -> doVerify(player, callback), + "Sdf1_login-PremiumVerify"); + t.setDaemon(true); + t.start(); + } + + private void doVerify(Player player, + Consumer callback) { + String name = player.getName(); + String uuid = player.getUniqueId().toString(); + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证开始 ======"); + plugin.getLogger().info( + "[Sdf1_login] 玩家: " + name); + plugin.getLogger().info( + "[Sdf1_login] 当前UUID: " + uuid); + + try { + UUID offlineUuid = UUID.nameUUIDFromBytes( + ("OfflinePlayer:" + name) + .getBytes(StandardCharsets.UTF_8)); + String offlineStr = offlineUuid.toString(); + + plugin.getLogger().info( + "[Sdf1_login] 离线UUID: " + offlineStr); + + boolean isOfflineUuid = + player.getUniqueId().equals(offlineUuid); + + plugin.getLogger().info( + "[Sdf1_login] UUID匹配离线格式: " + + isOfflineUuid); + + if (!isOfflineUuid) { + plugin.getLogger().info( + "[Sdf1_login] 结论: 正版玩家"); + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(true); + return; + } + + plugin.getLogger().info( + "[Sdf1_login] UUID为离线格式," + + "查询Mojang API..."); + UUID mojangUuid = fetchMojangUuid(name); + + if (mojangUuid != null) { + plugin.getLogger().info( + "[Sdf1_login] Mojang返回UUID: " + + mojangUuid); + plugin.getLogger().info( + "[Sdf1_login] 结论: 盗版客户端" + + "使用了正版名字"); + } else { + plugin.getLogger().info( + "[Sdf1_login] Mojang无此名字"); + plugin.getLogger().info( + "[Sdf1_login] 结论: 纯离线玩家"); + } + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(false); + + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 验证异常: " + + e.getMessage()); + callback.accept(false); + } + } + + private UUID fetchMojangUuid(String name) { + HttpURLConnection conn = null; + try { + URL url = new URL( + "https://api.mojang.com/users/profiles/minecraft/" + + name); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setRequestMethod("GET"); + + int code = conn.getResponseCode(); + plugin.getLogger().info( + "[Sdf1_login] Mojang API响应: " + code); + + if (code == 200) { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn.getInputStream(), + StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String json = sb.toString(); + String uuidStr = json.replaceAll( + ".*\"id\"\\s*:\\s*\"([^\"]+)\".*", + "$1"); + plugin.getLogger().info( + "[Sdf1_login] Mojang原始UUID: " + + uuidStr); + + String formatted = uuidStr.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5"); + return UUID.fromString(formatted); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] Mojang查询异常: " + + e.getMessage()); + } finally { + if (conn != null) conn.disconnect(); + } + return null; + } +} diff --git a/backup/src/main/java/Sdf1_login/WelcomeManager.java b/backup/src/main/java/Sdf1_login/WelcomeManager.java new file mode 100644 index 0000000..68bd3ef --- /dev/null +++ b/backup/src/main/java/Sdf1_login/WelcomeManager.java @@ -0,0 +1,163 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class WelcomeManager { + + private final Main plugin; + private final Random random = new Random(); + private final List registerMsgs = + new ArrayList<>(); + private final List loginMsgs = + new ArrayList<>(); + + public WelcomeManager(Main plugin) { + this.plugin = plugin; + loadMessages(); + } + + public void loadMessages() { + registerMsgs.clear(); + loginMsgs.clear(); + File f = new File( + plugin.getDataFolder(), + "欢迎仪式.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("注册") + || t.equals("注册:")) { + section = "register"; + continue; + } + if (t.equals("登录") + || t.equals("登录:")) { + section = "login"; + continue; + } + // 支持 = 和 : 两种分隔 + if (section.isEmpty()) { + if (t.startsWith("注册") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + registerMsgs.add( + colorize(v)); + continue; + } + if (t.startsWith("登录") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + loginMsgs.add( + colorize(v)); + continue; + } + continue; + } + if (!t.isEmpty()) { + if (section.equals("register")) + registerMsgs.add( + colorize(t)); + else + loginMsgs.add( + colorize(t)); + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[欢迎] 加载失败: " + + e.getMessage()); + } + plugin.getLogger().info( + "[欢迎] 加载完成: 注册=" + + registerMsgs.size() + + " 登录=" + loginMsgs.size()); + } + + public void onRegister(Player p) { + sendRandom(p, registerMsgs, "注册"); + } + + public void onLogin(Player p) { + sendRandom(p, loginMsgs, "登录"); + } + + private void sendRandom(Player p, + List msgs, String tag) { + if (msgs.isEmpty()) return; + String msg = msgs.get( + random.nextInt(msgs.size())); + msg = msg.replace("{player}", + p.getName()); + final String finalMsg = msg; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> Bukkit + .broadcastMessage( + finalMsg), 10L); + } + + private String colorize(String msg) { + return msg.replace("&", "\u00a7"); + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets + .UTF_8))) { + pw.println("# 欢迎仪式配置"); + pw.println("# &为颜色代码"); + pw.println("# {player} = 玩家名"); + pw.println(); + pw.println("注册:"); + pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]"); + pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]"); + pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]"); + pw.println(); + pw.println("登录:"); + pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]"); + pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]"); + pw.println("&b&l{player} &b回来了!&7[登录]"); + } catch (IOException ignored) { + } + } +} diff --git a/backup/src/main/resources/plugin.yml b/backup/src/main/resources/plugin.yml new file mode 100644 index 0000000..f6868d9 --- /dev/null +++ b/backup/src/main/resources/plugin.yml @@ -0,0 +1,104 @@ +name: Sdf1_login +version: 1.0 +main: Sdf1_login.Main +api-version: '1.21' +commands: + sdf1_login: + description: Sdf1_login主命令 + usage: /sdf1_login + reg: + description: 注册 + usage: /reg <密码> + login: + description: 登录 + usage: /login <密码> + l: + description: 登录快捷版 + usage: /l <密码> + 注册: + description: 注册(中文) + usage: /注册 <密码> + 登录: + description: 登录(中文) + usage: /登录 <密码> + 玩家信息: + description: 打开面板(中文) + usage: /玩家信息 + 改密码: + description: 修改密码(中文) + usage: /改密码 <旧密码> <新密码> <确认新密码> + 设置邮箱: + description: 设置邮箱(中文) + usage: /设置邮箱 <邮箱地址> + 撤销: + description: 撤销操作(中文) + usage: /撤销 + 删除: + description: 删除玩家(中文) + usage: /删除 <玩家> <密码> + 找回密码: + description: 找回密码(中文) + usage: /找回密码 + 签到: + description: 每日签到(中文) + usage: /签到 + #oa: + # description: 申请删除账号 + # usage: /oa [玩家名] + 删除账号: + description: 申请删除账号(中文) + usage: /删除账号 [玩家名] + 通过: + description: 批准OA请求(中文) + usage: /通过 <编号> + 拒绝: + description: 拒绝OA请求(中文) + usage: /拒绝 <编号> [理由] + accept: + description: 批准OA请求 + usage: /accept <编号> + deny: + description: 拒绝OA请求 + usage: /deny <编号> [理由] + email: + description: 绑定邮箱 + usage: /email <邮箱> + 绑定: + description: 绑定邮箱(中文) + usage: /绑定 <邮箱> + 绑定邮箱: + description: 绑定邮箱 + usage: /绑定邮箱 + chat: + description: 聊天过滤管理 + usage: /chat + recycle: + description: 打开垃圾回收站 + usage: /recycle + 垃圾清理: + description: 手动清理掉落物 + usage: /垃圾清理 + menu: + description: 雪球菜单开关控制 + usage: /menu <开/关> + pvp : + description: PVP榜单 + usage: /pvp + cypay : + description: 草原探险债券 + usage: /cypay + printer : + description: 打印债券流水 + usage: /printer + shop : + description: 游戏商人 + usage: /shop + 商店: + description: 打开商店 + usage: /商店 + #testorder: + # description: 测试订单中心 + # usage: /testorder + protect: + description: 区域保护 + usage: /protect \ No newline at end of file diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/AFKManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AFKManager.java new file mode 100644 index 0000000..f714c1b --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AFKManager.java @@ -0,0 +1,197 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; + +public class AFKManager { + + // 玩家最后活动时间(空闲才开始计时) + private final Map lastAction = + new HashMap<>(); + // 每个玩家当前的警告阶段 + private final Map lastStage = + new HashMap<>(); + // 是否已进入挂机检测(false=还在活动,不计时) + private final Map afkTracking = + new HashMap<>(); + + private final ConfigManager config; + private final JavaPlugin plugin; + + public AFKManager(ConfigManager config, + JavaPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public void startCheck() { + new BukkitRunnable() { + public void run() { + tick(); + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void tick() { + if (!config.afkEnabled) return; + long now = System.currentTimeMillis(); + + for (Player p : Bukkit.getOnlinePlayers()) { + String name = p.getName(); + UUID uuid = p.getUniqueId(); + + if (!isLoggedIn(name)) continue; + if (config.getAfkWhitelist().contains(name)) + continue; + + // 确保有记录 + if (!lastAction.containsKey(uuid)) { + lastAction.put(uuid, now); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + long last = lastAction.get(uuid); + long idle = (now - last) / 1000; + int timeout = config.afkTimeout; + + // 还在活动范围内:不进入挂机检测 + if (idle < 60) { + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + continue; + } + + // 超过60秒无活动:开始挂机检测 + afkTracking.put(uuid, true); + long remain = timeout - idle; + + // 已超时:踢出 + if (remain <= 0) { + p.kickPlayer("§c§l你已挂机 " + + fmtSec(idle) + ",被踢出"); + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + continue; + } + + int stage = lastStage.getOrDefault(uuid, 0); + + // 阶段3:最后10秒 + if (remain <= 10) { + if (stage < 3) { + lastStage.put(uuid, 3); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + remain + " 秒踢出", + 0, 22, 3); + playSound(p); + continue; + } + + // 阶段2:1分钟~10秒 + if (remain <= 60) { + if (stage < 2) { + lastStage.put(uuid, 2); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + } + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + fmtSec(remain) + " 踢出", + 5, 25, 5); + playSound(p); + continue; + } + + // 阶段1:3分钟提醒 + if (remain <= 180 && stage < 1) { + lastStage.put(uuid, 1); + p.sendMessage( + "§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + + fmtSec(idle) + + "§f,还剩 §c" + + fmtSec(remain) + + "§f踢出"); + p.sendTitle( + "§e§l挂机了 " + fmtSec(idle), + "§f还剩 " + fmtSec(remain) + " 踢出", + 10, 60, 20); + for (int i = 0; i < 3; i++) { + final int delay = i * 10; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + if (p.isOnline()) playSound(p); + }, delay); + } + } + + if (remain > 180) { + lastStage.remove(uuid); + } + } + } + + /** + * 玩家有活动时调用(移动/交互/放置/挖掘等) + * 重置计时,退出挂机检测 + */ + public void recordAction(UUID uuid) { + lastAction.put(uuid, System.currentTimeMillis()); + afkTracking.put(uuid, false); + lastStage.put(uuid, 0); + } + + /** + * 玩家下线时调用 + * 彻底停止计时,清理所有数据 + */ + public void remove(UUID uuid) { + lastAction.remove(uuid); + lastStage.remove(uuid); + afkTracking.remove(uuid); + } + + /** + * 获取玩家当前是否在挂机检测中 + */ + public boolean isAfkTracking(UUID uuid) { + return afkTracking.getOrDefault(uuid, false); + } + + private boolean isLoggedIn(String name) { + return ((Main) plugin).getLoggedIn() + .contains(name); + } + + private void playSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private String fmtSec(long sec) { + if (sec < 60) return sec + "秒"; + long m = sec / 60; + long s = sec % 60; + return m + "分" + (s > 0 ? s + "秒" : ""); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/AccountRequestManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AccountRequestManager.java new file mode 100644 index 0000000..ae5eaad --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AccountRequestManager.java @@ -0,0 +1,146 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AccountRequestManager { + + public enum ApprovalMode { AUTO, MANUAL, HYBRID } + + public static class Request { + public final int id; + public final String requester; + public final String target; + public final String ip; + public final long time; + public String status; + public String denyReason; + + public Request(int id, String requester, String target, String ip) { + this.id = id; + this.requester = requester; + this.target = target; + this.ip = ip; + this.time = System.currentTimeMillis(); + this.status = "PENDING"; + this.denyReason = ""; + } + } + + private final Main plugin; + private final Map requests = new ConcurrentHashMap<>(); + private final Map pendingByPlayer = new ConcurrentHashMap<>(); + private int nextId = 1; + private ApprovalMode mode = ApprovalMode.MANUAL; + private int autoDelayMinutes = 60; + + public AccountRequestManager(Main plugin) { + this.plugin = plugin; + } + + public ApprovalMode getMode() { return mode; } + + public void setMode(String m) { + try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } + catch (Exception e) { this.mode = ApprovalMode.MANUAL; } + } + + public void setAutoDelayMinutes(int min) { + this.autoDelayMinutes = min; + } + + public Request createRequest(String requester, String target, String ip) { + int id = nextId++; + Request req = new Request(id, requester, target, ip); + requests.put(id, req); + pendingByPlayer.put(requester, id); + + if (mode == ApprovalMode.AUTO) { + scheduleAutoApprove(id); + } else if (mode == ApprovalMode.HYBRID) { + String targetIp = (String) plugin.getDb().getField(target, "ip_address"); + if (ip != null && ip.equals(targetIp)) { + scheduleAutoApprove(id); + } + } + return req; + } + + private void scheduleAutoApprove(int id) { + long delayTicks = (long) autoDelayMinutes * 60L * 20L; + Request req = requests.get(id); + if (req == null) return; + String requesterName = req.requester; + String targetName = req.target; + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Request r = requests.get(id); + if (r != null && "PENDING".equals(r.status)) { + approveRequest(id); + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); + Player requester = Bukkit.getPlayer(requesterName); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); + } + } + }, delayTicks); + } + + public boolean approveRequest(int id) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "APPROVED"; + plugin.getDb().deleteUser(req.target); + pendingByPlayer.remove(req.requester); + // 清背包+传送出生点+踢出 + Player target = Bukkit.getPlayer(req.target); + if (target != null && target.isOnline()) { + target.getInventory().clear(); + target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); + target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); + target.teleport(target.getWorld().getSpawnLocation()); + target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); + } + return true; + } + + + public boolean denyRequest(int id, String reason) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "DENIED"; + req.denyReason = reason; + pendingByPlayer.remove(req.requester); + return true; + } + + public List getPendingRequests() { + List list = new ArrayList<>(); + for (Request r : requests.values()) { + if ("PENDING".equals(r.status)) list.add(r); + } + return list; + } + + public Request getRequest(int id) { + return requests.get(id); + } + + public boolean hasPending(String player) { + return pendingByPlayer.containsKey(player); + } + + public void cancelPending(String player) { + Integer id = pendingByPlayer.remove(player); + if (id != null) { + Request r = requests.get(id); + if (r != null) r.status = "CANCELLED"; + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/AreaProtection.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AreaProtection.java new file mode 100644 index 0000000..bc53d53 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/AreaProtection.java @@ -0,0 +1,4907 @@ +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.entity.Entity; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import java.sql.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +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 java.util.Set; +import java.util.HashSet; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.entity.Item; + + + + +public class AreaProtection implements Listener { + + 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 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<>(); + + // 选地 + private static final Material WAND = Material.BREEZE_ROD; + + private final Map pos1 = + new ConcurrentHashMap<>(); + private final Map pos2 = + new ConcurrentHashMap<>(); + private final Set selecting = + ConcurrentHashMap.newKeySet(); + 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<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + + + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + 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 (p.isOp()) { + plugin.getLogger().info( + "[防护-调试] 豁免原因: OP, 玩家=" + name); + return true; + }*/ + if (globalPlayerWhitelist.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 全局白名单, 玩家=" + + name);*/ + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (aw != null && aw.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 区域白名单, 玩家=" + + name + " 区域=" + areaName);*/ + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 模式排除名单, 玩家=" + + name);*/ + return true; + } + /* plugin.getLogger().info( + "[防护-调试] 无豁免, 玩家=" + name);*/ + 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(); + loadAllAreas(); + loadWhitelists(); + recoverPendingEffects(); + } + 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); + } + + +// ==================== 数据库迁移 ==================== + + /** + * 如果旧位置有 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("========================================"); + } + +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // & → § + 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", "\n"); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + int i = 0; + + while (i < msg.length()) { + 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(); + if (!stack.isEmpty()) { + // 重置后重新应用外层格式 + sb.append("§r"); + for (char c : stack) { + sb.append("§").append(c); + } + } else { + sb.append("§r"); + } + } + 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": return 'l'; + case "i": return 'o'; + case "u": return 'n'; + case "s": return 'm'; + default: return 0; + } + } + +// =============== 加载 ==================== + + public void loadAllAreas() { + areas.clear(); + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); + if (files == null) return; + for (File f : files) { + try { + String name = f.getName() + .replace(".txt", ""); + AreaConfig ac = parseArea(name, f); + areas.put(name, ac); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载失败: " + f.getName()); + } + } + } + + + 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)"); + stmt.close(); + } catch (SQLException e) { + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 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 == '\u200B' // 零宽空格 + || c == '\u200C' // 零宽非连接 + || c == '\u200D' // 零宽连接 + || c == '\uFEFF' // BOM + || c == '\u00A0' // 不间断空格 + || c == '\u2028' // 行分隔符 + || c == '\u2029' // 段分隔符 + || c == '\u200E' // LTR标记 + || c == '\u200F' // RTL标记 + || c == '\u2060' // 字连接符 + || (c >= '\u2000' && c <= '\u200A')) { // 各种空格 + continue; + } + + // 全角→半角映射 + if (c >= '\uFF01' && c <= '\uFF5E') { + c = (char) (c - 0xFEE0); + } + + // 全角空格→半角空格 + if (c == '\u3000') { + 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.denyDrop = 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("和平模式") + && !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() + 5000); + + /* 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); + /* plugin.getLogger().info("[和平] " + p.getName() + + " 附近实体总数=" + nearby.size());*/ + for (Entity ent : nearby) { + String typeName = ent.getType().name(); + /* plugin.getLogger().info("[和平] 实体: " + + typeName + " isHostile=" + isHostile(typeName));*/ + } + int scanned = 0; + int waiting = 0; + int saved = 0; + int banished = 0; + + for (Entity ent : p.getNearbyEntities(48, 48, 48)) { + if (!isHostile(ent.getType().name())) continue; + scanned++; + + UUID entUid = ent.getUniqueId(); + Long protectUntil = protectedEntities.get(entUid); + String customName = ent.getCustomName(); + + // 有保护期记录 + if (protectUntil != null) { + long remain = protectUntil - now; + + if (remain > 0) { + // 保护期内,精确剩余秒数 + int secondsLeft = (int) Math.ceil(remain / 1000.0); + waiting++; + + // 保护期内也检查名字(玩家可能刚贴上) + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + protectedEntities.remove(entUid); + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 白名单匹配,永久保留");*/ + continue; + } + + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 保护期剩余约" + secondsLeft + "秒");*/ + continue; + } + + // 保护期到了,最终检查 + protectedEntities.remove(entUid); + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + " 5秒到期");*/ + + // 到期时再查一次白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 到期但白名单匹配,保留");*/ + continue; + } + + // 没名字或不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名=[" + customName + "] 传送虚空");*/ + continue; + } + + // 没有保护期记录(非刷怪蛋生成的或记录被清了) + // 也检查一下白名单 + if (customName != null + && !customName.isEmpty() + && ac.peaceWhitelist.contains(customName)) { + saved++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 命名[" + customName + + "] 无保护期但白名单匹配,保留");*/ + continue; + } + + // 没有保护期且不在白名单,传送虚空 + Location voidLoc = ent.getLocation().clone(); + voidLoc.setY(ent.getWorld().getMinHeight() - 50); + ent.teleport(voidLoc); + banished++; + /* plugin.getLogger().info("[和平] " + + ent.getType().name() + + " 无保护期,命名=[" + customName + "] 传送虚空");*/ + } + + /* plugin.getLogger().info("[和平] " + + " 扫描=" + scanned + + " 等待=" + waiting + + " 保留=" + saved + + " 传送=" + banished + + " 白名单=" + ac.peaceWhitelist);*/ + } + + 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" + )); + + 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; + } + + // ==================== 白名单检查 ==================== + + private boolean isPlayerWhitelisted(String player, + AreaConfig ac) { + if (globalPlayerWhitelist.contains( + player.toLowerCase())) + return true; + Set areaList = + areaPlayerWhitelist.get(ac.name); + return areaList != null + && areaList.contains(player.toLowerCase()); + } + + 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 taskId = Bukkit.getScheduler() + .runTaskTimer(plugin, new Runnable() { + @Override + public void run() { + if (!p.isOnline()) return; + Location loc = p.getLocation(); + int px = loc.getBlockX(); + int py = loc.getBlockY(); + int pz = loc.getBlockZ(); + 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); + for (int dy = 5; dy <= 30; dy += 5) { + for (int x = minX; x <= maxX; x += 4) { + for (int z = minZ; z <= maxZ; z += 4) { + if (x == minX || x == maxX + || z == minZ || z == maxZ) { + p.spawnParticle(Particle.END_ROD, + x + 0.5, py + dy, z + 0.5, + 1, 0, 0, 0, 0); + } + } + } + } + } + }, 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; + + Location from = event.getFrom(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 防抖 + 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) { + // ★ 每次移动都清除指定效果 + clearBadEffects(p, 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); + + // 离开提示 + if (oldAc != null && oldAc.leaveMsg != null + && !oldAc.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(oldAc.leaveMsg)); + } + + // 收标 + 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 (marked != null && !marked.isEmpty()) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★创建延时任务: " + + toClear.size() + "个");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-移动-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + plugin.getLogger().info( + "[防护-移动-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size()); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + /* plugin.getLogger().info( + "[防护-移动-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] + cleared" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + 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) { + if (newAc.enterMsg != null + && !newAc.enterMsg.isEmpty()) { + p.sendMessage( + formatAreaMsg(newAc.enterMsg)); + } + 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()); + } + } + } + +// ===== 清除所有效果 ===== + if (newAc.clearAllBadEffects) { + for (PotionEffect eff : p.getActivePotionEffects()) { + p.removePotionEffect(eff.getType()); + } + plugin.getLogger().info( + "[防护] ★清除所有效果, 玩家=" + p.getName()); + } + + 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); + } + } +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + 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 || ac.giveEffects.isEmpty()) return; + if (ac.giveEffects.isEmpty()) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + for (String[] eff : ac.giveEffects) { + 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); + } + + // 给效果 + 贴标 + List applied = new ArrayList<>(); + for (String[] eff : ac.giveEffects) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + int lv = Integer.parseInt(eff[1]) - 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) { + } + } + + // 贴标 + // 新代码(合并,不覆盖): + 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());*/ + } + } + + /** + * 离开区域时,延时15秒清理效果 + * 流程:收标 → 15秒后清效果 → 确认清空 → 清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); + + // 启动15秒延时 + 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); + } + } + + // 确认清空后清DB + removePlayerEffects(uid, areaName); + plugin.getLogger().info( + "[防护] 15秒已到, 清除 " + + (marked != null ? marked.size() : 0) + + " 个效果 + DB, 玩家=" + + p.getName() + + " 区域=" + areaName); + } + }.runTaskLater(plugin, 300L); // 15秒 = 300 tick + + pendingClearTask.put(uid, task); + } + public Set getAllAreaNames() { + return areas.keySet(); + } + + /** + * 15秒内进了新区域: + * 清除旧区域有、新区域没有的效果 + */ + 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) { + for (String[] eff : newAc.giveEffects) { + 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 都跳过 + */ + private boolean isExemptFromConfiscation( + Player p, String areaName) { + String name = p.getName(); + + if (globalPlayerWhitelist.contains(name)) + return true; + + Set areaWl = + areaPlayerWhitelist.get(areaName); + if (areaWl != null && areaWl.contains(name)) + return true; + + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) + return true; + + return false; + } + + /** + * 没收玩家背包中的违禁品 + */ + private void confiscateItems(Player p, + AreaConfig ac) { + boolean confiscated = false; + + for (int i = 0; + i < p.getInventory().getSize(); i++) { + ItemStack item = p.getInventory().getItem(i); + if (item == null) continue; + + String typeName = item.getType().name(); + if (ac.confiscateItems + .contains(typeName)) { + p.getInventory().setItem(i, null); + confiscated = true; + } + } + + if (confiscated) { + String msg = ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty() + ? ac.confiscateMsg + : "§c你的违禁品已被没收"; + p.sendMessage(msg); + + // 通报批评 + if (ac.enableAnnounce + && ac.announceTemplate != null) { + String text = ac.announceTemplate + .replace("{player}", + p.getName()); + Bukkit.broadcastMessage(text); + } + } + } + + + + private String getChineseGameMode(GameMode mode) { + switch (mode) { + case SURVIVAL: return "生存模式"; + case CREATIVE: return "创造模式"; + case ADVENTURE: return "冒险模式"; + case SPECTATOR: return "旁观模式"; + default: return mode.name(); + } + } + + private void handleEnter(Player p, AreaConfig ac) { + UUID uid = p.getUniqueId(); + + // 清除负面效果 + clearBadEffects(p, ac); + + // 给予效果 + List given = new ArrayList<>(); + for (String[] parts : ac.giveEffects) { + 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) {} + PotionEffectType type = resolveEffectType(effName); + if (type != null) { + p.addPotionEffect(new PotionEffect( + type, duration * 20, level - 1)); + given.add(type); + } + } + + removePlayerEffects(uid, ac.name); + if (!given.isEmpty()) { + // savePlayerEffects(uid, p.getName(), ac.name, given); + } + + // 白名单检查(最先执行) + if (isPlayerWhitelisted(p.getName(), ac)) 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; + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return true; + } + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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; + + // 去掉"效果"后缀 + String clean = name; + if (clean.endsWith("效果")) { + clean = clean.substring(0, clean.length() - 2); + } + + // 硬编码中文名→英文ID映射(不依赖任何外部Map) + switch (clean) { + // 负面 + case "缓慢": + return resolveEffectType("slowness"); + case "挖掘疲劳": + return resolveEffectType("mining_fatigue"); + case "瞬间伤害": + return resolveEffectType("instant_damage"); + case "反胃": + return resolveEffectType("nausea"); + case "失明": + return resolveEffectType("blindness"); + case "饥饿": + return resolveEffectType("hunger"); + case "虚弱": + return resolveEffectType("weakness"); + case "中毒": + return resolveEffectType("poison"); + case "凋零": + return resolveEffectType("wither"); + case "飘浮": + return resolveEffectType("levitation"); + case "霉运": + return resolveEffectType("unluck"); + case "黑暗": + return resolveEffectType("darkness"); + case "蓄风": + return resolveEffectType("wind_charged"); + case "盘丝": + return resolveEffectType("weaving"); + case "渗浆": + return resolveEffectType("oozing"); + case "寄生": + return resolveEffectType("infested"); + // 中性 + case "不祥之兆": + case "不祥征兆": + return resolveEffectType("bad_omen"); + case "袭击之兆": + case "袭击征兆": + return resolveEffectType("raid_omen"); + case "试炼之兆": + case "试炼征兆": + return resolveEffectType("trial_omen"); + // 正面 + case "迅捷": + return resolveEffectType("speed"); + case "急迫": + return resolveEffectType("haste"); + case "力量": + return resolveEffectType("strength"); + case "瞬间治疗": + return resolveEffectType("instant_health"); + case "跳跃提升": + return resolveEffectType("jump_boost"); + case "生命恢复": + return resolveEffectType("regeneration"); + case "抗性提升": + return resolveEffectType("resistance"); + case "抗火": + return resolveEffectType("fire_resistance"); + case "水下呼吸": + return resolveEffectType("water_breathing"); + case "隐身": + return resolveEffectType("invisibility"); + case "夜视": + return resolveEffectType("night_vision"); + case "发光": + return resolveEffectType("glowing"); + case "生命提升": + return resolveEffectType("health_boost"); + case "伤害吸收": + return resolveEffectType("absorption"); + case "饱和": + return resolveEffectType("saturation"); + case "幸运": + return resolveEffectType("luck"); + case "村庄英雄": + return resolveEffectType("hero_of_the_village"); + case "缓降": + return resolveEffectType("slow_falling"); + case "潮涌能量": + return resolveEffectType("conduit_power"); + case "海豚的恩惠": + return resolveEffectType("dolphins_grace"); + case "致命中毒": + return resolveEffectType("poison"); + default: + break; + } + + +// 兜底:直接用原版英文ID查(★ 注意是 PotionEffectType.getByName,不是 resolveEffectType) + PotionEffectType fallback = PotionEffectType.getByName(clean); + if (fallback != null) return fallback; + fallback = PotionEffectType.getByName(clean.toLowerCase()); + if (fallback != null) return fallback; + + plugin.getLogger().warning("[防护] 无法识别效果: " + name); + 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); + } + + // 创建15秒延时清理 + if (marked != null && !marked.isEmpty()) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★创建延时任务: " + + toClear.size() + "个效果");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-传送-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); +/* + plugin.getLogger().info( + "[防护-传送-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size() + + " 区域=" + clearArea);*/ + + 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); + /* plugin.getLogger().info( + "[防护-传送-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] ★★★完成: 清除" + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 300L); + pendingClearTask.put(uid, task); + + } + + // 离开提示 + if (fromArea != null && fromArea.leaveMsg != null + && !fromArea.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(fromArea.leaveMsg)); + } + } + + // ===== 进入新区域 ===== + 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); + } + } + + // 进入提示 + if (toArea != null && toArea.enterMsg != null + && !toArea.enterMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(toArea.enterMsg)); + } + + // 发放效果 + if (toArea != null) { + /* plugin.getLogger().info( + "[防护-传送-进入] 调用applyRegionEffects");*/ + applyRegionEffects(p, uid, toName, toArea); +// 清除效果 + clearBadEffects(p, toArea); + + if (toArea.confiscateItems != null + && !toArea.confiscateItems.isEmpty() + && !isExemptFromConfiscation( + p, toName)) { + confiscateItems(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 (isPlayerWhitelisted(p.getName(), ac)) 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(); + } + + 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) { + for (String[] parts : ac.giveEffects) { + 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) {} + + 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 && ac.denyBlockPlace + && !isPlayerWhitelisted(p.getName(), ac)) { + 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 && ac.denyBlockBreak + && !isPlayerWhitelisted(p.getName(), ac)) { + 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; + // 保护性规则:对所有人无条件生效 + if (ac.denyAllDamage) { e.setCancelled(true); return; } + if (ac.denyFallDamage + && e.getCause() == EntityDamageEvent.DamageCause.FALL) + e.setCancelled(true); + } + + + @EventHandler + public void onPVP(EntityDamageByEntityEvent e) { + if (!(e.getDamager() instanceof Player)) return; + 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 && ac.denyPVP + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + + @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 && 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 && ac.denyDrop + && !isPlayerWhitelisted(p.getName(), ac)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止丢弃物品"); + } + } + + // 替换整个 onInteract 方法 + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack hand = p.getInventory().getItemInMainHand(); + if (hand == null) return; + + // ===== 选区工具 ===== + if (hand.getType() == Material.BREEZE_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); + 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 { + pos2.put(uid, block.getLocation()); + p.sendMessage("§a§l[防护] §f位置2: " + + block.getX() + ", " + + block.getY() + ", " + + block.getZ()); + } + } + return; + } + } + + // ===== 放置实体类物品时不检查(展示框、矿车、船、地图等)===== + Material mat = hand.getType(); + if (mat == Material.ITEM_FRAME + || mat == Material.GLOW_ITEM_FRAME + || 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.MAP + || mat == Material.ARMOR_STAND) { + return; + } + + // ===== 区域规则检查 ===== + AreaConfig ac = getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac == null) return; + + // 末影珍珠禁止 + if (ac.denyEnderPearl + && hand.getType() == Material.ENDER_PEARL) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用末影珍珠"); + return; + } + + // 弓箭禁止 + if (ac.denyBow + && (hand.getType() == Material.BOW + || hand.getType() == Material.CROSSBOW)) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止使用弓箭"); + return; + } + + // 白名单跳过 + if (isPlayerWhitelisted(p.getName(), ac)) return; + + // 禁止使用物品 + String typeId = hand.getType().name(); + if (ac.denyUseItems.contains(typeId)) { + 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(); + } + } + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + + 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("#")) { + 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("#")) { + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + 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 && ac.denyPotion + && !isPlayerWhitelisted(shooter.getName(), ac)) { + e.setCancelled(true); + shooter.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; + } + + /** + * 判断字符串是否是区域名(精准或模糊) + */ + 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", "创建", "删除", "重载", + "工具", "expand", "contraction", + "on", "off", "tempon", "modeexempt" + )); + + 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) { + showHelp(sender); + return true; + } + String sub = args[0].toLowerCase(); +// ===== 诊断命令 ===== + 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; + ItemStack tool = new ItemStack(WAND); + ItemMeta meta = tool.getItemMeta(); + meta.setDisplayName("§a§l区域选择工具"); + meta.setLore(Arrays.asList( + "§7左键: 位置1", "§7右键: 位置2")); + tool.setItemMeta(meta); + p.getInventory().addItem(tool); + p.sendMessage("§a§l[防护] §f已获取选择工具"); + return true; + } + + // ===== 创建 ===== + if (sub.equals("创建") || sub.equals("create")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 创建 <名>"); + return true; + } + Player p = (Player) sender; + UUID u = p.getUniqueId(); + if (!pos1.containsKey(u) + || !pos2.containsKey(u)) { + p.sendMessage("§c请先用工具选点"); + return true; + } + String areaName = args[1]; + File f = new File(rootDir, areaName + ".txt"); + if (f.exists()) { + p.sendMessage("§c区域已存在"); + return true; + } + Location l1 = pos1.get(u); + Location l2 = pos2.get(u); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 区域: " + areaName); + pw.println("起点: " + l1.getWorld().getName() + + "," + l1.getBlockX() + + "," + l1.getBlockZ()); + pw.println("终点: " + l2.getWorld().getName() + + "," + l2.getBlockX() + + "," + l2.getBlockZ()); + pw.println("高度范围: 0-255"); + pw.println(); + pw.println("# 禁止放置方块"); + pw.println("# 禁止破坏方块"); + pw.println("# 禁止PVP"); + pw.println("# 效果: 夜视 1 999"); + pw.println("# 进入提示: 欢迎来到保护区"); + pw.println("# 离开提示: 已离开保护区"); + pw.close(); + p.sendMessage("§a§l[防护] §f区域 " + + areaName + " 已创建"); + loadAllAreas(); + } catch (IOException ex) { + p.sendMessage("§c创建失败"); + } + 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; + } + ac.peaceWhitelist.add(parsed[1]); + saveAreaConfig(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; + } + ac.peaceWhitelist.remove(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.add(parsed[1]); + saveAreaConfig(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; + } + ac.modeExempt.remove(parsed[1]); + saveAreaConfig(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; + } + + // ===== 重载 ===== + if (sub.equals("重载") || sub.equals("reload")) { + reload(); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); + return true; + } + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { + if (args.length == 2) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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; + } + + // ===== 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; + } + 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); + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格"); + 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; + } + 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); + 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; + } + ac.x1 = minX; + ac.x2 = maxX; + ac.z1 = minZ; + ac.z2 = maxZ; + saveAreaConfig(ac); + p.sendMessage("§a§l[防护] §f区域 §e" + ac.name + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格"); + return true; + } + + // ===== 删除 ===== + if (sub.equals("删除") || sub.equals("delete")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /protect 删除 <名>"); + return true; + } + File f = new File(rootDir, args[1] + ".txt"); + if (f.exists()) { + f.delete(); + areas.remove(args[1]); + sender.sendMessage("§a已删除: " + args[1]); + } else { + sender.sendMessage("§c不存在: " + args[1]); + } + return true; + } + + // ===== add 加白名单 ===== + if (sub.equals("add")) { + if (args.length == 2) { + String name = args[1]; + 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]; + 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) { + String name = args[1]; + if (!globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); + return true; + } + globalPlayerWhitelist.remove(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]; + if (areaName.equalsIgnoreCase("global")) { + globalPlayerWhitelist.remove(playerName); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); + } else { + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + wl.remove(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; + } + + showHelp(sender); + return true; + } + + + private String[] parseAreaAndTarget(String arg1, String arg2) { + String r1 = resolveAreaName(arg1); + if (r1 != null) { + return new String[]{r1, arg2}; + } + String r2 = resolveAreaName(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) {} + } + + private void showHelp(CommandSender s) { + s.sendMessage("§e§l==== 区域防护 ===="); + s.sendMessage("§a/protect 工具 §7选地工具"); + s.sendMessage("§a/protect 创建 <名> §7创建区域"); + s.sendMessage("§a/protect 列表 §7区域列表"); + s.sendMessage("§a/protect 重载 §7重载配置"); + s.sendMessage("§a/protect 删除 <名> §7删除区域"); + s.sendMessage("§a/protect add [区域] <玩家> §7玩家加白"); + s.sendMessage("§a/protect remove [区域] <玩家> §7玩家删白"); + s.sendMessage("§a/protect additem [区域] <物品> §7物品加黑"); + s.sendMessage("§a/protect removeitem [区域] <物品> §7物品删黑"); + s.sendMessage("§a/protect on §7显示边框"); + s.sendMessage("§a/protect off §7关闭边框"); + s.sendMessage("§a/protect tempon §7临时边框(15秒)"); + s.sendMessage("§a/protect list §7列出全局白名单"); + s.sendMessage("§a/protect list <区域> §7列出区域白名单"); + s.sendMessage("§a/protect listitem §7列出全局物品黑名单"); + s.sendMessage("§a/protect listitem <区域> §7列出区域物品黑名单"); + + } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return globalPlayerWhitelist.contains(playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + return wl != null && wl.contains(playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = wl != null && wl.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + + + public int handleConfiscate(Player p, AreaConfig ac) { + if (isPlayerWhitelisted(p.getName(), ac)) 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 = ""; + + 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(); + all[i] = null; + } + } + p.getInventory().setContents(all); + + ItemStack off = p.getInventory().getItemInOffHand(); + if (off != null && off.getType() == mat) { + count += off.getAmount(); + p.getInventory().setItemInOffHand(null); + } + + if (count > 0) { + totalRemoved += count; + if (!confiscatedNames.isEmpty()) + confiscatedNames += "§7、§e"; + confiscatedNames += "§e" + matname(mat) + + " x" + count; + } + } + + if (totalRemoved > 0) { + String msg = (ac.confiscateMsg != null + && !ac.confiscateMsg.isEmpty()) + ? ac.confiscateMsg + : "你携带了违禁品,已被没收"; + 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("[防护] 已清空所有黑名单"); + } + + @EventHandler + public void onEntityDamage( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑 ===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + + // 展示框被点击(旋转/放入/取出) + @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(); + + // 全局白放行 + Set global = globalWhitelist.get("global"); + if (global != null + && global.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(clicked); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set areaWl = areaWhitelist.get(ac.name); + if (areaWl != null + && areaWl.contains(p.getName().toLowerCase())) { + 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 + public void onEntityDamageByEntity( + org.bukkit.event.entity.EntityDamageByEntityEvent e) { + Entity entity = e.getEntity(); + String typeName = entity.getType().name(); + + if (typeName.equals("ITEM_FRAME") + || typeName.equals("GLOW_ITEM_FRAME")) { + // 展示框被攻击 + if (!(e.getDamager() instanceof Player)) return; + Player p = (Player) e.getDamager(); + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(p.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(entity); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(p.getName().toLowerCase())) { + return; + } + + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f禁止破坏展示框"); + return; + } + + // ===== PVP 逻辑(原有的,不动)===== + if (!(e.getDamager() instanceof Player)) return; + if (!(e.getEntity() instanceof Player)) return; + Player p2 = (Player) e.getEntity(); + AreaConfig ac2 = getArea( + p2.getWorld().getName(), + p2.getLocation().getBlockX(), + p2.getLocation().getBlockY(), + p2.getLocation().getBlockZ()); + if (ac2 != null && ac2.denyPVP + && !isPlayerWhitelisted(p2.getName(), ac2)) { + e.setCancelled(true); + p2.sendMessage("§c§l[区域防护] §f禁止PVP"); + } + } + 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(); + + AreaConfig ac = getArea(world, bx, by, bz); + if (ac != null && ac.denyItemFrame) 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 && ac.denyItemFrame) return ac; + } + return null; + } + + // ===== 投射物击中展示框(弓箭/弩)===== + @EventHandler + public void onProjectileHit( + org.bukkit.event.entity.ProjectileHitEvent e) { + if (!(e.getEntity().getShooter() instanceof Player)) return; + Player shooter = (Player) e.getEntity().getShooter(); + Entity hit = e.getHitEntity(); + if (hit == null) return; + String typeName = hit.getType().name(); + if (!typeName.equals("ITEM_FRAME") + && !typeName.equals("GLOW_ITEM_FRAME")) { + return; + } + + // 全局白放行 + Set gwl = globalWhitelist.get("global"); + if (gwl != null + && gwl.contains(shooter.getName().toLowerCase())) { + return; + } + + AreaConfig ac = findFrameArea(hit); + if (ac == null || !ac.denyItemFrame) return; + + // 区域白放行 + Set awl = areaWhitelist.get(ac.name); + if (awl != null + && awl.contains(shooter.getName().toLowerCase())) { + 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 onIgnite(BlockIgniteEvent e) { + Player p = e.getPlayer(); + if (p == null) return; // 非玩家点燃的(闪电等) + Block block = e.getBlock(); + AreaConfig ac = getArea( + block.getWorld().getName(), + block.getX(), block.getY(), block.getZ()); + if (ac != null && ac.denyFireSpread) { + e.setCancelled(true); + p.sendMessage("§c§l[区域防护] §f此区域禁止点燃物品"); + } + } + + // ===== 区域内方块着火(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); + } + + + 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 AreaConfig { + public String name = ""; + public String world = ""; + public int x1, z1, x2, z2; + public int yMin = 0, yMax = 255; + public List confiscateItems = new ArrayList<>(); + public List denyUseItems = new ArrayList<>(); + public List clearEffects = new ArrayList<>(); // 清除单个效果名 + public boolean clearAllBadEffects = false; // 清除所有负面+中性 + public List giveEffects = new ArrayList<>(); // [效果名, 等级, 秒数] + public List punishCommands = new ArrayList<>(); + public boolean denyBlockPlace = false; + public boolean denyBlockBreak = false; + public boolean denyPVP = false; + public boolean denyFallDamage = false; + public boolean denyHunger = false; + public boolean denyAllDamage = false; + public boolean denyMount = false; + public boolean denyDrop = false; + public boolean denyEnderPearl = false; + public boolean denyBow = false; + public boolean denyPotion = false; + public boolean denyExplosion = false; + public boolean enableAnnounce = false; + public String announceTemplate = ""; + 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 boolean denyItemFrame = false; + public Set peaceWhitelist = new HashSet<>(); + public String enforceGameMode = null; // 强制游戏模式 + public Set modeExempt = new HashSet<>(); // 模式排除名单 + + + + + 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 (denyDrop) 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 (!peaceWhitelist.isEmpty()) c++; + + return c; + } + } + } \ No newline at end of file diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondManager.java new file mode 100644 index 0000000..ea78152 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondManager.java @@ -0,0 +1,697 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import java.io.File; +import java.sql.*; +import java.util.*; + +public class BondManager { + + private final Main plugin; + private Connection db; + + // ===== 交易类型常量 ===== + public static final String TX_ADMIN_GIVE = "admin_give"; + public static final String TX_ADMIN_DEDUCT = "admin_deduct"; + public static final String TX_TRANSFER_OUT = "transfer_out"; + public static final String TX_TRANSFER_IN = "transfer_in"; + public static final String TX_REDEEM = "redeem"; + public static final String TX_SHOP_BUY = "shop_buy"; + public static final String TX_DAILY_SIGN = "daily_sign"; + public static final String TX_FREEZE = "freeze"; + public static final String TX_UNFREEZE = "unfreeze"; + + + public static final int MAX_WATER_DAYS = 14; + public static final int ADMIN_MAX_WATER_DAYS = 30; + + public BondManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + // ======================== 数据库初始化 ======================== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 债券余额(含冻结状态) + st.execute("CREATE TABLE IF NOT EXISTS bonds (" + + "player_name TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "status TEXT DEFAULT 'normal')"); + + // 兼容旧表 + try { + st.execute("ALTER TABLE bonds " + + "ADD COLUMN status " + + "TEXT DEFAULT 'normal'"); + } catch (SQLException ignored) {} + + // 旧流水(保留兼容) + st.execute("CREATE TABLE IF NOT EXISTS bond_log (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "reason TEXT DEFAULT ''," + + "time INTEGER DEFAULT 0)"); + + // 新交易流水表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "bond_transaction (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "type TEXT NOT NULL," + + "amount INTEGER NOT NULL," + + "target_player TEXT DEFAULT ''," + + "operator TEXT DEFAULT ''," + + "reason TEXT DEFAULT ''," + + "balance_after INTEGER DEFAULT 0," + + "time INTEGER DEFAULT 0)"); + + try { + st.execute("CREATE INDEX " + + "IF NOT EXISTS idx_tx_pt " + + "ON bond_transaction(" + + "player_name, time)"); + } catch (SQLException ignored) {} + try { + st.execute("ALTER TABLE bond_transaction " + + "ADD COLUMN balance_before " + + "INTEGER DEFAULT 0"); + } catch (SQLException ignored) {} + + + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[Bond] DB init failed: " + + e.getMessage(), e); + } + } + + // ======================== 账户状态 ======================== + + public boolean isFrozen(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return "frozen".equals(s); + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return false; + } + + public String getAccountStatus(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT status FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String s = rs.getString("status"); + rs.close(); ps.close(); + return s != null ? s : "normal"; + } + rs.close(); ps.close(); + } catch (SQLException ignored) {} + return "normal"; + } + + public void freezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='frozen' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + public void unfreezeAccount(String player) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET status='normal' " + + "WHERE player_name=?"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + } + + private void ensureAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR IGNORE INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,0,'normal')"); + ps.setString(1, player); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + // ======================== 余额查询 ======================== + + public int getBonds(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount FROM bonds " + + "WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + int v = 0; + if (rs.next()) v = rs.getInt("amount"); + rs.close(); ps.close(); + return v; + } catch (SQLException e) { return 0; } + } + + /** 该玩家是否在 bond 表中有真实记录 */ + public boolean hasBondAccount(String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM bonds " + + "WHERE player_name=? " + + "AND amount > 0"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + boolean exists = rs.next(); + rs.close(); ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ======================== 增加债券 ======================== + + public int addBonds(String player, int amount) { + return addBondsInternal(player, amount, + TX_ADMIN_GIVE, "", "", + "管理员发放"); + } + + public int addBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return -1; + return addBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + private int addBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + int after = before + amount; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status)" + + " VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, amount); + return after; + } + + // ======================== 扣除债券 ======================== + + + + private boolean deductBondsInternal(String player, + int amount, + String type, + String targetPlayer, + String operator, + String reason) { + int before = getBonds(player); + if (before < amount) return false; + int after = before - amount; + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE bonds SET amount=? " + + "WHERE player_name=?"); + ps.setInt(1, after); + ps.setString(2, player); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + logTransaction(player, type, amount, + targetPlayer, operator, reason, + before, after); + logLegacy(player, -amount); + return true; + } + + // ======================== 流水写入 ======================== + + private void logTransaction(String player, + String type, + int amount, + String targetPlayer, + String operator, + String reason, + int balanceBefore, + int balanceAfter) { + try { + // 合并模式:shop_buy 30秒内同商品合并 + if ("shop_buy".equals(type) + && reason != null + && !reason.isEmpty()) { + long mergeWindow = + System.currentTimeMillis() - 30000L; + PreparedStatement chk = + db.prepareStatement( + "SELECT id, amount " + + "FROM bond_transaction " + + "WHERE player_name=? " + + "AND type=? " + + "AND reason=? " + + "AND time > ? " + + "ORDER BY time DESC " + + "LIMIT 1"); + chk.setString(1, player); + chk.setString(2, type); + chk.setString(3, reason); + chk.setLong(4, mergeWindow); + ResultSet rs = chk.executeQuery(); + if (rs.next()) { + int existId = rs.getInt("id"); + int existAmt = rs.getInt("amount"); + rs.close(); + chk.close(); + PreparedStatement upd = + db.prepareStatement( + "UPDATE bond_transaction " + + "SET amount=?, " + + "balance_after=?, " + + "time=? " + + "WHERE id=?"); + upd.setInt(1, existAmt + amount); + upd.setInt(2, balanceAfter); + upd.setLong(3, + System.currentTimeMillis()); + upd.setInt(4, existId); + upd.executeUpdate(); + upd.close(); + return; + } + rs.close(); + chk.close(); + } + + // 原始插入 + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_transaction " + + "(player_name, type, amount," + + " target_player, operator," + + " reason, balance_before," + + " balance_after, time)" + + " VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, type); + ps.setInt(3, amount); + ps.setString(4, + targetPlayer != null ? targetPlayer : ""); + ps.setString(5, + operator != null ? operator : ""); + ps.setString(6, + reason != null ? reason : ""); + ps.setInt(7, balanceBefore); + ps.setInt(8, balanceAfter); + ps.setLong(9, System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + // ======================== 扣除债券 ======================== + + /** 完整版:含冻结检查 + 流水记录 */ + public boolean deductBonds(String player, int amount, + String type, + String targetPlayer, + String operator, + String reason) { + if (isFrozen(player)) return false; + return deductBondsInternal(player, amount, + type, targetPlayer, operator, reason); + } + + public boolean checkoutCart(String player, + String[] itemNames, + int[] subtotals) { + int total = 0; + for (int s : subtotals) total += s; + if (isFrozen(player)) return false; + + int before = getBonds(player); + if (before < total) return false; + int after = before - total; + + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?,'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, after); + ps.setInt(3, after); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + + int running = before; + for (int i = 0; i < itemNames.length; i++) { + int next = running - subtotals[i]; + logTransaction(player, "shop_buy", subtotals[i], + "", "商店系统", + "购物车购买-" + itemNames[i], + running, next); + running = next; + } + + logLegacy(player, -total); + return true; + } + + public boolean setBonds(String player, int amount) { + ensureAccount(player); + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bonds " + + "(player_name,amount,status) " + + "VALUES (?,?, 'normal') " + + "ON CONFLICT(player_name) " + + "DO UPDATE SET amount=?"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setInt(3, amount); + ps.executeUpdate(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return true; + } + + // ======================== 转账 ======================== + + public TransferResult transfer(String from, + String to, + int amount) { + if (from.equalsIgnoreCase(to)) + return new TransferResult(false, + "不能给自己转账"); + if (isFrozen(from)) + return new TransferResult(false, + "你的账户已被冻结"); + if (isFrozen(to)) + return new TransferResult(false, + to + " 的账户已被冻结"); + if (isSameIP(from, to)) + return new TransferResult(false, + "不能与同IP玩家转账"); + + if (!deductBondsInternal(from, amount, + TX_TRANSFER_OUT, to, from, + "转账给" + to)) + return new TransferResult(false, "债券不足"); + + addBondsInternal(to, amount, + TX_TRANSFER_IN, from, from, + "收到来自" + from + "的转账"); + + return new TransferResult(true, "转账成功"); + } + + // ======================== 同IP检测 ======================== + + public boolean isSameIP(String p1, String p2) { + String ip1 = getPlayerIPAny(p1); + String ip2 = getPlayerIPAny(p2); + if (ip1 == null || ip2 == null) return false; + return ip1.equals(ip2); + } + + /** 获取同IP下的所有其他在线玩家 */ + public List getSameIPPlayers(String player) { + String ip = getPlayerIPAny(player); + if (ip == null) return Collections.emptyList(); + List list = new ArrayList<>(); + for (Player p : + plugin.getServer().getOnlinePlayers()) { + String pIp = plugin.getPlayerIP(p); + if (pIp != null && pIp.equals(ip) + && !p.getName() + .equalsIgnoreCase(player)) { + list.add(p.getName()); + } + } + return list; + } + + private String getPlayerIPAny(String player) { + // 在线:直接取 + Player p = plugin.getServer() + .getPlayerExact(player); + if (p != null) { + String ip = plugin.getPlayerIP(p); + if (ip != null) return ip; + } + // 离线:查主库 + try { + Object v = plugin.getDb() + .getField(player, "last_login_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + try { + Object v = plugin.getDb() + .getField(player, "register_ip"); + if (v != null) return v.toString(); + } catch (Exception ignored) {} + return null; + } + + // ======================== 流水查询 ======================== + + public List> getTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } +// 放在 getTransactions 方法后面: + + /** + * 获取该玩家全部流水(不限时间,供打印使用) + */ + public List> + getAllTransactions(String player) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "ORDER BY time ASC"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("player_name", + rs.getString("player_name")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("operator", + rs.getString("operator")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } +// 放在 getAllTransactions 方法后面: + + /** + * 获取所有有流水记录的玩家名(去重) + */ + public List getAllPlayerNames() { + List list = new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT DISTINCT player_name " + + "FROM bond_transaction " + + "ORDER BY player_name ASC"); + while (rs.next()) { + list.add(rs.getString("player_name")); + } + rs.close(); st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTransferTransactions( + String player, int days) { + days = Math.min(days, MAX_WATER_DAYS); + long cutoff = System.currentTimeMillis() + - (long) days * 24 * 60 * 60 * 1000; + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM bond_transaction " + + "WHERE player_name=? " + + "AND time>=? " + + "AND (type=? OR type=?) " + + "ORDER BY time DESC"); + ps.setString(1, player); + ps.setLong(2, cutoff); + ps.setString(3, TX_TRANSFER_OUT); + ps.setString(4, TX_TRANSFER_IN); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("type", rs.getString("type")); + row.put("amount", + rs.getInt("amount")); + row.put("target_player", + rs.getString("target_player")); + row.put("reason", + rs.getString("reason")); + row.put("balance_after", + rs.getInt("balance_after")); + row.put("balance_before", rs.getInt("balance_before")); + row.put("time", rs.getLong("time")); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { e.printStackTrace(); } + return list; + } + + + private void logLegacy(String player, int amount) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO bond_log " + + "(player_name,amount,time) " + + "VALUES (?,?,?)"); + ps.setString(1, player); + ps.setInt(2, amount); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); ps.close(); + } catch (SQLException ignored) {} + } + + public Map getLog( + String player, int limit) { + Map info = new LinkedHashMap<>(); + info.put("balance", getBonds(player)); + return info; + } + + // ======================== 转账结果 ======================== + + public static class TransferResult { + public final boolean success; + public final String message; + public TransferResult(boolean success, + String message) { + this.success = success; + this.message = message; + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondPrinter.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondPrinter.java new file mode 100644 index 0000000..674ec3c --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/BondPrinter.java @@ -0,0 +1,378 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +public class BondPrinter { + + private final Main plugin; + private final File outputDir; + + private static final SimpleDateFormat FILE_SDF = + new SimpleDateFormat( + "yyyy-MM-dd_HH-mm-ss"); + private static final SimpleDateFormat CELL_SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + private static final LinkedHashMap + TYPE_NAMES = new LinkedHashMap<>(); + + static { + TYPE_NAMES.put("admin_give", "管理员发放"); + TYPE_NAMES.put("admin_deduct", "管理员扣除"); + TYPE_NAMES.put("transfer_out", "转出"); + TYPE_NAMES.put("transfer_in", "转入"); + TYPE_NAMES.put("redeem", "CDK兑换"); + TYPE_NAMES.put("shop_buy", "商城购买"); + TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上 + TYPE_NAMES.put("daily_sign", "签到奖励"); + TYPE_NAMES.put("freeze", "冻结"); + TYPE_NAMES.put("unfreeze", "解冻"); + + } + + public BondPrinter(Main plugin) { + this.plugin = plugin; + this.outputDir = new File( + plugin.getDataFolder(), "printer"); + if (!this.outputDir.exists()) { + this.outputDir.mkdirs(); + } + } + + // ============ 打印单个玩家 ============ + + public void printPlayer(String player, + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintPlayer( + player, sender)); + } + + private void doPrintPlayer(String player, + CommandSender sender) { + try { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + + String fileName = player + "_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + deleteOld(player); + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, + escXml(player)); + writeXmlRow(pw, + new String[]{ + "时间", "类型", + "金额", "对象", + "操作者", "理由", + "操作前余额", + "操作后余额"}); + + for (Map tx : txs) { + writeXmlRow(pw, new String[]{ + txTime(tx), + txTypeName(tx), + String.valueOf( + txInt(tx, + "amount")), + txStr(tx, + "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")) + }); + } + + writeXmlFooter(pw); + pw.flush(); + } finally { + pw.close(); + } + + if (sender != null) { + sender.sendMessage("§a[打印] §f" + + player + " §a流水已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + txs.size() + + " §7条,§724小时后删除"); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 打印全服 ============ + + public void printAll(CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doPrintAll(sender)); + } + + private void doPrintAll(CommandSender sender) { + try { + List players = + plugin.getBonds() + .getAllPlayerNames(); + if (players.isEmpty()) { + if (sender != null) { + sender.sendMessage("§c没有流水数据"); + } + return; + } + + String fileName = "全服流水_" + + FILE_SDF.format(new Date()) + + ".xls"; + File file = new File(outputDir, fileName); + + // ===== 收集所有流水 ===== + // key = 时间戳+玩家+金额,用于去重 + // value = 一行数据 + List allRows = new ArrayList<>(); + Set seenKeys = new HashSet<>(); + + for (String player : players) { + List> txs = + plugin.getBonds() + .getAllTransactions(player); + for (Map tx : txs) { + int txId = ((Number) tx.get("id")) + .intValue(); + String dedupKey = + String.valueOf(txId); + if (!seenKeys.add(dedupKey)) { + continue; + } + + String timeStr = txTime(tx); + long timeMs = 0; + Object t = tx.get("time"); + if (t != null) { + timeMs = + ((Number) t).longValue(); + } + + allRows.add(new String[]{ + player, + timeStr, + txTypeName(tx), + String.valueOf( + txInt(tx, "amount")), + txStr(tx, "target_player"), + txStr(tx, "operator"), + txStr(tx, "reason"), + String.valueOf( + txInt(tx, + "balance_before")), + String.valueOf( + txInt(tx, + "balance_after")), + // 额外存时间戳用于排序 + String.valueOf(timeMs) + }); + } + } + + // ===== 按时间戳排序 ===== + allRows.sort((a, b) -> { + long ta = Long.parseLong( + a[a.length - 1]); + long tb = Long.parseLong( + b[b.length - 1]); + return Long.compare(ta, tb); + }); + + // ===== 写入文件 ===== + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8)); + try { + writeXmlHeader(pw, "全服流水"); + writeXmlRow(pw, new String[]{ + "玩家", "时间", "类型", + "金额", "对象", "操作者", + "理由", "操作前余额", + "操作后余额" + }); + + for (String[] row : allRows) { + // 去掉最后的时间戳列 + String[] cells = + new String[row.length - 1]; + System.arraycopy(row, 0, + cells, 0, cells.length); + writeXmlRow(pw, cells); + } + + writeXmlFooter(pw); + pw.flush(); + + if (sender != null) { + sender.sendMessage( + "§a[打印] §f全服流水 §a已导出"); + sender.sendMessage("§7文件: §f" + + fileName); + sender.sendMessage("§7共 §f" + + players.size() + + " 名玩家,§f" + + allRows.size() + " §7条"); + sender.sendMessage( + "§7按时间从早到晚排序"); + sender.sendMessage( + "§724小时后自动删除"); + } + } finally { + pw.close(); + } + } catch (Exception e) { + if (sender != null) { + sender.sendMessage("§c导出失败: " + + e.getMessage()); + } + plugin.getLogger().severe( + "[Printer] " + e.getMessage()); + } + } + + // ============ 清理 ============ + + public void cleanupOldFiles() { + long now = System.currentTimeMillis(); + long maxAge = 24L * 60 * 60 * 1000; + File[] files = outputDir.listFiles( + (d, n) -> n.endsWith(".xls")); + if (files == null) { + return; + } + int count = 0; + for (File f : files) { + if (now - f.lastModified() > maxAge) { + if (f.delete()) { + count++; + } + } + } + if (count > 0) { + plugin.getLogger().info( + "[Printer] 清理 " + + count + " 个过期文件"); + } + } + + private void deleteOld(String player) { + File[] old = outputDir.listFiles( + (d, n) -> n.startsWith(player + "_") + && n.endsWith(".xls")); + if (old != null) { + for (File f : old) { + f.delete(); + } + } + } + + // ============ XML工具 ============ + + private void writeXmlHeader(PrintWriter pw, + String sheetName) { + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(" "); + pw.println(" "); + } + + private void writeXmlRow(PrintWriter pw, + String[] cells) { + pw.println(" "); + for (String cell : cells) { + pw.println(" " + + "" + + escXml(cell != null ? cell : "") + + ""); + } + pw.println(" "); + } + + private void writeXmlFooter(PrintWriter pw) { + pw.println("
"); + pw.println("
"); + pw.println("
"); + } + + private String escXml(String val) { + if (val == null) { + return ""; + } + return val.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private String txTime(Map tx) { + Object t = tx.get("time"); + if (t == null) return ""; + long ms = ((Number) t).longValue(); + return CELL_SDF.format(new Date(ms)); + } + + private String txTypeName( + Map tx) { + String type = (String) tx.get("type"); + return TYPE_NAMES.getOrDefault(type, type); + } + + private int txInt(Map tx, + String key) { + Object v = tx.get(key); + if (v == null) return 0; + return ((Number) v).intValue(); + } + + private String txStr(Map tx, + String key) { + Object v = tx.get(key); + return v != null ? v.toString() : ""; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/CDKManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CDKManager.java new file mode 100644 index 0000000..e07569f --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CDKManager.java @@ -0,0 +1,293 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.nio.charset.StandardCharsets; + +public class CDKManager { + + private final Main plugin; + private Connection db; + private final Map pendingInputs + = new ConcurrentHashMap<>(); + + public CDKManager(Main plugin) { + this.plugin = plugin; + initDB(); + } + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), "bond.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS cdk (" + + "code TEXT PRIMARY KEY," + + "amount INTEGER DEFAULT 0," + + "type TEXT DEFAULT 'bond'," + + "used INTEGER DEFAULT 0," + + "used_by TEXT DEFAULT ''," + + "created_time INTEGER DEFAULT 0," + + "used_time INTEGER DEFAULT 0)"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[CDK] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== CDK兑换(供SDF1调用) ===== + + /** + * 兑换CDK,返回结果字符串 + * @return "success:金额:余额前:余额后" + * 或 "fail:原因" + */ + public String redeem(String code, String player) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT amount,type,used FROM cdk " + + "WHERE code=?"); + ps.setString(1, code); + ResultSet rs = ps.executeQuery(); + if (!rs.next()) { + rs.close(); ps.close(); + return "fail:not_found"; + } + int amount = rs.getInt("amount"); + String type = rs.getString("type"); + int used = rs.getInt("used"); + rs.close(); ps.close(); + + if (used == 1) + return "fail:already_used"; + + if (plugin.getBonds() + .isFrozen(player)) + return "fail:frozen"; + + PreparedStatement up = db.prepareStatement( + "UPDATE cdk SET used=1," + + "used_by=?,used_time=? " + + "WHERE code=?"); + up.setString(1, player); + up.setLong(2, + System.currentTimeMillis()); + up.setString(3, code); + up.executeUpdate(); up.close(); + + if ("bond".equals(type)) { + int bef = plugin.getBonds() + .getBonds(player); + plugin.getBonds().addBonds(player, + amount, + BondManager.TX_REDEEM, + "", "CDK系统", + "CDK兑换: " + code); + int aft = plugin.getBonds() + .getBonds(player); + return "success:" + amount + + ":" + bef + ":" + aft; + } + return "fail:unknown_type"; + } catch (SQLException e) { + return "fail:" + e.getMessage(); + } + } + + // ===== 聊天输入系统 ===== + + public void requestInput(Player p, + String type, + String context) { + CDKInput input = new CDKInput(); + input.type = type; + input.context = context; + input.time = System.currentTimeMillis(); + pendingInputs.put(p.getName(), input); + } + + public boolean isListening(String playerName) { + return pendingInputs + .containsKey(playerName); + } + + public void onChat(Player p, String msg) { + CDKInput input = pendingInputs + .remove(p.getName()); + if (input == null) return; + + if (System.currentTimeMillis() + - input.time > 60000) { + p.sendMessage("§c输入超时"); + return; + } + + if ("cdk".equals(input.type)) { + String result = redeem( + msg.trim(), p.getName()); + if (result != null + && result.startsWith("success:")) { + String[] parts = result.split(":"); + int amt = Integer.parseInt(parts[1]); + int bef = Integer.parseInt(parts[2]); + int aft = Integer.parseInt(parts[3]); + p.sendMessage("§e§l[CDK] §f兑换成功!" + + "获得 §c" + amt + "§f债券"); + p.sendMessage("§7余额: §e" + bef + + " §7-> §a" + aft); + } else { + p.sendMessage("§c§l[CDK] §f" + + (result != null + ? result.replace("fail:", "") + : "无效的兑换码")); + } + return; + } + + if ("econ".equals(input.type)) { + handleEconInput(p, msg.trim(), + input.context); + return; + } + + if ("bond".equals(input.type)) { + handleBondInput(p, msg.trim(), + input.context); + } + } + + private void handleEconInput(Player admin, + String msg, + String target) { + net.milkbowl.vault.economy.Economy econ + = null; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy + .Economy.class); + if (reg != null) + econ = reg.getProvider(); + } catch (Exception ignored) {} + if (econ == null) { + admin.sendMessage("§c经济系统不可用"); + return; + } + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String numStr = + msg.substring(1).trim(); + double amount = + Double.parseDouble(numStr); + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + if (msg.startsWith("-")) { + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp == null) { + admin.sendMessage("§c" + target + + " 不在线"); + return; + } + if (!econ.has(tp, amount)) { + admin.sendMessage("§c余额不足"); + return; + } + econ.withdrawPlayer(tp, amount); + admin.sendMessage("§a扣款成功"); + } else { + econ.depositPlayer(target, amount); + admin.sendMessage("§a到账成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + private void handleBondInput(Player admin, + String msg, + String target) { + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String sign = msg.substring(0, 1); + String rest = + msg.substring(1).trim(); + String[] parts = + rest.split("\\s+", 2); + int amount = Integer.parseInt(parts[0]); + String reason = parts.length >= 2 + ? parts[1].trim() + : "管理员手动调整"; + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + String opName = admin.getName(); + String txType = "-".equals(sign) + ? BondManager.TX_ADMIN_DEDUCT + : BondManager.TX_ADMIN_GIVE; + + if ("-".equals(sign)) { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + if (plugin.getBonds().deductBonds( + target, amount, txType, + "", opName, reason)) { + admin.sendMessage("§a扣除成功"); + } else { + admin.sendMessage("§c债券不足"); + } + } else { + if (plugin.getBonds() + .isFrozen(target)) { + admin.sendMessage("§c账户冻结"); + return; + } + plugin.getBonds().addBonds(target, + amount, txType, + "", opName, reason); + admin.sendMessage("§a发放成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + + public void cancelInput(String player) { + pendingInputs.remove(player); + } + + static class CDKInput { + String type; + String context; + long time; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatFilterManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatFilterManager.java new file mode 100644 index 0000000..59cb5de --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatFilterManager.java @@ -0,0 +1,612 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatFilterManager { + + private final JavaPlugin plugin; + private final List whitelistUrls = + new ArrayList<>(); + private final Set whitelistPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map + punishmentRules = + new LinkedHashMap<>(); + private final Map + punishmentDurations = + new LinkedHashMap<>(); + private final Map + violationCount = + new ConcurrentHashMap<>(); + private final Map + mutedPlayers = + new ConcurrentHashMap<>(); + private final Map messages = + new LinkedHashMap<>(); + private boolean notifyAdmin = true; + private boolean notifyAll = false; + private int muteDuration = 300; + private boolean enabled = true; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean v) { + enabled = v; + } + + + private static final Pattern URL_PATTERN = + Pattern.compile( + "(?i)(?:https?://\\S+)" + + "|(?:(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9\\-]*" + + "[a-zA-Z0-9])?\\.)+" + + "[a-zA-Z]{2,})" + + "(?:/\\S*)?"); + private static final Pattern DOT_VARIANTS = + Pattern.compile( + "[\uff0e\u3002\u2025\u2026\u00b7]"); + private static final Pattern COLOR_CODES = + Pattern.compile( + "\u00a7[0-9a-fk-orA-FK-OR]"); + + private static final Set FILE_EXT; + static { + Set s = new HashSet<>( + Arrays.asList( + "txt", "yml", "yaml", + "json", "xml", "java", + "class", "jar", "png", + "jpg", "jpeg", "gif", + "bmp", "svg", "ico", + "webp", "mp3", "mp4", + "avi", "mkv", "wav", + "flac", "ogg", "zip", + "rar", "7z", "tar", + "gz", "cfg", "conf", + "ini", "log", "db", + "sql", "sh", "bat", + "html", "htm", "css", + "js", "md", "pdf", + "dll", "so", "exe", + "bin", "c", "cpp", + "py", "rb", "go", + "rs", "kt", "lua", + "php", "asp", "jsp")); + FILE_EXT = Collections.unmodifiableSet(s); + } + + public ChatFilterManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + // ========== 配置加载 ========== + + public void loadConfig() { + whitelistUrls.clear(); + whitelistPlayers.clear(); + punishmentRules.clear(); + punishmentDurations.clear(); + messages.clear(); + + File f = new File( + plugin.getDataFolder(), "chat.txt"); + if (!f.exists()) writeDefaultFile(f); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("白名单") + || t.equals("白名单:")) { + section = "whitelist"; + continue; + } + if (t.equals("处罚规则") + || t.equals("处罚规则:")) { + section = "punishment"; + continue; + } + if (t.equals("白名单玩家") + || t.equals("白名单玩家:")) { + section = "players"; + continue; + } + if (t.contains(":")) { + String[] kv = t.split(":", 2); + String k = kv[0].trim(); + String v = kv[1].trim(); + if (equals("启用过滤") + || equals("enabled")) { + enabled = parseBool(v); + continue; + } + + if (k.equals("通知管理员")) { + notifyAdmin = parseBool(v); + continue; + } + if (k.equals("全服通报")) { + notifyAll = parseBool(v); + continue; + } + if (k.equals("禁言时长")) { + try { + muteDuration = + Integer.parseInt(v); + } catch (Exception ignored) { + } + continue; + } + } + switch (section) { + case "whitelist": + whitelistUrls.add( + t.toLowerCase()); + break; + case "punishment": + String[] parts = t.split(":"); + if (parts.length >= 2) { + try { + int cnt = + Integer.parseInt( + parts[0] + .trim()); + String type = + parts[1].trim() + .toLowerCase(); + int dur = + parts.length >= 3 + ? Integer.parseInt( + parts[2] + .trim()) + : muteDuration; + punishmentRules.put( + cnt, type); + punishmentDurations.put( + cnt, dur); + } catch (Exception ignored) { + } + } + break; + case "players": + whitelistPlayers.add(t); + break; + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_chat] 配置加载失败: " + + e.getMessage()); + } + + // 从消息.txt读取chat_开头的消息 + loadMessages(); + } + + private void loadMessages() { + File msgFile = new File( + plugin.getDataFolder(), + "消息.txt"); + if (!msgFile.exists()) return; + try (BufferedReader mr = + new BufferedReader( + new InputStreamReader( + new FileInputStream( + msgFile), + StandardCharsets.UTF_8))) { + String mline; + while ((mline = mr.readLine()) != null) { + mline = mline.trim(); + if (mline.isEmpty() + || mline.startsWith("#")) + continue; + // 支持 = 和 : 两种分隔符 + int eqIdx = mline.indexOf('='); + int coIdx = mline.indexOf(':'); + int splitIdx = -1; + if (eqIdx >= 0 && coIdx >= 0) { + splitIdx = Math.min(eqIdx, coIdx); + } else if (eqIdx >= 0) { + splitIdx = eqIdx; + } else if (coIdx >= 0) { + splitIdx = coIdx; + } + if (splitIdx < 0) continue; + String k = mline.substring(0, + splitIdx).trim(); + String v = mline.substring( + splitIdx + 1).trim(); + if (k.startsWith("chat_")) { + messages.put(k, v); + } + } + } catch (Exception ignored) { + } + } + + + private void writeDefaultFile(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("启用过滤: " + enabled); + pw.println("通知管理员: true"); + pw.println("全服通报: false"); + pw.println("禁言时长: 300"); + pw.println(); + pw.println("白名单:"); + pw.println("*.minecraft.net"); + pw.println("baidu.com"); + pw.println("github.com"); + pw.println(); + pw.println("处罚规则:"); + pw.println("1:warn:0"); + pw.println("2:warn:0"); + pw.println("3:mute:300"); + pw.println("5:mute:600"); + pw.println("8:kick:0"); + pw.println("10:ban:0"); + pw.println(); + pw.println("白名单玩家:"); + } catch (IOException ignored) { + } + } + + public void saveConfig() { + File f = new File( + plugin.getDataFolder(), "chat.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: " + notifyAdmin); + pw.println("全服通报: " + notifyAll); + pw.println("禁言时长: " + muteDuration); + pw.println(); + pw.println("白名单:"); + for (String u : whitelistUrls) + pw.println(u); + pw.println(); + pw.println("处罚规则:"); + for (Map.Entry entry + : punishmentRules.entrySet()) { + int dur = punishmentDurations + .getOrDefault(entry.getKey(), + muteDuration); + pw.println(entry.getKey() + ":" + + entry.getValue() + + ":" + dur); + } + pw.println(); + pw.println("白名单玩家:"); + for (String p : whitelistPlayers) + pw.println(p); + } catch (IOException e) { + plugin.getLogger().severe( + "[Sdf1_chat] 保存失败: " + + e.getMessage()); + } + } + + // ========== 消息 ========== + + public String msg(String key, + String... args) { + String template = messages.getOrDefault( + key, key); + for (int i = 0; i < args.length - 1; + i += 2) { + template = template.replace( + "{" + args[i] + "}", + args[i + 1]); + } + return template; + } + + // ========== URL检测 ========== + + private String normalizeMessage(String msg) { + msg = COLOR_CODES.matcher(msg) + .replaceAll(""); + msg = DOT_VARIANTS.matcher(msg) + .replaceAll("."); + return msg; + } + + public List extractUrls( + String message) { + String normalized = + normalizeMessage(message); + List urls = new ArrayList<>(); + Matcher m = URL_PATTERN + .matcher(normalized); + while (m.find()) { + String url = m.group().toLowerCase(); + url = url.replaceAll( + "[\\s.,;!?,。;!?、)》」』】" + + "\\]\\[\\((]+$", + ""); + if (!url.isEmpty()) urls.add(url); + } + return urls; + } + + public boolean isWhitelisted(String url) { + String clean = url + .replaceAll("^https?://", "") + .toLowerCase(); + int colon = clean.indexOf(':'); + int slash = clean.indexOf('/'); + if (colon > 0 && (slash < 0 + || colon < slash)) { + clean = clean.substring(0, colon); + } + slash = clean.indexOf('/'); + if (slash > 0) { + clean = clean.substring(0, slash); + } + for (String entry : whitelistUrls) { + String e = entry.toLowerCase().trim(); + if (e.startsWith("*.")) { + String suffix = e.substring(1); + if (clean.endsWith(suffix) + || clean.equals( + suffix.substring(1))) + return true; + } else { + if (clean.equals(e) + || clean.endsWith( + "." + e)) + return true; + } + } + return false; + } + + public boolean isLikelyDomain(String url) { + int lastDot = url.lastIndexOf('.'); + if (lastDot < 0 + || lastDot >= url.length() - 1) + return false; + String ext = url.substring(lastDot + 1) + .toLowerCase(); + return !FILE_EXT.contains(ext); + } + + // ========== 玩家操作 ========== + + public boolean isMuted(String name) { + Long expiry = mutedPlayers.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() + >= expiry) { + mutedPlayers.remove(name); + return false; + } + return true; + } + + public String fmtDuration(int seconds) { + if (seconds <= 0) return "0秒"; + int day = seconds / 86400; + int hr = (seconds % 86400) / 3600; + int min = (seconds % 3600) / 60; + int sec = seconds % 60; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hr > 0) + sb.append(hr).append("小时"); + if (min > 0) + sb.append(min).append("分"); + if (sec > 0 || sb.length() == 0) + sb.append(sec).append("秒"); + return sb.toString(); + } + + public void applyPunishment(Player player, + int violation) { + String type = "warn"; + int duration = muteDuration; + int maxRule = 0; + for (Map.Entry entry + : punishmentRules.entrySet()) { + if (violation >= entry.getKey() + && entry.getKey() > maxRule) { + maxRule = entry.getKey(); + type = entry.getValue(); + duration = punishmentDurations + .getOrDefault( + entry.getKey(), + muteDuration); + } + } + if (type.equals("mute")) { + if (isMuted(player.getName())) + return; + mutedPlayers.put(player.getName(), + System.currentTimeMillis() + + (long) duration + * 1000L); + } + final String fType = type; + final int fDur = duration; + final int fV = violation; + final String fName = player.getName(); + Bukkit.getScheduler().runTask(plugin, + () -> { + Player p = Bukkit.getPlayer(fName); + if (p == null) return; + switch (fType) { + case "warn": + // 警告不处罚,只提示 + break; + case "mute": + p.sendMessage( + msg("chat_muted")); + break; + case "kick": + p.kickPlayer( + msg("chat_muted")); + break; + case "ban": + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban " + fName + + " §c多次发送违规链接"); + break; + case "banip": + InetSocketAddress addr = + p.getAddress(); + if (addr != null) { + String ip = addr + .getAddress() + .getHostAddress(); + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban-ip " + ip + + " §c发送违规链接"); + } + break; + } + }); + } + + public void unmutePlayer(String name) { + mutedPlayers.remove(name); + } + + public void resetPlayer(String name) { + mutedPlayers.remove(name); + violationCount.remove(name); + } + + public boolean isPlayerWhitelisted( + String name) { + return whitelistPlayers.contains(name); + } + + public boolean isNotifyAdmin() { + return notifyAdmin; + } + + public boolean isNotifyAll() { + return notifyAll; + } + + public int getViolationCount(String name) { + return violationCount + .getOrDefault(name, 0); + } + + public void incrementViolation( + String name) { + violationCount.put(name, + getViolationCount(name) + 1); + } + + public List getWhitelistUrls() { + return whitelistUrls; + } + + public Set getWhitelistPlayers() { + return whitelistPlayers; + } + + public Map + getPunishmentRules() { + return punishmentRules; + } + + public Map + getPunishmentDurations() { + return punishmentDurations; + } + + public Map + getMutedPlayers() { + return mutedPlayers; + } + + public void setNotifyAdmin(boolean v) { + notifyAdmin = v; + } + + public void setNotifyAll(boolean v) { + notifyAll = v; + } + + public void setMuteDuration(int v) { + muteDuration = v; + } + + public void addUrl(String url) { + whitelistUrls.add( + url.toLowerCase().trim()); + saveConfig(); + } + + public boolean removeUrl(String url) { + boolean r = whitelistUrls.remove( + url.toLowerCase().trim()); + saveConfig(); + return r; + } + + public void addWhitelistPlayer( + String name) { + whitelistPlayers.add(name); + saveConfig(); + } + + public boolean removeWhitelistPlayer( + String name) { + boolean r = + whitelistPlayers.remove(name); + saveConfig(); + return r; + } + + public void cleanExpired() { + long now = System.currentTimeMillis(); + mutedPlayers.entrySet().removeIf( + e -> e.getValue() <= now); + } + + private boolean parseBool(String v) { + String s = v.toLowerCase().trim(); + return s.equals("true") + || s.equals("on") + || s.equals("yes") + || s.equals("1") + || s.contains("开启") + || s.contains("启用") + || s.contains("是"); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatInputManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatInputManager.java new file mode 100644 index 0000000..49f53fd --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ChatInputManager.java @@ -0,0 +1,842 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class ChatInputManager { + + public enum InputType { + NONE, + CHANGE_PWD_STEP1, + CHANGE_PWD_STEP2, + SET_EMAIL, + SET_EMAIL_VERIFY, + ADMIN_AUTH, + ADMIN_SET_POINTS, + ADMIN_SET_PWD, + ADMIN_DELETE_CONFIRM, + ADMIN_SET_AFK_TIME, + SMTP_CONFIG, + SMTP_VERIFY, + BACK_CHECKIN, + INVITE_INPUT_CODE, + TICKET_TITLE, + TICKET_DESC, + TICKET_SCORE, + TICKET_REJECT, + TICKET_ASSIGN, + TICKET_INVITE_PROVIDER, + TICKET_REMOVE_PROVIDER, + TICKET_COMPLETE, + TICKET_REPORT_ISSUE, + TICKET_REPLY, + TICKET_ADMIN_TYPE, + TICKET_ADMIN_TITLE, + TICKET_ADMIN_DESC, + TICKET_ADMIN_PROVIDER, + BACK_CHECK_ECONOMY_CONFIRM, + USER_MGMT_SEARCH, + + } + + public static class PlayerState { + public InputType type = InputType.NONE; + public String targetPlayer = ""; + public String ticketType = ""; + public String ticketTitle = ""; + public String ticketDesc = ""; + public int ticketId = 0; + public int smtpStep = 0; + public int scoreValue = 0; + + } + + private final Map states = + new HashMap<>(); + private Main plugin; + + public PlayerState getState(Player p) { + return states.computeIfAbsent( + p.getName(), + k -> new PlayerState()); + } + + public void reset(Player p) { + states.remove(p.getName()); + } + public boolean isInFlow(Player p) { + PlayerState state = states.get(p.getName()); + return state != null + && state.type != InputType.NONE; + } + + public void handleInput(Player p, String msg, + Main mainPlugin) { + if (this.plugin == null) + this.plugin = mainPlugin; + PlayerState state = getState(p); + ConfigManager config = + mainPlugin.getConfig2(); + DatabaseManager db = mainPlugin.getDb(); + + switch (state.type) { + + case CHANGE_PWD_STEP1: { + String name = p.getName(); + String salt = (String) db.getField( + name, "password_salt"); + String hash = + PasswordUtils.hash(msg, salt); + boolean mainOk = + db.checkPassword(name, hash); + if (mainOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "main"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + boolean tempOk = + db.checkPasswordOrTemp( + name, hash); + if (tempOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "temp"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + p.sendMessage(config.msg( + "password_wrong")); + return; + } + + case CHANGE_PWD_STEP2: { + String name = p.getName(); + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String oldHash = + (String) db.getField(name, + "password_hash"); + String oldSalt = + (String) db.getField(name, + "password_salt"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + msg, newSalt); + if (newHash.equals(oldHash)) { + p.sendMessage(config.msg( + "password_same")); + reset(p); + return; + } + mainPlugin.recordPasswordChange( + name, oldHash, oldSalt); + db.setField(name, + "password_hash", newHash); + db.setField(name, + "password_salt", newSalt); + mainPlugin + .getNeedsPasswordChange() + .remove(name); + p.sendMessage(config.msg( + "password_changed")); + reset(p); + return; + } + + case SET_EMAIL: { + if (!msg.contains("@") + || !msg.contains(".")) { + p.sendMessage(config.msg( + "email_format_error")); + reset(p); + return; + } + String oldEmail = (String) + db.getField(p.getName(), + "email"); + String emailAddr = msg.trim(); + // 已绑定邮箱:验证登录密码 + if (oldEmail != null + && !oldEmail.isEmpty()) { + state.targetPlayer = emailAddr; + state.type = + InputType.SET_EMAIL_VERIFY; + state.ticketTitle = + "need_pwd"; + p.sendMessage( + "§e您已绑定邮箱: " + + oldEmail); + p.sendMessage( + "§e请验证登录密码以确认修改:"); + return; + } + // 未绑定邮箱:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + + 100000)); + state.targetPlayer = emailAddr; + state.ticketTitle = code; + state.type = + InputType.SET_EMAIL_VERIFY; + final String to = emailAddr; + final String c = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + boolean sent = mainPlugin + .getEmail() + .sendVerifyCode( + to, c); + Bukkit.getScheduler() + .runTask(mainPlugin, + () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + reset(fp); + } + }); + }); + return; + } + + + case SET_EMAIL_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§c邮箱设置已取消"); + reset(p); + return; + } + String expected = + state.ticketTitle; + if (msg.trim().equals(expected)) { + db.setField(p.getName(), + "email", + state.targetPlayer); + p.sendMessage( + "§a邮箱已设置: " + + state.targetPlayer); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case ADMIN_AUTH: { + if (msg.equals( + config.adminPassword)) { + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openAdmin(p)); + } else { + p.sendMessage( + "§c管理密码错误"); + } + reset(p); + return; + } + + case ADMIN_SET_POINTS: { + try { + int pts = Integer.parseInt( + msg.trim()); + db.setField( + state.targetPlayer, + "points", pts); + p.sendMessage("§a已将 " + + state.targetPlayer + + " 积分设为 " + pts); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + String tgt = + state.targetPlayer; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openUserDetail( + p, tgt)); + reset(p); + return; + } + + case ADMIN_SET_PWD: { + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String tgt = state.targetPlayer; + String salt = + PasswordUtils + .generateSalt(); + String hash = + PasswordUtils.hash( + msg, salt); + db.setField(tgt, + "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + db.setField(tgt, + "temp_pw_used", 0); + p.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码," + + "请使用临时密码登录"); + // 邮件通知 + String emailAddr = (String) + db.getField(tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fBody = body; + final String fName = tgt; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> + mainPlugin.getEmail() + .sendTempPassword( + to, + fName, + fBody)); + } + reset(p); + return; + } + + case ADMIN_DELETE_CONFIRM: { + if (msg.equals( + state.targetPlayer)) { + db.deleteUser( + state.targetPlayer); + p.sendMessage("§a已删除 " + + state.targetPlayer); + Player tgt = + Bukkit.getPlayer( + state.targetPlayer); + if (tgt != null + && tgt.isOnline()) + tgt.kickPlayer( + "§c账号已被删除"); + } else { + p.sendMessage( + "§c输入不匹配,已取消"); + } + reset(p); + return; + } + + case ADMIN_SET_AFK_TIME: { + try { + int minutes = + Integer.parseInt( + msg.trim()); + config.afkTimeout = + minutes * 60; + config.saveSettings(); + p.sendMessage( + "§a挂机时长已设为 " + + minutes + + " 分钟"); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + reset(p); + return; + } + + case SMTP_CONFIG: { + String m = msg.trim(); + if ("000".equals(m) + || "取消取消取消" + .equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if ("取消".equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + int step = state.smtpStep; + if (step <= 1) { + if (!"0".equals(m)) + config.setSmtp( + "smtp地址", m); + state.smtpStep = 2; + p.sendMessage("§e当前端口: " + + config.getSmtp( + "smtp端口")); + p.sendMessage( + "§e输入端口(0跳过):"); + } else if (step == 2) { + if (!"0".equals(m)) + config.setSmtp( + "smtp端口", m); + state.smtpStep = 3; + p.sendMessage( + "§e输入发件邮箱:"); + } else if (step == 3) { + if (!"0".equals(m)) + config.setSmtp( + "smtp账号", m); + state.smtpStep = 4; + p.sendMessage( + "§e输入SMTP密码(0跳过):"); + } else if (step == 4) { + if (!"0".equals(m)) + config.setSmtp( + "smtp密码", m); + state.smtpStep = 5; + p.sendMessage( + "§e输入接收邮箱(留空发到发件邮箱):"); + } else if (step == 5) { + String recv = m; + if (recv.isEmpty() + || "0".equals(recv)) + recv = config.getSmtp( + "smtp账号"); + config.setSmtp( + "验证码接收邮箱", recv); + if (!config + .canSendSmtpVerify()) { + p.sendMessage( + "§c发送间隔不足1分钟"); + reset(p); + return; + } + String code = config + .generateSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String acct = config.getSmtp( + "smtp账号"); + if (acct.isEmpty() + || addr.isEmpty()) { + p.sendMessage( + "§c地址或账号为空"); + reset(p); + return; + } + config.recordSmtpSendTime(); + final String fRecv = recv; + final String fCode = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> fp.sendMessage( + "§e正在发送验证码...")); + boolean sent = + mainPlugin.getEmail() + .sendVerifyCode( + fRecv, + fCode); + final boolean fSent = sent; + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> { + if (fSent) { + state.type = + InputType + .SMTP_VERIFY; + state.smtpStep = 0; + fp.sendMessage( + "§a验证码已发送到 " + + fRecv); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败,查看控制台"); + reset(fp); + } + }); + }); + } + return; + } + + case SMTP_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if (config.checkSmtpVerifyCode( + msg.trim())) { + config.saveSmtp(); + config.clearSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String port = config.getSmtp( + "smtp端口"); + String acct = config.getSmtp( + "smtp账号"); + String recv = config.getSmtp( + "验证码接收邮箱"); + boolean ssl = config.smtpSsl; + p.sendMessage("§a§l=== SMTP配置完成 ==="); + p.sendMessage("§7地址: §f" + addr); + p.sendMessage("§7端口: §f" + port); + p.sendMessage("§7账号: §f" + acct); + p.sendMessage("§7接收: §f" + recv); + p.sendMessage("§7加密: §f" + ssl); + p.sendMessage("§a§l===================="); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case BACK_CHECKIN: { + String result = + mainPlugin.getCheckIn() + .backCheckIn(p, msg); + if (result == null) { + // 等待选择(NEED_CONFIRM) + return; + } + if (result.startsWith( + "NEED_CONFIRM:")) { + String[] parts = + result.split(":"); + // 存储到Main的Map + mainPlugin.pendingBackCheck.put( + p.getName(), + parts[1] + ":" + parts[2]); + return; + } + if (result.startsWith("§c")) { + p.sendMessage(result); + } else { + p.sendMessage(result); + } + reset(p); + return; + } + + + case INVITE_INPUT_CODE: { + String code = msg.trim(); + String myName = p.getName(); + String myCode = + (String) db.getField( + myName, + "invite_code"); + if (code.isEmpty()) { + p.sendMessage( + "§c邀请码不能为空"); + reset(p); + return; + } + if (code.equals(myCode)) { + p.sendMessage( + "§c不能用自己邀请码"); + reset(p); + return; + } + boolean found = false; + for (Map u : + db.getAllUsers()) { + String uc = (String) + u.getOrDefault( + "invite_code", ""); + if (code.equals(uc)) { + String inv = (String) + u.get("player_name"); + db.setField(myName, + "invited_by", inv); + db.addPoints(inv, 50); + p.sendMessage( + "§a邀请成功!邀请人: " + + inv); + Player ip = + Bukkit.getPlayer(inv); + if (ip != null + && ip.isOnline()) + ip.sendMessage("§a" + + myName + + " 使用了您的邀请码"); + found = true; + break; + } + } + if (!found) + p.sendMessage( + "§c邀请码不存在"); + reset(p); + return; + } + + case TICKET_TITLE: { + state.ticketTitle = msg; + state.type = + InputType.TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return; + } + + case TICKET_DESC: { + String tt = state.ticketType; + String tl = state.ticketTitle; + reset(p); + mainPlugin.getTicket() + .submitTicket( + p, tt, tl, msg); + return; + } + + case TICKET_SCORE: { + int tid = state.ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + reset(p); + mainPlugin.getTicket() + .confirmAndScore( + p, tid, sc); + } catch ( + NumberFormatException e) { + reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return; + } + + case TICKET_REJECT: { + int tid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .rejectTicket( + p, tid, msg); + return; + } + + case TICKET_ASSIGN: { + int tid = state.ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + reset(p); + mainPlugin.getTicket() + .assignToProvider( + p, tid, prov, + price); + final int fTid = tid; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getTicket() + .openDetail( + p, fTid)); + return; + } + + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return; + } + db.addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return; + } + + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.isServiceProvider(tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return; + } + db.removeServiceProvider(tgt); + p.sendMessage("§a已移除 " + tgt); + return; + } + + case TICKET_COMPLETE: { + try { + int tid = Integer.parseInt( + msg.trim()); + mainPlugin.getTicket() + .completeAsProvider( + p, tid); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效工单号"); + } + reset(p); + return; + } + + case TICKET_REPORT_ISSUE: { + String report = msg.trim(); + reset(p); + p.sendMessage("§e异常已上报"); + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§c§l[工单异常] §f" + + p.getName() + + ": " + report); + } + } + return; + } + + case TICKET_REPLY: { + int replyTid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .replyTicket( + p, replyTid, msg); + return; + } + + case TICKET_ADMIN_TYPE: { + state.ticketType = msg.trim(); + state.type = + InputType + .TICKET_ADMIN_TITLE; + p.sendMessage( + "§e请输入工单标题:"); + return; + } + + case TICKET_ADMIN_TITLE: { + state.ticketTitle = msg; + state.type = + InputType + .TICKET_ADMIN_DESC; + p.sendMessage( + "§e请输入工单描述:"); + return; + } + + case TICKET_ADMIN_DESC: { + state.targetPlayer = msg; + state.type = + InputType + .TICKET_ADMIN_PROVIDER; + p.sendMessage( + "§e输入服务商名和价格:"); + p.sendMessage( + "§7例: playerA 20"); + return; + } + + case TICKET_ADMIN_PROVIDER: { + String[] parts2 = msg.trim() + .split("\\s+"); + String prov2 = parts2[0]; + int price2 = 0; + if (parts2.length >= 2) { + try { + price2 = Integer.parseInt( + parts2[1]); + } catch ( + NumberFormatException e) { + } + } + String t2 = state.ticketType; + String ti2 = state.ticketTitle; + String d2 = state.targetPlayer; + reset(p); + mainPlugin.getTicket() + .adminCreateAndAssign( + p, t2, ti2, + d2, prov2, price2); + return; + } + + default: + reset(p); + return; + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/CheckInManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CheckInManager.java new file mode 100644 index 0000000..b594b0f --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CheckInManager.java @@ -0,0 +1,302 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class CheckInManager { + + private final Main plugin; + + public CheckInManager(Main plugin) { + this.plugin = plugin; + } + + // ===== 今日是否已签到 ===== + public boolean isCheckedInToday( + String name) { + String last = (String) plugin.getDb() + .getField(name, + "last_checkin_date"); + String today = new SimpleDateFormat( + "yyyy-MM-dd") + .format(new Date()); + return today.equals(last); + } + + // ===== 签到 ===== + // ===== 签到 ===== + public String checkIn(Player p) { + String name = p.getName(); + if (isCheckedInToday(name)) { + return plugin.getConfig2() + .msg("checkin_already"); + } + + Object streakObj = plugin.getDb() + .getField(name, "checkin_streak"); + int streak = streakObj != null + ? ((Number) streakObj).intValue() + : 0; + + String lastDate = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + + if (lastDate != null && !lastDate.isEmpty()) { + try { + java.util.Date lastD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(lastDate); + java.util.Date todayD = + new SimpleDateFormat("yyyy-MM-dd") + .parse(today); + long diff = todayD.getTime() + - lastD.getTime(); + if (diff > 86400000L * 2) { + streak = 0; + } else if (diff > 86400000L) { + streak++; + } + } catch (Exception ignored) { + } + } + + if (streak == 0) streak = 1; + else streak++; + + double multi = 1.0; + if (streak >= 21) multi = 1.5; + else if (streak >= 14) multi = 1.3; + else if (streak >= 7) multi = 1.1; + + int basePoints = + plugin.getConfig2().checkinPoints; + int points = (int) (basePoints * multi); + + plugin.getDb().addPoints(name, points); + plugin.getDb().setField(name, + "checkin_streak", streak); + plugin.getDb().setField(name, + "last_checkin_date", today); + + Object totalObj = plugin.getDb() + .getField(name, "total_checkin_days"); + int total = totalObj != null + ? ((Number) totalObj).intValue() + : 0; + plugin.getDb().setField(name, + "total_checkin_days", total + 1); + + // ★ 债券盲盒奖励(6参数版,写入流水)★ + int bondMin = + plugin.getConfig2().checkinBondMin; + int bondMax = + plugin.getConfig2().checkinBondMax; + if (bondMin <= 0) bondMin = 1; + if (bondMax < bondMin) bondMax = bondMin; + int bondReward = bondMin + + new java.util.Random() + .nextInt(bondMax - bondMin + 1); + plugin.getBonds().addBonds(name, bondReward, + "daily_sign", "", "签到系统", + "每日签到连续" + streak + "天"); + + return plugin.getConfig2() + .msg("checkin_success") + .replace("{points}", + String.valueOf(points)) + .replace("{streak}", + String.valueOf(streak)) + .replace("{multi}", + String.valueOf(multi)) + + " §e+" + bondReward + "债券"; + } + + // ===== 获取连续天数 ===== + public int getStreak(String name) { + Object val = plugin.getDb() + .getField(name, + "checkin_streak"); + if (val instanceof Number) { + return ((Number) val) + .intValue(); + } + return 0; + } + // 标准化日期 + private String normalizeDate(String input) { + String s = input.trim().replace("/", "-"); + if (s.matches("\\d{8}")) { + return s.substring(0, 4) + "-" + + s.substring(4, 6) + "-" + + s.substring(6, 8); + } + return s; + } + // ===== 补签入口 ===== + public String backCheckIn( + Player p, String dateStr) { + String name = p.getName(); + String normalized = + normalizeDate(dateStr); + + // 严格日期验证 + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd"); + sdf.setLenient(false); + java.util.Date target; + try { + target = sdf.parse(normalized); + } catch (Exception ex) { + return "§c无效日期: " + dateStr; + } + String reformat = sdf.format(target); + if (!reformat.equals(normalized)) { + return "§c无效日期: " + dateStr; + } + + String today = sdf.format(new Date()); + if (normalized.compareTo(today) >= 0) { + return "§c今天不需要补签"; + } + + String threeDaysAgo = getPastDate(3); + if (normalized.compareTo( + threeDaysAgo) < 0) { + return "§c只能补签3天内" + + "(最多到 " + threeDaysAgo + ")"; + } + + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + boolean alreadyBacked = false; + if (!existing.isEmpty()) { + for (String d : existing.split(",")) { + if (normalizeDate(d.trim()) + .equals(normalized)) { + alreadyBacked = true; + break; + } + } + } + if (alreadyBacked) { + return "§c该日期已补签: " + normalized; + } + + Object normalCheck = + plugin.getDb().getField( + name, "last_checkin_date"); + String lastCheckin = normalCheck != null + ? normalCheck.toString() : ""; + if (lastCheckin.equals(normalized)) { + return "§c该日期已签到: " + normalized; + } + + // ★ 补签消耗:3债券 + 10积分 + // 积分不够时:用 9债券(3倍)代替 ★ + int bondCost = 3; + int pointCost = 10; + int bondAltCost = 9; // 3倍债券替代 + + int myBonds = plugin.getBonds() + .getBonds(name); + Object ptsObj = plugin.getDb() + .getField(name, "points"); + int myPoints = ptsObj != null + ? ((Number) ptsObj).intValue() + : 0; + + + // 方案A:3债券 + 10积分 + boolean canA = myBonds >= bondCost + && myPoints >= pointCost; + // 方案B:9债券(积分不够时) + boolean canB = myBonds >= bondAltCost + && myPoints < pointCost; + + if (!canA && !canB) { + if (myPoints < pointCost + && myBonds < bondAltCost) { + return "§c补签费用不足" + + "(需3债券+10积分," + + "或9债券代替)" + + " 当前: " + + myBonds + "债券 " + + myPoints + "积分"; + } + if (myBonds < bondCost) { + return "§c债券不足(需" + + bondCost + "枚)"; + } + return "§c积分不足(需" + + pointCost + "分)"; + } + + // 执行扣款 + if (canA) { + // 方案A:3债券 + 10积分 + plugin.getBonds().deductBonds( + name, bondCost, + "admin_deduct", "", + "补签系统", "补签扣款"); + plugin.getDb().deductPoints( + name, pointCost); + } else { + // 方案B:9债券代替 + plugin.getBonds().deductBonds( + name, bondAltCost, + "admin_deduct", "", + "补签系统", "补签扣款(债券替代)"); + } + + doBackCheckIn(p, normalized); + + String costMsg = canA + ? bondCost + "债券+" + pointCost + "积分" + : bondAltCost + "债券(替代)"; + return "§a补签成功!日期: " + + normalized + + " §7消耗: " + costMsg; + } + + + + // ===== 执行补签 ===== + // ===== 执行补签 ===== + public void doBackCheckIn( + Player p, String dateStr) { + String name = p.getName(); + Object val = plugin.getDb() + .getField(name, "backcheck_dates"); + String existing = val != null + ? val.toString() : ""; + String updated = existing.isEmpty() + ? dateStr + : existing + "," + dateStr; + plugin.getDb().setField(name, + "backcheck_dates", updated); + int streak = getStreak(name); + plugin.getDb().setField(name, + "checkin_streak", streak + 1); + // ★ 补签不发放奖励,只记录 + } + + // ===== 获取N天前日期 ===== + private String getPastDate( + int daysAgo) { + Calendar cal = + Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, + -daysAgo); + return new SimpleDateFormat( + "yyyy-MM-dd") + .format(cal.getTime()); + } +} \ No newline at end of file diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/CommissionManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CommissionManager.java new file mode 100644 index 0000000..cb24687 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CommissionManager.java @@ -0,0 +1,227 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Logger; + +public class CommissionManager { + + private final Main main; + private final Logger logger; + private final List rules = + new ArrayList<>(); + + public CommissionManager(Main main) { + this.main = main; + this.logger = main.getLogger(); + loadRules(); + } + + public void loadRules() { + rules.clear(); + File f = new File( + main.getDataFolder(), + "提成规则.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + // 格式: 邀请人数:提成百分比 + // 如 "5:15" = 邀请5人提成15% + String[] parts = + line.split(":"); + if (parts.length >= 2) { + try { + int count = Integer.parseInt( + parts[0].trim()); + double rate = Double + .parseDouble( + parts[1] + .trim()) / 100.0; + rules.add(new double[]{ + count, rate}); + } catch (Exception ignored) { + } + } + } + // 按人数排序(从大到小) + rules.sort((a, b) -> + Double.compare(b[0], a[0])); + logger.info( + "[Commission] 加载 " + + rules.size() + " 条提成规则"); + } catch (Exception e) { + logger.warning( + "[Commission] 加载失败: " + + e.getMessage()); + } + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 邀请提成规则"); + pw.println("# 格式: 邀请人数:提成百分比"); + pw.println("# 邀请10人提成20%"); + pw.println("10:20"); + pw.println("# 邀请5人提成15%"); + pw.println("5:15"); + pw.println("# 邀请1人提成10%"); + pw.println("1:10"); + } catch (IOException ignored) { + } + } + + /** + * 获取邀请人的提成比例 + * 根据已邀请人数匹配最高规则 + */ + public double getCommissionRate( + String inviter) { + int inviteCount = getInviteCount( + inviter); + for (double[] rule : rules) { + if (inviteCount >= (int) rule[0]) { + return rule[1]; + } + } + return 0; + } + + /** + * 获取邀请人数 + */ + public int getInviteCount( + String inviter) { + try { + Object val = + main.getDb().getField( + inviter, + "monthly_invite_count"); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + Object total = + main.getDb().getField( + inviter, + "total_invite_count"); + if (total instanceof Number) { + return ((Number) total).intValue(); + } + } catch (Exception ignored) { + } + return 0; + } + + /** + * 发放提成 + * 被邀请人获得奖励时,邀请人获得提成 + */ + public void payCommission( + String invitee, + double originalAmount) { + // 查找邀请人 + String inviter = getInviter(invitee); + if (inviter == null + || inviter.isEmpty()) return; + + double rate = getCommissionRate( + inviter); + if (rate <= 0) return; + + double commission = + originalAmount * rate; + if (commission <= 0) return; + + // 通过 Vault 发放 + var rsp = main.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy.Economy.class); + if (rsp == null) return; + net.milkbowl.vault.economy.Economy econ = + rsp.getProvider(); + if (econ == null) return; + + econ.depositPlayer(inviter, commission); + + // 通知邀请人 + Player inviterP = + main.getServer() + .getPlayer(inviter); + if (inviterP != null + && inviterP.isOnline()) { + inviterP.sendMessage( + "§a§l[邀请提成] §f" + + invitee + " 完成任务," + + "你获得 §e" + + String.format("%.2f", + commission) + + " §f游戏币提成 (" + + (int)(rate * 100) + "%)"); + } + + logger.info( + "[Commission] 提成: " + + inviter + " ← " + + invitee + " | " + + String.format("%.2f", + originalAmount) + + " × " + + (int)(rate * 100) + "% = " + + String.format("%.2f", + commission)); + } + + /** + * 获取邀请人 + */ + private String getInviter(String player) { + try { + Object val = + main.getDb().getField( + player, + "invited_by"); + if (val != null + && !val.toString() + .isEmpty()) { + return val.toString(); + } + } catch (Exception ignored) { + } + return null; + } + + /** + * 判断金额是否包含经济内容 + */ + public boolean hasEconomyContent( + String text) { + String t = text.toLowerCase(); + return t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额"); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ConfigManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ConfigManager.java new file mode 100644 index 0000000..a15ada0 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ConfigManager.java @@ -0,0 +1,372 @@ +package Sdf1_login; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class ConfigManager { + + private final File dataFolder; + private final Map messages + = new LinkedHashMap<>(); + private final Map smtpSettings + = new LinkedHashMap<>(); + + // ===== 公共字段 ===== + public String adminTag = "admin"; + public String adminPassword = "qweasd"; + public int maxAccountsPerIP = 3; + public String approvalMode = "manual"; + public int autoApproveDelayMinutes = 30; + public int loginTimeout = 60; + public boolean afkEnabled = false; + public int afkTimeout = 300; + public boolean smtpSsl = true; + public String smtpVerifyCode = ""; + public long smtpVerifyExpire = 0; + public long smtpLastSendTime = 0; + public String smtpConfiguringPlayer = ""; + public String broadcastServerUrl = ""; + public int httpPort = 8080; + public String garbageTag = "admin"; + + public double checkinRewardFixed = 0; + public double checkinRewardMin = 0; + public double checkinRewardMax = 0; + public boolean checkinGivePoints = true; + public int checkinPoints = 10; + public String checkinRewardType = "none"; + public int backCheckPointMultiplier = 5; + public int checkinBondMin = 1; + public int checkinBondMax = 3; + + public double providerPointsPerScore = 2.0; + public int baseEconomyReward = 100; + public double providerEconomyPerScore = 50.0; + public int requesterPointsReward = 10; + + public boolean garbageEnabled = true; + public int garbageInterval = 300; + public int garbageMaxRounds = 1; + + // ★ 构造函数接收 File,和 Main.java 调用一致 ★ + public ConfigManager(File dataFolder) { + this.dataFolder = dataFolder; + } + public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济 + + + // ==================== 消息 ==================== + + public void loadMessages() { + messages.clear(); + File file = new File(dataFolder, "消息.txt"); + if (!file.exists()) createDefaultMessages(file); + loadMap(file, messages); + } + + public String msg(String key) { + String val = messages.getOrDefault(key, key); + val = val.replace("", "&l"); + val = val.replace("", "&r"); + val = val.replace("
", "\n"); + val = val.replace("\\n", "\n"); + val = val.replaceAll( + "&([0-9a-fk-orA-FK-OR])", "§$1"); + return val; + } + + private void createDefaultMessages(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 消息配置 ====="); + L.add("login_timeout=§c登录超时"); + L.add("not_registered=§e您尚未注册"); + L.add("not_logged_in=§e您尚未登录"); + L.add("already_registered=§c您已注册过"); + L.add("already_logged_in=§c您已登录"); + L.add("reg_success=§a注册成功!"); + L.add("login_success=§a登录成功!"); + L.add("login_failed=§c密码错误"); + L.add("password_wrong=§c密码错误"); + L.add("password_changed=§a密码修改成功!"); + L.add("password_format_error=§c密码需6位以上含大小写和数字"); + L.add("password_same=§c新密码不能与旧密码相同"); + L.add("checkin_already=§c您今日已签到"); + L.add("checkin_success=§a签到成功!获得 {points} 积分"); + L.add("chat_muted=§c你已被禁言"); + L.add("未知参数=§c未知参数或权限不足"); + writeLines(f, L); + } + + // ==================== SMTP ==================== + + public void loadSmtp() { + smtpSettings.clear(); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); + loadMap(file, smtpSettings); + } + + public String getSmtp(String key) { + return smtpSettings.getOrDefault(key, ""); + } + + public void setSmtp(String key, String value) { + smtpSettings.put(key, value); + } + + public void saveSmtp() { + File file = new File(dataFolder, "SMTP设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + for (Map.Entry e + : smtpSettings.entrySet()) { + L.add(e.getKey() + "=" + e.getValue()); + } + writeLines(file, L); + } + + private void createDefaultSmtp(File f) { + List L = new ArrayList<>(); + L.add("# ===== SMTP 配置 ====="); + L.add("smtp地址=smtp.example.com"); + L.add("smtp端口=465"); + L.add("smtp账号="); + L.add("smtp密码="); + L.add("发件人名称=Sdf1_login"); + L.add("smtp加密=true"); + writeLines(f, L); + } + + // ==================== 插件设置 ==================== + + public void loadSettings() { + File file = new File(dataFolder, "插件设置.txt"); + if (!file.exists()) createDefaultSettings(file); + Map m = new LinkedHashMap<>(); + loadMap(file, m); + + adminTag = m.getOrDefault("管理标签", adminTag); + adminPassword = m.getOrDefault("管理密码", adminPassword); + maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3); + approvalMode = m.getOrDefault("审批模式", approvalMode); + autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30); + loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60); + afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false")); + afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300); + smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true")); + httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080); + broadcastServerUrl = m.getOrDefault("resource-pack-url", ""); + + providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0); + baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100); + providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0); + requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10); + + garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true")); + garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300); + garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1); + + checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0); + checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0); + checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0); + checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true")); + checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10); + backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5); + checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1); + checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3); + if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin; + + if (checkinRewardFixed > 0) checkinRewardType = "fixed"; + else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range"; + else checkinRewardType = "none"; + rewardChannel = m.getOrDefault("奖励发放方式", "债券"); + rewardChannel = normalizeRewardChannel(rewardChannel); + +// 验证合法值 + if (!"economy".equalsIgnoreCase(rewardChannel) + && !"bonds".equalsIgnoreCase(rewardChannel)) { + rewardChannel = "bonds"; + } + + + + // ★ 自动补充新配置项 ★ + boolean needAppend = false; + List newKeys = new ArrayList<>(); + if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); } + if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); } + if (!m.containsKey("奖励发放方式")) { + needAppend = true; + newKeys.add("奖励发放方式=债券"); + } + if (needAppend) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + for (String key : newKeys) { bw.write(key); bw.newLine(); } + bw.flush(); bw.close(); + System.out.println("[Config] 已补充签到债券配置"); + + } catch (IOException ignored) {} + if (!m.containsKey("奖励发放方式")) { + try { + FileWriter fw = new FileWriter(file, true); + BufferedWriter bw = new BufferedWriter(fw); + bw.newLine(); + bw.write("奖励发放方式=bonds"); + bw.newLine(); + bw.flush(); bw.close(); + } catch (IOException ignored) {} + } + + } + } + /** + * 奖励方式标准化:中英文全兼容 + * "债券" "bonds" "Bond" "BONDS" → "bonds" + * "经济" "economy" "Economy" "ECONOMY" → "economy" + * 其他任何值 → "bonds"(默认债券) + */ + private String normalizeRewardChannel(String raw) { + if (raw == null) return "bonds"; + String s = raw.trim().toLowerCase(); + // 中文 + if (s.contains("债券")) return "bonds"; + if (s.contains("经济")) return "economy"; + // 英文 + if (s.contains("bond")) return "bonds"; + if (s.contains("econ")) return "economy"; + if (s.contains("Vault")) return "economy"; + // 兜底 + return "bonds"; + } + + public void saveSettings() { + File file = new File(dataFolder, "插件设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=" + adminTag); + L.add("管理密码=" + adminPassword); + L.add("每IP最大账号数=" + maxAccountsPerIP); + L.add("审批模式=" + approvalMode); + L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); + L.add("登录超时秒数=" + loginTimeout); + L.add("挂机踢出=" + afkEnabled); + L.add("挂机超时秒数=" + afkTimeout); + L.add("resource-pack-url=" + broadcastServerUrl); + L.add("http-port=" + httpPort); + L.add("垃圾站_启用=" + garbageEnabled); + L.add("垃圾站_清理间隔秒=" + garbageInterval); + L.add("垃圾站_保留轮数=" + garbageMaxRounds); + L.add("签到固定金额=" + checkinRewardFixed); + L.add("签到最小金额=" + checkinRewardMin); + L.add("签到最大金额=" + checkinRewardMax); + L.add("签到给积分=" + checkinGivePoints); + L.add("签到积分数量=" + checkinPoints); + L.add("签到债券最小=" + checkinBondMin); + L.add("签到债券最大=" + checkinBondMax); + L.add("补签积分倍率=" + backCheckPointMultiplier); + L.add("奖励发放方式=债券"); + writeLines(file, L); + } + + private void createDefaultSettings(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=admin"); + L.add("管理密码=qweasd"); + L.add("每IP最大账号数=3"); + L.add("审批模式=manual"); + L.add("自动审批延迟分钟=30"); + L.add("登录超时秒数=60"); + L.add("挂机踢出=false"); + L.add("挂机超时秒数=300"); + L.add("resource-pack-url="); + L.add("http-port=8080"); + L.add("垃圾站_启用=true"); + L.add("垃圾站_清理间隔秒=300"); + L.add("垃圾站_保留轮数=1"); + L.add("签到固定金额=0"); + L.add("签到最小金额=50"); + L.add("签到最大金额=500"); + L.add("签到给积分=true"); + L.add("签到积分数量=10"); + L.add("签到债券最小=1"); + L.add("签到债券最大=3"); + L.add("补签积分倍率=5"); + L.add("奖励发放方式=债券"); + writeLines(f, L); + } + public List getAfkWhitelist() { + List list = new ArrayList<>(); + File file = new File(dataFolder, "挂机白名单.txt"); + if (!file.exists()) { + try { + file.createNewFile(); + FileWriter fw = new FileWriter(file); + fw.write("# 每行一个玩家名\n"); + fw.flush(); + fw.close(); + } catch (IOException ignored) {} + return list; + } + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) + list.add(line); + } + } catch (IOException ignored) {} + return list; + } + + // ==================== 其他 ==================== + + public String generateSmtpVerifyCode() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length()))); + smtpVerifyCode = sb.toString(); + smtpVerifyExpire = System.currentTimeMillis() + 300000L; + return smtpVerifyCode; + } + + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; } + return smtpVerifyCode.equalsIgnoreCase(code); + } + + public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; } + public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; } + public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); } + + // ==================== 文件工具 ==================== + + private void loadMap(File file, Map map) { + try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); + } + } catch (IOException ignored) {} + } + + private void writeLines(File file, List lines) { + try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + for (String l : lines) { w.write(l); w.newLine(); } + } catch (IOException ignored) {} + } + + private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } } + private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/CypayCommand.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CypayCommand.java new file mode 100644 index 0000000..7fd9067 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/CypayCommand.java @@ -0,0 +1,577 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +public class CypayCommand + implements CommandExecutor, TabCompleter { + + private final Main plugin; + private static final SimpleDateFormat SDF = + new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + public CypayCommand(Main plugin) { + this.plugin = plugin; + } + + private boolean hasAdminPerm(CommandSender s) { + if (s.isOp()) return true; + if (!(s instanceof Player)) return true; + Player p = (Player) s; + String tag = + plugin.getConfig2().adminTag; + return !tag.isEmpty() + && p.getScoreboardTags().contains(tag); + } + + private int parseAmount(String s) { + return Integer.parseInt( + s.trim().replaceAll("[+]", "")); + } + + private List filter( + List list, String prefix) { + String low = prefix.toLowerCase(); + return list.stream() + .filter(x -> x.toLowerCase() + .startsWith(low)) + .collect(Collectors.toList()); + } + + @Override + public boolean onCommand(CommandSender sender, + Command cmd, + String label, + String[] args) { + + // ===== /cypay — 查自己 ===== + if (args.length == 0) { + String name = sender instanceof Player + ? sender.getName() : "Console"; + int bonds = + plugin.getBonds().getBonds(name); + String status = plugin.getBonds() + .getAccountStatus(name); + sender.sendMessage( + "§e§l[债券] §f" + name + + " 余额: §a" + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals(status) + ? "§c冻结中" + : "§a正常")); + return true; + } + + // ===== /cypay import ===== + /* if (args.length == 2 + && "import".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String file = args[1]; + if (!file.endsWith(".txt")) + file += ".txt"; + int result = plugin.getCDK() + .importFromFile(file); + if (result == -1) + sender.sendMessage("§c文件不存在: " + + file); + else + sender.sendMessage("§a导入完成: " + + result + " 条"); + return true; + }*/ + + // ===== /cypay freeze ===== + if (args.length == 2 + && "freeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (plugin.getBonds().isFrozen(target)) { + sender.sendMessage("§c" + target + + " 已处于冻结状态"); + return true; + } + plugin.getBonds().freezeAccount(target); + sender.sendMessage("§a已冻结 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§c§l[系统] 你的债券账户" + + "已被管理员冻结"); + return true; + } + + // ===== /cypay unfreeze ===== + if (args.length == 2 + && "unfreeze".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + if (!plugin.getBonds() + .isFrozen(target)) { + sender.sendMessage("§c" + target + + " 未被冻结"); + return true; + } + plugin.getBonds().unfreezeAccount(target); + sender.sendMessage("§a已解冻 " + target + + " 的债券账户"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a§l[系统] 你的债券账户" + + "已被解冻"); + return true; + } + + // ===== /cypay water [days] ===== + if (args.length >= 2 + && "water".equalsIgnoreCase( + args[0])) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + int days = 7; + if (args.length >= 3) { + try { + days = Integer.parseInt(args[2]); + } catch (NumberFormatException ignored) { + } + } + days = Math.min(days, + BondManager.ADMIN_MAX_WATER_DAYS); + + if (days <= 0) days = 1; + + List> txs = + plugin.getBonds() + .getTransactions( + target, days); + + sender.sendMessage( + "§e§l===== " + target + + " 流水记录 (近" + + days + "天) ====="); + sender.sendMessage("§f当前余额: §a" + + plugin.getBonds() + .getBonds(target) + + " §f状态: " + + (plugin.getBonds() + .isFrozen(target) + ? "§c冻结" : "§a正常")); + + if (txs.isEmpty()) { + sender.sendMessage("§7 (暂无记录)"); + } else { + for (Map tx : txs) { + sender.sendMessage( + formatTxLine(tx)); + } + } + sender.sendMessage( + "§e§l==========================" + + "=========="); + return true; + } + // ===== /cypay printer [player] ===== + if ("printer".equalsIgnoreCase(args[0])) { + if (!hasAdminPerm(sender) + && !(sender instanceof Player) + == false) { + // 控制台也可以用 + if (!(sender instanceof Player)) + ; // 放行 + else if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + } + if (args.length == 1) { + // 打印全服 + sender.sendMessage( + "§e正在导出全服流水..."); + plugin.getBonds(); // 确保实例就绪 + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printAll(sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + if (args.length == 2) { + // 打印指定玩家 + String target = args[1]; + sender.sendMessage("§e正在导出 " + + target + " 的流水..."); + if (plugin.getBondPrinter() != null) { + plugin.getBondPrinter() + .printPlayer(target, sender); + } else { + sender.sendMessage("§cPrinter未初始化"); + } + return true; + } + } + + // ===== /cypay give/remove ===== + if (args.length == 3) { + String action = args[0].toLowerCase(); + if ("give".equals(action) + || "remove".equals(action)) { + if (!hasAdminPerm(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + String target = args[1]; + try { + int amount = + parseAmount(args[2]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + String txType = + "give".equals(action) + ? BondManager + .TX_ADMIN_GIVE + : BondManager + .TX_ADMIN_DEDUCT; + String opName = + sender instanceof Player + ? sender.getName() + : "Console"; + + if ("give".equals(action)) { + plugin.getBonds().addBonds( + target, amount, + txType, "", + opName, "管理员发放"); + sender.sendMessage( + "§a已给 " + target + + " " + amount + + " 债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage( + "§a管理员给了你 " + + amount + + " 债券"); + } else { + if (plugin.getBonds() + .deductBonds(target, + amount, + txType, "", + opName, + "管理员扣除")) { + sender.sendMessage( + "§a已扣除 " + target + + " " + amount + + " 债券"); + Player tp = plugin + .getServer() + .getPlayerExact( + target); + if (tp != null) + tp.sendMessage( + "§c管理员扣除了" + + "你的 " + + amount + + " 债券"); + } else { + sender.sendMessage( + "§c" + target + + " 债券不足" + + "或账户冻结"); + } + } + } catch (NumberFormatException e) { + sender.sendMessage( + "§c金额格式错误"); + } + return true; + } + } + + // ===== /cypay — 仅玩家转账 ===== + if (args.length == 2) { + try { + int amount = parseAmount(args[1]); + if (amount <= 0) { + sender.sendMessage( + "§c金额必须大于0"); + return true; + } + // ★ 控制台禁止转账 + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c控制台不能转账,请使用 " + + "/cypay give <玩家> <金额>"); + return true; + } + Player p = (Player) sender; + String target = args[0]; + BondManager.TransferResult r = + plugin.getBonds().transfer( + p.getName(), + target, amount); + if (r.success) { + p.sendMessage("§e§l[债券转账] §f向 §a" + + target + " §f转出 §c" + + amount + " §f枚债券"); + Player tp = plugin.getServer() + .getPlayerExact(target); + if (tp != null) + tp.sendMessage("§e§l[债券转账] §f收到 §a" + + p.getName() + + " §f转入 §a" + + amount + " §f枚债券"); + } else { + p.sendMessage("§c" + r.message + + ",当前: " + + plugin.getBonds() + .getBonds(p.getName())); + } + return true; + } catch (NumberFormatException ignored) { + } + } + + // ===== /cypay — 查指定玩家 ===== + if (args.length == 1) { + String target = args[0]; + // ★ 验证玩家在 login.db 中存在 ★ + if (!plugin.getDb().userExists(target)) { + sender.sendMessage("§c玩家 " + target + + " 不存在于服务器"); + return true; + } + + // String target = args[0]; + if (hasAdminPerm(sender)) { + int bonds = plugin.getBonds() + .getBonds(target); + String status = plugin.getBonds() + .getAccountStatus(target); + sender.sendMessage( + "§e§l[债券] §f" + target + + " 余额: §a" + + bonds); + sender.sendMessage( + "§e§l[债券] §f状态: " + + ("frozen".equals( + status) + ? "§c冻结中" + : "§a正常")); + } else { + int bonds = plugin.getBonds() + .getBonds( + sender.getName()); + sender.sendMessage( + "§e§l[债券] §f你的余额: §a" + + bonds); + } + return true; + } + + showHelp(sender); + return true; + } + + // ===== 流水行格式化 ===== + + private String formatTxLine( + Map tx) { + String type = (String) tx.get("type"); + int amount = ((Number) tx.get("amount")) + .intValue(); + String tgt = (String) tx.get("target_player"); + String reason = (String) tx.get("reason"); + int bal = ((Number) tx.get("balance_after")) + .intValue(); + long time = ((Number) tx.get("time")) + .longValue(); + + boolean pos = amount > 0; + String sign = pos ? "+" : ""; + String color = pos ? "§a" : "§c"; + + StringBuilder sb = new StringBuilder(); + sb.append("§7[") + .append(SDF.format(new Date(time))) + .append("] ") + .append(formatTxType(type)) + .append(" ") + .append(color).append(sign) + .append(amount).append("§7"); + if (tgt != null && !tgt.isEmpty()) + sb.append(" §e↔ ").append(tgt); + if (reason != null && !reason.isEmpty()) + sb.append(" §7(").append(reason) + .append(")"); + sb.append(" §7余额:§f").append(bal); + return sb.toString(); + } + + private String formatTxType(String type) { + if (type == null) return "§7未知"; + switch (type) { + case BondManager.TX_ADMIN_GIVE: + return "§a管理员发放"; + case BondManager.TX_ADMIN_DEDUCT: + return "§c管理员扣除"; + case BondManager.TX_TRANSFER_OUT: + return "§c转出"; + case BondManager.TX_TRANSFER_IN: + return "§a转入"; + case BondManager.TX_REDEEM: + return "§aCDK兑换"; + case BondManager.TX_SHOP_BUY: + return "§e商城购买"; + case BondManager.TX_DAILY_SIGN: + return "§a签到奖励"; + case BondManager.TX_FREEZE: + return "§c冻结"; + case BondManager.TX_UNFREEZE: + return "§a解冻"; + default: + return "§7" + type; + } + } + + private void showHelp(CommandSender sender) { + sender.sendMessage("§e§l债券命令帮助:"); + sender.sendMessage( + "§e/cypay §7- 查询自己余额"); + sender.sendMessage( + "§e/cypay <玩家> §7- 查余额(管理员)"); + sender.sendMessage( + "§e/cypay <玩家> <金额> §7- 转账"); + if (hasAdminPerm(sender)) { + sender.sendMessage( + "§e/cypay give <玩家> <金额> §7- 给债券"); + sender.sendMessage( + "§e/cypay remove <玩家> <金额> §7- 扣债券"); + sender.sendMessage( + "§e/cypay import <文件> §7- 导入CDK"); + sender.sendMessage( + "§e/cypay freeze <玩家> §7- 冻结"); + sender.sendMessage( + "§e/cypay unfreeze <玩家> §7- 解冻"); + sender.sendMessage( + "§e/cypay water <玩家> [天数] §7- 流水"); + sender.sendMessage( + "§e/cypay printer §7- 导出全服流水xlsx"); + sender.sendMessage( + "§e/cypay printer <玩家> §7- 导出指定玩家xlsx"); + + } + } + + @Override + public List onTabComplete( + CommandSender sender, Command command, + String label, String[] args) { + List list = new ArrayList<>(); + + if (args.length == 1) { + String last = args[0].toLowerCase(); + if (hasAdminPerm(sender)) { + list.add("give"); + list.add("remove"); + list.add("import"); + list.add("freeze"); + list.add("unfreeze"); + list.add("water"); + list.add("printer"); + + File cdkDir = new File( + plugin.getDataFolder(), + "cdk"); + if (cdkDir.exists()) { + File[] files = cdkDir.listFiles( + (d, n) -> n + .endsWith(".txt")); + if (files != null) + for (File f : files) + list.add(f.getName()); + } + } + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, last); + } + + if (args.length == 2) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first) + || "freeze".equals(first) + || "unfreeze".equals(first) + || "water".equals(first)) { + for (Player o : + Bukkit.getOnlinePlayers()) + list.add(o.getName()); + return filter(list, + args[1].toLowerCase()); + } + list.add("10"); + list.add("50"); + list.add("100"); + return filter(list, + args[1].toLowerCase()); + } + + if (args.length == 3) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first)) { + list.add("10"); + list.add("50"); + list.add("100"); + list.add("999"); + return filter(list, + args[2].toLowerCase()); + } + if ("water".equals(first)) { + list.add("1"); + list.add("3"); + list.add("7"); + list.add("14"); + return filter(list, + args[2].toLowerCase()); + } + } + + return list; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/DatabaseManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/DatabaseManager.java new file mode 100644 index 0000000..cb2b8f7 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/DatabaseManager.java @@ -0,0 +1,1758 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'," + + "backcheck_dates TEXT DEFAULT ''" + + ")"); + + + // 安全添加 users 缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + + // ===== 背包备份表:安全迁移 ===== + // 检查旧表是否存在(用 inventory_data 列判断) + boolean hasOldTable = false; + try { + ResultSet rs = st.executeQuery( + "SELECT inventory_data " + + "FROM inventory_backups " + + "LIMIT 1"); + hasOldTable = true; + rs.close(); + } catch (SQLException e) { + hasOldTable = false; + } + + if (hasOldTable) { + // 旧表存在,迁移数据到新表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups_new (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 迁移旧数据 + try { + st.execute("INSERT INTO " + + "inventory_backups_new " + + "(player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience) " + + "SELECT player_name, " + + "inventory_data, " + + "armor_data, " + + "extra_data, " + + "player_level, " + + "player_exp " + + "FROM inventory_backups"); + } catch (SQLException ignored) { + } + st.execute("DROP TABLE " + + "inventory_backups"); + st.execute("ALTER TABLE " + + "inventory_backups_new " + + "RENAME TO " + + "inventory_backups"); + } else { + // 无旧表或已迁移,直接创建 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT DEFAULT ''," + + "armor_b64 TEXT DEFAULT ''," + + "extra_b64 TEXT DEFAULT ''," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + } + + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + // PVP区域表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "name TEXT NOT NULL UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER DEFAULT 0" + + ")"); + +// PVP击杀总榜表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT NOT NULL," + + "region_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, region_name)" + + ")"); + + +// 菜单图标表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "menu_icons (" + + "player_name TEXT PRIMARY KEY," + + "icon_b64 TEXT DEFAULT ''," + + "item_name TEXT DEFAULT ''," + + "save_time INTEGER DEFAULT 0" + + ")"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 垃圾回收站物品表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "item_data TEXT DEFAULT ''," + + "amount INTEGER DEFAULT 1," + + "create_time INTEGER DEFAULT 0" + + ")"); + safeAdd(st, "menu_snowball", + "INTEGER DEFAULT 1"); + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +// ==================== 菜单图标 ==================== + + public void saveMenuIcon(String name, + String iconB64, String itemName) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO menu_icons " + + "(player_name, icon_b64, " + + "item_name, save_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, iconB64); + ps.setString(3, itemName); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT icon_b64 FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + String b64 = null; + if (rs.next()) { + b64 = rs.getString("icon_b64"); + } + rs.close(); + ps.close(); + return b64; + } catch (SQLException e) { + return null; + } + } + + public void deleteMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM menu_icons " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasMenuIcon(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM menu_icons " + + "WHERE player_name=? " + + "AND icon_b64 IS NOT NULL " + + "AND icon_b64 != '' " + + "LIMIT 1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public void createUser(String name, String hash, + String salt) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + public boolean checkPasswordOrTemp( + String name, String hash) { + // 先检查主密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean checkPassword(String name, + String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } +// ==================== PVP区域 ==================== + + public void savePVPRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions " + + "(name, world, " + + "x1, y1, z1, " + + "x2, y2, z2, " + + "create_time) " + + "VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map + getPVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getAllPVPRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + db.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deletePVPRegion(String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + +// ==================== PVP击杀 ==================== + + public int getPVPKills( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int kills = 0; + if (rs.next()) + kills = rs.getInt("kills"); + rs.close(); + ps.close(); + return kills; + } catch (SQLException e) { + return 0; + } + } + + public int getPVPDeaths( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=? " + + "AND region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = ps.executeQuery(); + int deaths = 0; + if (rs.next()) + deaths = rs.getInt("deaths"); + rs.close(); + ps.close(); + return deaths; + } catch (SQLException e) { + return 0; + } + } + + public void addPVPKill( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,1,0) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "kills=kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void addPVPDeath( + String player, String region) { + try { + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO pvp_kills " + + "(player_name, " + + "region_name, " + + "kills, deaths) " + + "VALUES (?,?,0,1) " + + "ON CONFLICT" + + "(player_name, " + + "region_name) " + + "DO UPDATE SET " + + "deaths=deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getPVPKillTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT player_name, " + + "kills, deaths " + + "FROM pvp_kills " + + "WHERE " + + "region_name=? " + + "AND kills>0 " + + "ORDER BY kills " + + "DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Connection getDb() { + return db; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/EmailManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/EmailManager.java new file mode 100644 index 0000000..b6906e5 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/EmailManager.java @@ -0,0 +1,284 @@ +package Sdf1_login; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Base64; +import java.util.logging.Logger; + +public class EmailManager { + + private final ConfigManager config; + private final Logger logger; + + public EmailManager(ConfigManager config) { + this.config = config; + this.logger = Logger.getLogger("Sdf1_login"); + } + + public boolean isConfigured() { + String host = config.getSmtp("smtp地址"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + return host != null && !host.isEmpty() + && user != null && !user.isEmpty() + && pass != null && !pass.isEmpty(); + } + + public boolean sendVerifyCode(String to, + String code) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置," + + "无法发送验证码"); + return false; + } + logger.info("[Sdf1_login] 发送验证码到 " + to); + return sendEmailSync(to, + "[Sdf1_login] 验证码", + "您的验证码: " + code + + "\n5分钟内有效。\n" + + "如非本人操作请忽略。"); + } + + public boolean sendTempPassword(String to, + String player, String tempPwd) { + if (!isConfigured()) { + logger.warning( + "[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, + "[Sdf1_login] 临时密码", + "玩家 " + player + + " 的临时密码: " + tempPwd + + "\n请尽快登录并修改密码!"); + } + + public boolean sendBody(String to, String subject, String body) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置"); + return false; + } + return sendEmailSync(to, subject, body); + } + + + private boolean sendEmailSync(String to, + String subject, String body) { + final boolean[] result = {false}; + Thread t = new Thread(() -> { + try { + result[0] = sendEmail( + to, subject, body); + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件线程异常: " + + e.getMessage()); + } + }, "Sdf1_login-SMTP"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch (InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + logger.severe( + "[Sdf1_login] SMTP超时(15秒)"); + return false; + } + return result[0]; + } + + private boolean sendEmail(String to, + String subject, String body) { + String host = config.getSmtp("smtp地址"); + String portStr = config.getSmtp("smtp端口"); + String user = config.getSmtp("smtp账号"); + String pass = config.getSmtp("smtp密码"); + String fromName = config.getSmtp( + "发件人名称"); + + int port; + try { + port = Integer.parseInt(portStr); + } catch (Exception e) { + port = 465; + } + + try { + String encSubject = "=?UTF-8?B?" + + Base64.getEncoder() + .encodeToString( + subject.getBytes("UTF-8")) + + "?="; + String encBody = Base64.getEncoder() + .encodeToString( + body.getBytes("UTF-8")); + + if (port == 465) { + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket socket = (SSLSocket) + factory.createSocket( + host, port); + socket.setSoTimeout(10000); + doSmtp(socket, host, user, pass, + to, encSubject, encBody, + fromName); + socket.close(); + } else { + Socket plainSocket = + new Socket(host, port); + plainSocket.setSoTimeout(10000); + BufferedReader preReader = + new BufferedReader( + new InputStreamReader( + plainSocket + .getInputStream(), + "UTF-8")); + PrintWriter preWriter = + new PrintWriter( + plainSocket + .getOutputStream(), + true); + checkResponse(preReader, "220"); + sendLine(preWriter, + "EHLO " + host); + checkResponse(preReader, "250"); + sendLine(preWriter, "STARTTLS"); + checkResponse(preReader, "220"); + SSLSocketFactory factory = + (SSLSocketFactory) + SSLSocketFactory + .getDefault(); + SSLSocket sslSocket = (SSLSocket) + factory.createSocket( + plainSocket, host, + port, true); + sslSocket.startHandshake(); + doSmtp(sslSocket, host, user, pass, + to, encSubject, encBody, + fromName); + sslSocket.close(); + } + logger.info( + "[Sdf1_login] 邮件发送成功 -> " + + to); + return true; + } catch (Exception e) { + logger.severe( + "[Sdf1_login] 邮件异常: " + + e.getClass() + .getSimpleName() + + ": " + e.getMessage()); + if (e.getCause() != null) { + logger.severe( + "[Sdf1_login] 根因: " + + e.getCause() + .getMessage()); + } + return false; + } + } + + private void doSmtp(SSLSocket socket, + String host, String user, String pass, + String to, String subject, String body, + String fromName) throws Exception { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + socket.getInputStream(), + "UTF-8")); + OutputStream rawOut = + socket.getOutputStream(); + PrintWriter writer = + new PrintWriter( + new java.io.BufferedOutputStream( + rawOut), true); + + checkResponse(reader, "220"); + sendLine(writer, "EHLO " + host); + checkResponse(reader, "250"); + sendLine(writer, "AUTH LOGIN"); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + user.getBytes("UTF-8"))); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder() + .encodeToString( + pass.getBytes("UTF-8"))); + checkResponse(reader, "235"); + sendLine(writer, + "MAIL FROM:<" + user + ">"); + checkResponse(reader, "250"); + sendLine(writer, + "RCPT TO:<" + to + ">"); + checkResponse(reader, "250"); + sendLine(writer, "DATA"); + checkResponse(reader, "354"); + sendLine(writer, + "From: " + fromName + + " <" + user + ">"); + sendLine(writer, + "To: <" + to + ">"); + sendLine(writer, + "Reply-To: <" + user + ">"); + sendLine(writer, + "Subject: " + subject); + sendLine(writer, "MIME-Version: 1.0"); + sendLine(writer, + "Content-Type: text/plain; " + + "charset=UTF-8"); + sendLine(writer, + "Content-Transfer-Encoding: " + + "base64"); + sendLine(writer, ""); + sendLine(writer, body); + sendLine(writer, "."); + checkResponse(reader, "250"); + sendLine(writer, "QUIT"); + } + + private void sendLine(PrintWriter writer, + String line) { + writer.print(line + "\r\n"); + writer.flush(); + } + + private void checkResponse( + BufferedReader reader, + String expected) throws Exception { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + if (line.length() >= 4 + && Character.isDigit( + line.charAt(0)) + && line.charAt(3) == ' ') + break; + } + String resp = sb.toString().trim(); + if (!resp.startsWith(expected)) { + if (resp.startsWith("535")) + logger.severe( + "[Sdf1_login] SMTP 535 " + + "认证失败"); + throw new Exception( + "SMTP响应异常: 期望 " + + expected + + ",实际: " + resp); + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/GUIManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/GUIManager.java new file mode 100644 index 0000000..cbc7102 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/GUIManager.java @@ -0,0 +1,2681 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + + + + +import java.util.*; + +public class GUIManager implements Listener { + + private final Main plugin; + + public static final String T_MAIN = + "§6§l服务器菜单"; + public static final String T_MORE = + "§6§l更多功能"; + public static final String T_EDITOR = + "§6§l编辑菜单项"; + public static final String T_BIOME = + "§b§l目标群系传送"; + public static final String T_MY_INFO = + "§6§l我的信息"; + public static final String T_GIFT_STAGES = + "§6§l礼包阶段"; + public static final String T_ADMIN = + "§6§l管理员面板"; + public static final String T_INVITE = + "§6§l邀请数据"; + public static final String T_USER_MGMT = + "§6§l用户管理"; + public static final String T_TASK_CENTER = + "§6§l任务中心"; + private final Map userMgmtPages = new HashMap<>(); + private static final int UMGMT_PAGE_SIZE = 45; + + + + public GUIManager(Main plugin) { + this.plugin = plugin; + } + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_MAIN); +// ===== 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + + // 已占用的槽位 + Set occupied = new HashSet<>(); + occupied.add(10); + occupied.add(12); + occupied.add(14); + occupied.add(16); + occupied.add(19); + occupied.add(21); + occupied.add(23); + occupied.add(25); + occupied.add(29); + occupied.add(31); + occupied.add(32); + occupied.add(34); + + // 自定义区域:第1排(0-8) + 第5排(36-44) + 第6排(45-53) + int[] customArea = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53 + }; + + int autoIdx = 0; + for (int ci = 0; + ci < customs.size() && ci < 27; ci++) { + MenuManager.MenuItem mi = + customs.get(ci); + + // 权限过滤 + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + + int targetSlot = -1; + + // 玩家指定了格子? + if (mi.slot != null) { + targetSlot = findBestSlot( + mi.slot - 1, occupied); + } + + // 未指定或冲突 → 自动排队 + if (targetSlot < 0) { + while (autoIdx < customArea.length) { + int s = customArea[autoIdx++]; + if (!occupied.contains(s)) { + targetSlot = s; + break; + } + } + } + + if (targetSlot < 0 + || targetSlot >= 54) continue; + occupied.add(targetSlot); + + // 放置物品 + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(java.util.Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(targetSlot, icon); + } + } + +// 背景填充 + fillBg(g); + p.openInventory(g); + + // ===== Row1(9-17): 核心功能 ===== + g.setItem(10, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(12, mkItem(Material.PAPER, + "§b§l邀请数据")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + g.setItem(16, mkItem(Material.COMPASS, + "§b§l群系传送", + "§7传送到指定群系")); + + // ===== Row2(18-26): 日常功能 ===== + g.setItem(19, mkItem(Material.BOOK, + "§6工单系统")); + g.setItem(21, mkItem(Material.EMERALD_BLOCK, + "§a§l每日签到", + "§7签到获取积分和惊喜奖励")); + g.setItem(23, mkItem(Material.ENDER_CHEST, + "§6§l任务中心")); + if (p.isOp() || isAdmin(p)) { + g.setItem(25, mkItem(Material.HOPPER, + "§6§l垃圾回收站", + "§7查看和取回清理的物品")); + + + } + // ===== Row3(27-35): 管理功能 ===== + // ★ 商店 - 所有人都能看 ★ + g.setItem(27, mkItem(Material.CHEST, + "§6§l商店", + "§7浏览、购买、出售物品", + "§7货币: 债券")); + + if (isAdmin(p)) { + g.setItem(29, mkItem( + Material.REDSTONE_BLOCK, + "§c§l管理员面板")); + } + if (p.isOp()) { + g.setItem(31, mkItem(Material.ECHO_SHARD, + "§c§l管理菜单", + "§7添加、编辑、删除菜单项")); + } + // 在 openMain 方法中,Row3 区域加: + + // ★ 余额查询 - 所有人都能看 ★ + double myBal = 0; + try { + var reg = plugin.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault.economy + .Economy.class); + if (reg != null) + myBal = reg.getProvider() + .getBalance(p); + } catch (Exception ignored) {} + int myBonds = plugin.getBonds().getBonds(p.getName()); + g.setItem(32, mkItem(Material.EMERALD, + "§b§l我的钱包", + "§7查看债券余额与流水记录")); + +// ★ 余额操作 - 仅OP和tag玩家 ★ + if (isAdmin(p)) { + g.setItem(34, mkItem(Material.GOLD_INGOT, + "§6§l余额操作", + "§7给钱、扣钱")); + } + + + + // ===== Row0(0-8): 自定义菜单项 ===== + if (plugin.getMenu() != null) { + List customs = + plugin.getMenu().getItems(); + for (int i = 0; + i < customs.size() && i < 9; + i++) { + MenuManager.MenuItem mi = + customs.get(i); + if ("OP".equals(mi.permType) + && !p.isOp() + && !isAdmin(p)) continue; + ItemStack icon = + mi.icon.clone(); + ItemMeta im = + icon.getItemMeta(); + im.setDisplayName( + "§e§l" + mi.title); + im.setLore(Arrays.asList( + "§7指令: §f" + + mi.command, + "§7权限: §f" + + mi.permType, + "", + "§a左键执行")); + icon.setItemMeta(im); + g.setItem(i, icon); + } + } + + // 背景最后填充(不覆盖物品) + fillBg(g); + p.openInventory(g); + } + private int findBestSlot( + int requested, + Set occupied) { + // 目标位空闲直接用 + if (requested >= 0 && requested < 54 + && !occupied.contains(requested)) { + return requested; + } + // BFS上下左右找最近空位 + int row = requested / 9; + int col = requested % 9; + boolean[][] visited = + new boolean[6][9]; + java.util.Queue queue = + new java.util.LinkedList<>(); + queue.add(new int[]{row, col}); + visited[row][col] = true; + int[][] dirs = { + {-1, 0}, {1, 0}, + {0, -1}, {0, 1}}; + while (!queue.isEmpty()) { + int[] cur = queue.poll(); + for (int[] d : dirs) { + int nr = cur[0] + d[0]; + int nc = cur[1] + d[1]; + if (nr >= 0 && nr < 6 + && nc >= 0 && nc < 9 + && !visited[nr][nc]) { + visited[nr][nc] = true; + int s = nr * 9 + nc; + if (!occupied.contains(s)) { + return s; + } + queue.add(new int[]{nr, nc}); + } + } + } + return -1; + } + + public void openMenuManager(Player p) { + List list = + plugin.getMenu().getItems(); + Inventory g = Bukkit.createInventory( + null, 27, + "§c§l菜单管理"); + fillBg(g); + + for (int i = 0; + i < list.size() && i < 9; i++) { + MenuManager.MenuItem mi = list.get(i); + g.setItem(i, mkItem( + mi.icon.getType(), + "§e" + mi.title, + "§7指令: " + mi.command, + "§7权限: " + mi.permType, + "", + "§a左键编辑", + "§7右键切换权限")); + } + + g.setItem(22, mkItem(Material.LIME_WOOL, + "§a§l添加菜单项")); + + g.setItem(26, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + + public void openBalanceOps(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作"); + fillBg(g); + + net.milkbowl.vault.economy.Economy eco = null; + try { + var reg = plugin.getServer().getServicesManager() + .getRegistration(net.milkbowl.vault.economy.Economy.class); + if (reg != null) eco = reg.getProvider(); + } catch (Exception ignored) {} + + int slot = 0; + for (Player op : Bukkit.getOnlinePlayers()) { + if (slot >= 45) break; + double bal = 0; + if (eco != null) bal = eco.getBalance(op); + boolean isSelf = op.getName().equals(p.getName()); + int bondBal = plugin.getBonds().getBonds(op.getName()); + g.setItem(slot, mkItem( + isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD, + "§e" + op.getName(), + "§7金币: §a$" + String.format("%.2f", bal), + "§7债券: §e" + bondBal + " §6枚", + "", + "§a左键经济(±) §6右键债券(±)")); + slot++; + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openShop(Player p) { + p.closeInventory(); + plugin.getShopManager().openShopMain(p); + } + + public void openTicket(Player p) { + p.closeInventory(); + p.performCommand("sdf1_login ticket"); + } + @EventHandler + public void onInvClose( + + InventoryCloseEvent e) { + if (!(e.getPlayer() instanceof Player)) + return; + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + Player p3 = (Player) e.getPlayer(); + String t = e.getView().getTitle(); + if (!(e.getPlayer() instanceof Player)) return; + Player p2 = (Player) e.getPlayer(); + + if (!t.equals(T_EDITOR)) return; + + // ★ 编辑器关闭时清理空项 ★ + List list = + plugin.getMenu().getItems(); + if (!list.isEmpty()) { + MenuManager.MenuItem last = + list.get(list.size() - 1); + if ((last.command == null + || last.command.trim().isEmpty() + || last.command.trim() + .equals("/")) + && "新菜单项" + .equals(last.title)) { + list.remove(list.size() - 1); + } + } + } + + public void openSubMenu( + Player p, String fileName) { + String menuTitle = + "§6§l" + fileName + .replace(".txt", ""); + Inventory g = Bukkit.createInventory( + null, 54, menuTitle); + fillBg(g); + + java.io.File f = new java.io.File( + plugin.getDataFolder(), fileName); + if (!f.exists()) { + p.sendMessage("§c文件不存在: " + + fileName); + return; + } + try { + List lines = + java.nio.file.Files + .readAllLines( + f.toPath(), + java.nio.charset + .StandardCharsets + .UTF_8); + List items = + new ArrayList<>(); + StringBuilder buf = + new StringBuilder(); + boolean inItem = false; + + for (String raw : lines) { + String l = raw.trim(); + if (l.startsWith("#") + || l.startsWith("//")) + continue; + if (l.contains("")) { + inHtml = false; + l = l.substring( + l.indexOf("-->") + 3) + .trim(); + } else { + plugin.getLogger().info( + "[Menu] L" + lineNum + + ": [HTML注释中]"); + continue; + } + } + + // ===== 第3优先:整行注释(最高裁决) ===== + // 以 # 或 // 开头的行,直接跳过 + // 不管后面包含什么 /* "); + pw.println(); + pw.println("{"); + pw.println(" 标题:主城传送"); + pw.println(" 指令:/spawn"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:COMPASS"); + pw.println("}"); + pw.println(); + pw.println("{"); + pw.println(" 标题:随机传送"); + pw.println(" 指令:/rtp"); + pw.println(" 权限类型:玩家"); + pw.println(" 图标:ENDER_PEARL"); + pw.println("}"); + } catch (Exception ignored) { + } + } + + // ===== 保存 ===== + public void saveMenu() { + File f = new File( + plugin.getDataFolder(), + "菜单.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 服务器菜单"); + pw.println(); + for (MenuItem mi : items) { + pw.println("{"); + pw.println(" 标题:" + + mi.title); + pw.println(" 指令:" + + mi.command); + pw.println(" 权限类型:" + + mi.permType); + // ★ 保存实际材质名 ★ + pw.println(" 图标:" + + mi.icon.getType().name()); + pw.println("}"); + pw.println(); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Menu] 保存失败: " + + e.getMessage()); + } + } + + // ===== 聊天编辑 ===== + public boolean isEditing(String name) { + return chatWait.contains(name); + } + + public void editTitle( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "title"); + p.closeInventory(); + p.sendMessage("§e请输入菜单标题:"); + } + + public void editCommand( + Player p, int idx) { + chatIdx.put(p.getName(), idx); + chatWait.add(p.getName()); + chatField.put(p.getName(), "command"); + p.closeInventory(); + p.sendMessage("§e请输入指令" + + "(自动补全/):"); + } + + public void onChat(Player p, String input) { + String name = p.getName(); + if (!chatWait.remove(name)) return; + + String field = chatField.remove(name); + Integer idx = chatIdx.get(name); + if (idx == null || idx >= items.size()) return; + + MenuItem mi = items.get(idx); + + if ("title".equals(field)) { + mi.title = input; + p.sendMessage("§a标题: " + input); + } else if ("command".equals(field)) { + if (!input.startsWith("/")) + input = "/" + input; + mi.command = input; + p.sendMessage("§a指令: " + input); + } + + Bukkit.getScheduler().runTask(plugin, () -> + plugin.gui.openEditor(p, idx)); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/OrderManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/OrderManager.java new file mode 100644 index 0000000..9dcb625 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/OrderManager.java @@ -0,0 +1,1345 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; + +public class OrderManager implements Listener { + private final Main plugin; + private final List orders = + Collections.synchronizedList(new ArrayList<>()); + private final Map userViewPage = new HashMap<>(); + private final Map adminViewPage = new HashMap<>(); + private final Map adminViewTarget = new HashMap<>(); + private final Set partialRefundListening = new HashSet<>(); + private final Map partialRefundTarget = new HashMap<>(); + private final Map receiptBuilderSection = new HashMap<>(); + private String tplHeader = ""; + private String tplItemLine = "{品名} x{数量} {原价}→{执行价} 小计{小计}枚"; + private String tplDiscountLine = "优惠: -{优惠额}枚 ({优惠类型})"; + private String tplPackLine = "打包: +{打包费}枚 ({打包颜色})"; + private String tplSummary = "原价{原价合计} → 实付{实付}"; + private String tplFooter = "感谢惠顾,欢迎再来!"; + private static final long RETENTION_30D = 30L * 24 * 60 * 60 * 1000; + private static final long SELF_REFUND_WINDOW = 5 * 60 * 1000; + private static final int PAGE_SIZE = 21; + private static final int[] SLOTS = { + 10,11,12,13,14,15,16,19,20,21,22,23,24,25,28,29,30,31,32,33,34}; + // ===== 字段 ===== + private final Set shulkerUsedOrders = + Collections.synchronizedSet(new HashSet<>()); + // ===== 待审批的退货退款 ===== + private final Map pendingReturns = + new HashMap<>(); + // ===== 退货申请确认 ===== + private final Map returnConfirmWaiting = + new HashMap<>(); // uuid -> orderId + private static final int RETURN_CONFIRM_TIMEOUT = 600; // 30秒 + + + private static class PendingReturn { + long orderId; + UUID playerUUID; + String playerName; + int totalPaid; + long createTime; + String packColor; + String packType; + List items; + + PendingReturn(OrderRecord r) { + this.orderId = r.orderId; + this.playerUUID = r.uuid; + this.playerName = r.player; + this.totalPaid = r.totalPaid; + this.createTime = System.currentTimeMillis(); + this.packColor = r.packColor; + this.packType = r.packType; + this.items = new ArrayList<>(r.items); + } + } + + + public static class OrderItem { + public String name; public String mat; + public int originalPrice; public int finalPrice; public int qty; + public OrderItem(String n,String m,int op,int fp,int q){ + name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;} + public int subtotal(){return finalPrice*qty;} + public int originalSubtotal(){return originalPrice*qty;} + } + public static class OrderRecord { + public long orderId; public UUID uuid; public String player; + public List items; public int totalOriginal; public int totalPaid; + public int discount; public String discountType; public String couponCode; + public int packFee; public String packType; public String packColor; + public long timestamp; public long printTime; public int status; + public int refundedAmount; public String refundType; + } + + public OrderManager(Main plugin) { + this.plugin = plugin; + loadReceiptConfig(); + loadOrders(); + startCleanupTask(); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + public List getOrders() { return orders; } + + // ===== 小票模板 ===== + private void loadReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + if (!f.exists()) createDefaultReceiptConfig(f); + Map m = loadMap(f); + tplHeader = m.getOrDefault("头部", tplHeader); + tplItemLine = m.getOrDefault("商品行", tplItemLine); + tplDiscountLine = m.getOrDefault("优惠行", tplDiscountLine); + tplPackLine = m.getOrDefault("打包行", tplPackLine); + tplSummary = m.getOrDefault("汇总", tplSummary); + tplFooter = m.getOrDefault("页脚", tplFooter); + } + private void saveReceiptConfig() { + File f = new File(plugin.getDataFolder(), "小票设置.txt"); + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=" + tplHeader); + L.add("商品行=" + tplItemLine); + L.add("优惠行=" + tplDiscountLine); + L.add("打包行=" + tplPackLine); + L.add("汇总=" + tplSummary); + L.add("页脚=" + tplFooter); + writeLines(f, L); + } + private void createDefaultReceiptConfig(File f) { + List L = new ArrayList<>(); + L.add("# 小票模板配置"); + L.add("头部=§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}"); + L.add("商品行=§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚"); + L.add("优惠行=优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})"); + L.add("打包行=打包: §c+{打包费}枚§r ({打包颜色})"); + L.add("汇总=原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚"); + L.add("页脚=感谢惠顾,欢迎再来!"); + writeLines(f, L); + } + + // ===== 订单记录 ===== + public void recordOrder(Player p, List items, + int totalOriginal, int totalPaid, int discount, + String discountType, String couponCode, int packFee, + String packType, String packColor) { + OrderRecord r = new OrderRecord(); + r.orderId = System.currentTimeMillis(); + r.uuid = p.getUniqueId(); r.player = p.getName(); + r.items = items; r.totalOriginal = totalOriginal; + r.totalPaid = totalPaid; r.discount = discount; + r.discountType = discountType; r.couponCode = couponCode; + r.packFee = packFee; r.packType = packType; + r.packColor = packColor; r.timestamp = r.orderId; + r.printTime = 0; r.status = 0; r.refundedAmount = 0; + r.refundType = null; + orders.add(r); saveOrders(); + } + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + // ===== 小票聊天栏 ===== + /** + * 聊天栏打印小票(给定参数 Player + OrderRecord) + * 不要改动方法签名 + */ + public void sendReceiptChat(Player p, OrderRecord r) { + if (p == null || r == null) return; + + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + p.sendMessage("§7§m "); + p.sendMessage("§6§l======== 商店购物小票 ========"); + + // 退款标记 + String statusTag = getStatusTag(r); + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§7订单号: §f#" + r.orderId); + p.sendMessage("§7下单时间: §f" + + df.format(new Date(r.timestamp))); + p.sendMessage("§7玩家: §f" + r.player); + p.sendMessage("§7------------------------------"); + + for (OrderItem it : r.items) { + String price; + if (it.originalPrice != it.finalPrice) { + price = "§f" + it.originalPrice + + "§7→§6" + it.finalPrice; + } else { + price = "§6" + it.finalPrice; + } + p.sendMessage("§7 §e" + it.name + + " §7x" + it.qty + + " " + price + "枚" + + " §6小计" + it.subtotal() + "枚"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7原价: §f" + r.totalOriginal + "枚"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "§7(优惠券 -" + pct + "%)" + : "eco".equals(r.discountType) + ? "§7(环保 -" + pct + "%)" + : "§7(-" + pct + "%)"; + p.sendMessage("§7优惠: §a-" + r.discount + + "枚 " + dt); + } + + if (r.packFee > 0) { + String cn = r.packColor != null + ? r.packColor + .replace("_SHULKER_BOX", "") + .replace("SHULKER_BOX", "原色") + : "默认"; + p.sendMessage("§7打包: §c+" + r.packFee + + "枚 §7(" + cn + "潜影盒)"); + } else { + p.sendMessage("§7包装: 直接放入背包"); + } + + p.sendMessage("§7------------------------------"); + p.sendMessage("§7实付: §6" + r.totalPaid + "枚"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + p.sendMessage("§7余额: §a" + bal + "枚"); + + // 退款标记 + if (!statusTag.isEmpty()) { + p.sendMessage(statusTag); + } + + p.sendMessage("§6§l=============================="); + r.printTime = System.currentTimeMillis(); + } + + /** + * 生成书本小票内容 + */ + private String buildReceiptText(OrderRecord r) { + String statusTag = getStatusTag(r); + SimpleDateFormat df = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + StringBuilder sb = new StringBuilder(); + + sb.append("§6§l===== 商店小票 =====\n"); + + // 退款标记 + String tag = getStatusTag(r); + if (!tag.isEmpty()) sb.append(tag).append("\n"); + + sb.append("订单号: #").append(r.orderId).append("\n"); + sb.append("时间: ").append( + df.format(new Date(r.timestamp))).append("\n"); + sb.append("玩家: ").append(r.player).append("\n"); + sb.append("------------------------------\n"); + + for (OrderItem it : r.items) { + String pr = (it.originalPrice != it.finalPrice) + ? it.originalPrice + "->" + it.finalPrice + : String.valueOf(it.finalPrice); + sb.append(it.name).append(" x").append(it.qty) + .append(" ").append(pr).append("枚") + .append(" 小计").append(it.subtotal()) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("原价: ").append(r.totalOriginal) + .append("枚\n"); + + if (r.discount > 0) { + int pct = (r.totalOriginal > 0) + ? (r.discount * 100 / r.totalOriginal) + : 0; + String dt = "coupon".equals(r.discountType) + ? "优惠券(-" + pct + "%)" + : "环保(-" + pct + "%)"; + sb.append("优惠: -").append(r.discount) + .append("枚 ").append(dt).append("\n"); + } + + if (r.packFee > 0) { + sb.append("打包: +").append(r.packFee) + .append("枚\n"); + } + + sb.append("------------------------------\n"); + sb.append("实付: ").append(r.totalPaid).append("枚\n"); + + int bal = 0; + try { + bal = plugin.getBonds().getBonds(r.player); + } catch (Exception ignored) {} + sb.append("余额: ").append(bal).append("枚\n"); + + // 退款标记 + if (!statusTag.isEmpty()) sb.append("\n").append(tag).append("\n"); + + return sb.toString(); + } + + /** + * 退款状态标记文本 + */ + private String getStatusTag(OrderRecord r) { + switch (r.status) { + case 2: + return "§c§l<<<<<<< 已全额退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 3: + return "§c§l<<<<<<< 已部分退款 >>>>>>>" + + "\n§c退款金额: " + + r.refundedAmount + "枚"; + case 4: + return "§c§l<<<<<<< 退款申请已拒绝 >>>>>>>"; + default: + return ""; + } + } + + /** + * 创建小票书 + */ + public ItemStack createReceiptBook(OrderRecord r) { + String text = buildReceiptText(r); + String[] lines = text.split("\n"); + + ItemStack book = new ItemStack(Material.WRITTEN_BOOK); + BookMeta bm = (BookMeta) book.getItemMeta(); + bm.setTitle("购物小票 #" + r.orderId); + bm.setAuthor("商店"); + + StringBuilder page = new StringBuilder(); + int lineCount = 0; + for (String line : lines) { + // ★ 保留颜色代码 + page.append(line).append("\n"); + lineCount++; + if (lineCount >= 13) { + bm.addPage(page.toString().trim()); + page = new StringBuilder(); + lineCount = 0; + } + } + if (page.length() > 0) { + bm.addPage(page.toString().trim()); + } + + book.setItemMeta(bm); + return book; + } + + // ===== 退款工具 ===== + private boolean withinSelfRefundWindow(OrderRecord r) { return System.currentTimeMillis() - r.timestamp < SELF_REFUND_WINDOW; } + private int countDirectItems(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private int countShulkers(Player p, String matName) { + Material m = Material.matchMaterial(matName); if (m == null) return 0; + int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount(); + return c; + } + private void removeItems(Player p, String matName, int amt) { + Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt)); + } + private boolean removeOneShulker(Player p, + String materialName) { + // 先尝试精确匹配 + Material mat = Material.matchMaterial(materialName); + if (mat != null) { + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + return true; + } + } + } + // 匹配不到,移除任意一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .endsWith("_SHULKER_BOX")) { + p.getInventory().clear(i); + return true; + } + } + return false; + } + public String validateRefund(OrderRecord r, Player p) { + if (r.status != 0) + return "订单状态: " + getStatusName(r.status); + + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // ★ 检查:潜影盒是否被放置过 + if (shulkerUsedOrders.contains(r.orderId)) { + return "§c潜影盒已被使用过,无法自助退款"; + } + + // ★ 检查:背包里是否有同一订单的潜影盒 + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im == null || im.getLore() == null) + continue; + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + return null; // 找到了,可退 + } + } + } + return "§c对应的潜影盒已不在背包中"; + } else { + // ★ 散装:检测物品数量 + for (OrderItem it : r.items) { + int have = countDirectItems(p, it.mat); + if (have < it.qty) + return "§c" + it.name + "不足(有" + + have + "/" + it.qty + ")"; + } + } + return null; + } + + + + private void notifyPlayer(UUID uuid, String msg) { Player p = Bukkit.getPlayer(uuid); if (p != null && p.isOnline()) p.sendMessage(msg); } + private void notifyAdmins(Player p, OrderRecord r) { + String m = "§e[订单] " + p.getName() + " 申请退款 #" + r.orderId + " §6" + r.totalPaid + "枚"; + for (Player a : Bukkit.getOnlinePlayers()) if (isAdmin(a) && !a.equals(p)) a.sendMessage(m); + } + private String getStatusName(int s) { switch(s){case 0:return "正常";case 1:return "申请中";case 2:return "已退";case 3:return "部分退";case 4:return "已拒绝";default:return "未知";} } + private boolean isAdmin(Player p) { + if (p.isOp()) return true; + try { ConfigManager cm = plugin.getConfigMgr(); if (cm == null) return false; return p.hasPermission("sdf1.admin") || p.hasPermission("sdf1." + cm.adminTag); } catch (Exception e) { return p.hasPermission("sdf1.admin"); } + } + + // ===== 订单中心 ===== + public void openOrderCenter(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l订单中心"); + g.setItem(13, mkItem(Material.BOOK, "§e§l购买记录", "§7查看历史订单", "", "§a点击进入")); + if (isAdmin(p)) { g.setItem(31, mkItem(Material.NETHER_STAR, "§c§l管理员面板", "§7在线玩家购物记录", "§7支持退款操作", "", "§a点击进入")); + g.setItem(33, mkItem(Material.CRAFTING_TABLE, "§e§l小票配置", "§7配置小票模板样式", "", "§a点击进入")); } + g.setItem(49, mkItem(Material.ARROW, "§7返回商店")); + p.openInventory(g); + } + + // ===== 购买记录 ===== + public void openMyOrders(Player p, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l购买记录"); + List list = getPlayerAllOrders(p.getUniqueId()); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + userViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon; String st; String act; + if (r.status != 0) { icon = r.status == 1 ? Material.PAPER : Material.BARRIER; st = "§7状态: " + getStatusName(r.status); act = "§b左键: 补打小票"; } + else if (withinSelfRefundWindow(r)) { icon = Material.EMERALD; st = "§7状态: §a可退款(5分钟内)"; act = "§b左键: 补打小票\n§a右键: 退款退货\n§eShift+右键: 仅退款"; } + else { icon = Material.BOOK; st = "§7状态: §7已过自助退款期"; act = "§b左键: 补打小票\n§e右键: 申请退款退货\n§6Shift+右键: 申请仅退款"; } + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7原价: §f" + r.totalOriginal + "枚"); + if (r.discount > 0) lore.add("§7优惠: §a-" + r.discount + "枚"); + lore.add("§7实付: §6" + r.totalPaid + "枚"); lore.add(st); lore.add(""); + for (String line : act.split("\n")) lore.add(line); + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7暂无订单记录")); + addPageButtons(g, page, total); + p.openInventory(g); + } + + // ===== 管理员面板 ===== + public void openAdminPanel(Player p, int page) { + if (!isAdmin(p)) { p.sendMessage("§c权限不足"); return; } + Inventory g = Bukkit.createInventory(null, 54, "§c§l管理员订单面板"); + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int total = Math.max(1, (int) Math.ceil((double) online.length / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); adminViewPage.put(p.getUniqueId(), page); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, online.length); + // ★ 替换 openAdminPanel 中的物品创建部分 + for (int i = start; i < end; i++) { + int si = i - start; + if (si >= SLOTS.length) break; + Player t = online[i]; + List tl = getPlayerOrdersWithin( + t.getUniqueId(), RETENTION_30D); + int ts = 0; + for (OrderRecord r : tl) ts += r.totalPaid; + + ItemStack is = new ItemStack(Material.PLAYER_HEAD); + org.bukkit.inventory.meta.SkullMeta sm = + (org.bukkit.inventory.meta.SkullMeta) + is.getItemMeta(); + if (sm != null) { + sm.setOwner(t.getName()); // ★ 设置头颅皮肤 + sm.setDisplayName("§e" + t.getName()); + sm.setLore(Arrays.asList( + "§7订单: §f" + tl.size() + "笔", + "§7消费: §6" + ts + "枚", + "", "§a点击进入")); + is.setItemMeta(sm); // ★ 这行之前缺失了 + } + g.setItem(SLOTS[si], is); + } + + if (online.length == 0) g.setItem(31, mkItem(Material.BARRIER, "§7无在线玩家")); + addPageButtons(g, page, total); p.openInventory(g); + } + + // ===== 管理员订单 ===== + public void openAdminOrders(Player p, UUID targetUUID, int page) { + Inventory g = Bukkit.createInventory(null, 54, "§c§l订单管理"); + String name = Bukkit.getOfflinePlayer(targetUUID).getName(); + List list = getPlayerOrdersWithin(targetUUID, RETENTION_30D); + int total = Math.max(1, (int) Math.ceil((double) list.size() / PAGE_SIZE)); + page = Math.max(0, Math.min(page, total - 1)); + adminViewPage.put(p.getUniqueId(), page); adminViewTarget.put(p.getUniqueId(), targetUUID); + int start = page * PAGE_SIZE; int end = Math.min(start + PAGE_SIZE, list.size()); + for (int i = start; i < end; i++) { + int si = i - start; if (si >= SLOTS.length) break; + OrderRecord r = list.get(i); + Material icon = r.status == 2 || r.status == 4 ? Material.BARRIER : r.status == 1 ? Material.PAPER : Material.BOOK; + ItemStack is = new ItemStack(icon); ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e订单 #" + r.orderId); + List lore = new ArrayList<>(); lore.add("§7" + fmtDate(r.timestamp)); + for (OrderItem it : r.items) lore.add("§7 §e" + it.name + " §7x" + it.qty + " §6" + it.subtotal() + "枚"); + lore.add("§7─────────"); lore.add("§7实付: §6" + r.totalPaid + "枚"); + if (r.status == 1) { lore.add("§7状态: §e申请中(" + nn(r.refundType) + ")"); lore.add(""); lore.add("§a右键: 批准"); lore.add("§c左键: 拒绝"); } + else if (r.status == 2) lore.add("§7已退: §a" + r.refundedAmount + "枚"); + else if (r.status == 3) lore.add("§7部分退: §a" + r.refundedAmount + "枚"); + else if (r.status == 4) lore.add("§7已拒绝"); + else { lore.add(""); lore.add("§a右键: 整单退款"); lore.add("§e左键: 部分退款(输入%)"); } + im.setLore(lore); is.setItemMeta(im); + } + g.setItem(SLOTS[si], is); + } + if (list.isEmpty()) g.setItem(31, mkItem(Material.BARRIER, "§7该玩家暂无订单")); + addPageButtons(g, page, total); + g.setItem(49, mkItem(org.bukkit.Material.PAPER, name + " §e" + (page + 1) + "/" + total)); + p.openInventory(g); + } + private void addPageButtons(Inventory g, int page, int total) { + g.setItem(48, page > 0 ? mkItem(Material.ARROW, "§7上一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + if (g.getItem(49) == null) g.setItem(49, mkItem(Material.PAPER, "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 ? mkItem(Material.ARROW, "§7下一页") : mkItem(Material.BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + } + + // ===== 小票配置 ===== + public void openReceiptConfig(Player p) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l小票配置"); + g.setItem(10, mkItem(Material.PAPER, "§e§l头部", "§7" + brief(tplHeader), "", "§a点击编辑")); + g.setItem(11, mkItem(Material.PAPER, "§e§l商品行", "§7" + brief(tplItemLine), "", "§a点击编辑")); + g.setItem(12, mkItem(Material.PAPER, "§e§l优惠行", "§7" + brief(tplDiscountLine), "", "§a点击编辑")); + g.setItem(14, mkItem(Material.PAPER, "§e§l打包行", "§7" + brief(tplPackLine), "", "§a点击编辑")); + g.setItem(15, mkItem(Material.PAPER, "§e§l汇总", "§7" + brief(tplSummary), "", "§a点击编辑")); + g.setItem(16, mkItem(Material.PAPER, "§e§l页脚", "§7" + brief(tplFooter), "", "§a点击编辑")); + g.setItem(22, mkItem(Material.BOOK, "§6§l预览(随机价格)", "§7预览小票效果")); + g.setItem(48, mkItem(Material.LIME_WOOL, "§a§l保存")); g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(50, mkItem(Material.RED_WOOL, "§c§l重置")); + p.openInventory(g); + } + public void openReceiptBuilder(Player p, String section) { + Inventory g = Bukkit.createInventory(null, 54, "§6§l编辑: " + sectionName(section)); + g.setItem(10, mkItem(Material.EMERALD, "§a{品名}")); g.setItem(11, mkItem(Material.EMERALD, "§a{原价}")); + g.setItem(12, mkItem(Material.EMERALD, "§a{执行价}")); g.setItem(13, mkItem(Material.EMERALD, "§a{数量}")); + g.setItem(14, mkItem(Material.EMERALD, "§a{小计}")); g.setItem(15, mkItem(Material.EMERALD, "§a{商店名}")); + g.setItem(16, mkItem(Material.EMERALD, "§a{玩家}")); g.setItem(19, mkItem(Material.DIAMOND, "§b{订单号}")); + g.setItem(20, mkItem(Material.DIAMOND, "§b{订单时间}")); g.setItem(21, mkItem(Material.DIAMOND, "§b{打印时间}")); + g.setItem(22, mkItem(Material.DIAMOND, "§b{日期}")); g.setItem(23, mkItem(Material.DIAMOND, "§b{时间}")); + g.setItem(24, mkItem(Material.DIAMOND, "§b{原价合计}")); g.setItem(25, mkItem(Material.DIAMOND, "§b{实付}")); + g.setItem(28, mkItem(Material.GOLD_INGOT, "§6{优惠额}")); g.setItem(29, mkItem(Material.GOLD_INGOT, "§6{优惠类型}")); + g.setItem(30, mkItem(Material.GOLD_INGOT, "§6{优惠券码}")); g.setItem(31, mkItem(Material.GOLD_INGOT, "§6{打包费}")); + g.setItem(32, mkItem(Material.GOLD_INGOT, "§6{打包颜色}")); g.setItem(33, mkItem(Material.GOLD_INGOT, "§6{余额}")); + g.setItem(37, mkItem(Material.STONE, "§f═══")); g.setItem(38, mkItem(Material.STONE, "§f───")); + g.setItem(39, mkItem(Material.STONE, "§f═════════════")); g.setItem(40, mkItem(Material.PAPER, "§e换行(\\n)")); + g.setItem(47, mkItem(Material.RED_STAINED_GLASS_PANE, "§c删末尾")); g.setItem(48, mkItem(Material.RED_WOOL, "§c清空")); + g.setItem(49, mkItem(Material.ARROW, "§7返回")); g.setItem(50, mkItem(Material.LIME_WOOL, "§a保存")); + g.setItem(51, mkItem(Material.BLUE_WOOL, "§9重置")); + p.openInventory(g); + } + // ===== 点击处理 ===== + public boolean handleCenterClick(Player p, int raw) { + if (raw == 13) { openMyOrders(p, 0); return true; } + if (raw == 31 && isAdmin(p)) { openAdminPanel(p, 0); return true; } + if (raw == 33 && isAdmin(p)) { openReceiptConfig(p); return true; } + if (raw == 49) { plugin.getShopManager().openShopMain(p); return true; } + return true; + } + public boolean handleMyOrdersClick(Player p, int raw, boolean left, boolean shift) { + List list = getPlayerAllOrders(p.getUniqueId()); + int page = userViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openMyOrders(p, page - 1); return true; } + if (raw == 50) { openMyOrders(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (left) { + try { ItemStack book = createReceiptBook(r); + HashMap ov = p.getInventory().addItem(book); + for (ItemStack d : ov.values()) p.getWorld().dropItemNaturally(p.getLocation(), d); + r.printTime = System.currentTimeMillis(); p.sendMessage("§a已补打小票书: #" + r.orderId); + } catch (Exception e) { p.sendMessage("§c补打失败"); } + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // ★ Shift+右键:仅退款申请(需管理员审批) + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请,等待管理员审批"); + notifyAdmins(p, r); + } else { + // ★ 普通右键:5分钟内直接退(自动通过) + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason + + ",Shift+右键可申请仅退款"); + } else { + // 扣货 + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + if (isShulker) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory() + .getItem(i); + if (is != null && is.getType() + .name().contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + // 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + } + + + } else { + if (r.status != 0) { + p.sendMessage("§c状态: " + + getStatusName(r.status)); + } else if (withinSelfRefundWindow(r)) { + if (shift) { + // 仅退款申请 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } else { + // 5分钟内退货退款 + String reason = validateRefund(r, p); + if (reason != null) { + // ★ 不满足条件:询问是否申请 + p.sendMessage("§c" + reason); + askReturnConfirm(p, r); + } else { + // 满足条件:自动退 + doAutoReturn(p, r); + } + } + } else { + if (shift) { + // 申请仅退款 + r.status = 1; + r.refundType = "refund_only_apply"; + saveOrders(); + p.sendMessage("§e已提交仅退款申请"); + notifyAdmins(p, r); + } else { + // ★ 申请退货退款:先检查条件 + String reason = validateRefund(r, p); + if (reason != null) { + p.sendMessage("§c" + reason); + } + // ★ 无论是否满足条件都询问 + askReturnConfirm(p, r); + } + } + } + } + if (left) { + // 已退款订单仍然允许补打小票,但小票内会显示退款标记 + try { + ItemStack book = createReceiptBook(r); + HashMap overflow = + p.getInventory().addItem(book); + for (ItemStack drop : overflow.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + r.printTime = System.currentTimeMillis(); + if (r.status != 0) { + p.sendMessage("§e已补打小票(含退款标记): #" + + r.orderId); + } else { + p.sendMessage("§a已补打小票书: #" + + r.orderId); + } + } catch (Exception e) { + p.sendMessage("§c补打失败"); + } + } + + final int fp = page; Bukkit.getScheduler().runTask(plugin, () -> openMyOrders(p, fp)); + return true; + } + public boolean handleAdminClick(Player p, int raw) { + Player[] online = Bukkit.getOnlinePlayers().toArray(new Player[0]); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { openAdminPanel(p, page - 1); return true; } + if (raw == 50) { openAdminPanel(p, page + 1); return true; } + if (raw == 53) { openOrderCenter(p); return true; } + int si = slotIndex(raw); if (si < 0) return true; + int actual = si + page * PAGE_SIZE; if (actual < 0 || actual >= online.length) return true; + openAdminOrders(p, online[actual].getUniqueId(), 0); return true; + } + public boolean handleAdminOrdersClick(Player p, int raw, boolean left, boolean shift) { + UUID tu = adminViewTarget.get(p.getUniqueId()); + if (tu == null) return false; + List list = getPlayerOrdersWithin(tu, RETENTION_30D); + int page = adminViewPage.getOrDefault(p.getUniqueId(), 0); + if (raw == 48) { + openAdminOrders(p, tu, page - 1); + return true; + } + if (raw == 50) { + openAdminOrders(p, tu, page + 1); + return true; + } + if (raw == 53) { + openAdminPanel(p, 0); + return true; + } + int si = slotIndex(raw); + if (si < 0) return true; + int actual = si + page * PAGE_SIZE; + if (actual < 0 || actual >= list.size()) return true; + OrderRecord r = list.get(actual); + if (r.status == 2 || r.status == 3 || r.status == 4) { + p.sendMessage("§c状态: " + getStatusName(r.status)); + return true; + } + if (left) { + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款被拒绝"); + openAdminOrders(p, tu, page); + } else if (r.status == 0) { + partialRefundListening.add(p.getUniqueId()); + partialRefundTarget.put(p.getUniqueId(), r); + p.closeInventory(); + p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚"); + } + } else { + if (r.status == 0 || r.status == 1) { + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + } + } + // ★ 替换 handleAdminOrdersClick 中的审批逻辑 + if (left) { + // 左键 = 拒绝 + if (r.status == 1) { + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款: #" + r.orderId); + notifyPlayer(r.uuid, "§c退款申请已被拒绝"); + openAdminOrders(p, tu, page); + } + } else { + // 右键 = 批准 + if (r.status == 1) { + String rt = r.refundType != null + ? r.refundType : ""; + + // ★ 退款退货申请 → 检查潜影盒 + if (rt.equals("refund_return_apply")) { + String pt = r.packType != null + ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 检查玩家背包里有没有带订单标签的盒子 + Player target = Bukkit.getPlayer(r.uuid); + if (target == null || !target.isOnline()) { + p.sendMessage("§c玩家不在线,无法执行退货退款"); + openAdminOrders(p, tu, page); + return true; + } + + boolean foundTagged = false; + boolean foundAny = false; + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is == null) continue; + if (!is.getType().name() + .contains("SHULKER")) continue; + foundAny = true; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + foundTagged = true; + // 收走盒子 + target.getInventory().clear(i); + break; + } + } + } + if (foundTagged) break; + } + + if (foundTagged) { + // ★ 有带标记的盒子 → 收走 + 退钱 + plugin.getBonds().addBonds( + r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + + r.player + " §6" + + r.totalPaid + "枚(盒子已收走)"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + return true; + } else if (foundAny) { + // ★ 有盒子但没标记 → 等管理员决定 + pendingReturns.put(r.orderId, + new PendingReturn(r)); + p.closeInventory(); + p.sendMessage("§e§l[退货审批] 订单 #" + + r.orderId); + p.sendMessage("§7玩家背包有潜影盒但" + + "无订单标记,可能已被替换"); + p.sendMessage("§a输入 §e1 §a= 收走盒子并退款"); + p.sendMessage("§e输入 §e2 §e= 仅退款(不收盒子)"); + p.sendMessage("§c输入 §c3 §c= 拒绝退款"); + p.sendMessage("§72分钟内未操作自动拒绝"); + // 启动2分钟超时 + startReturnTimeout(r.orderId); + return true; + } else { + // ★ 背包里没有任何盒子 + p.sendMessage("§c玩家背包里没有潜影盒"); + p.sendMessage("§7可拒绝并通知玩家" + + "申请仅退款或把货拿回来"); + openAdminOrders(p, tu, page); + return true; + } + } + // ★ 散装退货退款:扣货+退钱 + Player target = Bukkit.getPlayer(r.uuid); + if (target != null && target.isOnline()) { + for (OrderItem it : r.items) + removeItems(target, it.mat, it.qty); + } + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退货退款成功,§6" + + r.totalPaid + "枚"); + openAdminOrders(p, tu, page); + return true; + } + + // ★ 仅退款申请 / 5分钟内自动退款 + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + saveOrders(); + p.sendMessage("§a已批准退款 §e" + r.player + + " §6" + r.totalPaid + "枚"); + notifyPlayer(r.uuid, + "§a[订单] 退款已批准,§6" + + r.totalPaid + "枚已退还"); + openAdminOrders(p, tu, page); + } + } + return true; + } + + /** + * 询问用户是否提交退货退款申请 + */ + private void askReturnConfirm(Player p, OrderRecord r) { + returnConfirmWaiting.put(p.getUniqueId(), r.orderId); + p.sendMessage(""); + p.sendMessage("§e§l[退货退款申请]"); + p.sendMessage("§7订单 #" + r.orderId + + " 实付" + r.totalPaid + "枚"); + p.sendMessage("§a输入 §e1 §a= 提交退货退款申请"); + p.sendMessage("§c输入 §e2 §c= 取消"); + p.sendMessage("§730秒内未操作自动取消"); + p.sendMessage(""); + + // 30秒超时 + final UUID uuid = p.getUniqueId(); + final long oid = r.orderId; + new BukkitRunnable() { + @Override + public void run() { + Long waiting = returnConfirmWaiting.get(uuid); + if (waiting != null && waiting == oid) { + returnConfirmWaiting.remove(uuid); + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§7退货退款申请已超时取消"); + } + } + } + }.runTaskLater(plugin, RETURN_CONFIRM_TIMEOUT); + } + + /** + * 执行5分钟内自动退货退款 + */ + private void doAutoReturn(Player p, OrderRecord r) { + String pt = r.packType != null ? r.packType : "none"; + boolean isShulker = pt.equals("custom") + || pt.equals("default") + || pt.equals("shulker"); + + if (isShulker) { + // 找并收走带标签的盒子 + boolean found = false; + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is == null) continue; + if (!is.getType().name().contains("SHULKER")) + continue; + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + r.orderId)) { + p.getInventory().clear(i); + found = true; + break; + } + } + } + if (found) break; + } + // 没找到标签盒子,收任意潜影盒 + if (!found) { + for (int i = 0; i < 36; i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + p.getInventory().clear(i); + break; + } + } + } + } else { + for (OrderItem it : r.items) + removeItems(p, it.mat, it.qty); + } + + plugin.getBonds().addBonds(r.player, r.totalPaid); + r.status = 2; + r.refundedAmount = r.totalPaid; + r.refundType = "auto_return"; + saveOrders(); + p.sendMessage("§a退款退货成功: §6" + + r.totalPaid + "枚"); + } + + + // ===== 2分钟超时 ===== + private void startReturnTimeout(long orderId) { + new BukkitRunnable() { + @Override + public void run() { + PendingReturn pr = pendingReturns + .remove(orderId); + if (pr == null) return; // 已处理 + Player admin = Bukkit.getPlayer(pr.playerUUID); + if (admin != null && admin.isOnline()) { + admin.sendMessage("§c[退货审批] 订单 #" + + orderId + " 超时,自动拒绝"); + } + // 通知玩家 + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款申请已超时拒绝"); + // 恢复订单状态 + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 0; + r.refundType = null; + saveOrders(); + } + } + }.runTaskLater(plugin, 2400L); // 2分钟 + } + + // ===== 查找订单 ===== + private OrderRecord findOrder(long orderId) { + synchronized (orders) { + for (OrderRecord r : orders) { + if (r.orderId == orderId) return r; + } + } + return null; + } + + + public boolean handleReceiptConfigClick(Player p, int raw) { + if (raw == 49) { openOrderCenter(p); return true; } + if (raw == 48) { saveReceiptConfig(); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 50) { createDefaultReceiptConfig(new File(plugin.getDataFolder(), "小票设置.txt")); loadReceiptConfig(); p.sendMessage("§e已重置"); openReceiptConfig(p); return true; } + if (raw == 22) { sendRandomPreview(p); return true; } + String sec = null; + if (raw == 10) sec = "header"; else if (raw == 11) sec = "itemLine"; else if (raw == 12) sec = "discountLine"; + else if (raw == 14) sec = "packLine"; else if (raw == 15) sec = "summary"; else if (raw == 16) sec = "footer"; + if (sec != null) { receiptBuilderSection.put(p.getUniqueId(), sec); openReceiptBuilder(p, sec); } + return true; + } + public boolean handleReceiptBuilderClick(Player p, int raw) { + String sec = receiptBuilderSection.get(p.getUniqueId()); if (sec == null) return false; + String token = getTokenBySlot(raw); + if (token != null) { String cur = getSection(sec); setSection(sec, cur.isEmpty() ? token : cur + token); openReceiptBuilder(p, sec); return true; } + if (raw == 47) { String c = getSection(sec); int lb = c.lastIndexOf('}'); if (lb >= 0) { int ls = c.lastIndexOf('{', lb); if (ls >= 0) setSection(sec, c.substring(0, ls)); } else setSection(sec, c.substring(0, Math.max(0, c.length() - 8))); openReceiptBuilder(p, sec); return true; } + if (raw == 48) { setSection(sec, ""); openReceiptBuilder(p, sec); return true; } + if (raw == 49) { receiptBuilderSection.remove(p.getUniqueId()); openReceiptConfig(p); return true; } + if (raw == 50) { saveReceiptConfig(); receiptBuilderSection.remove(p.getUniqueId()); p.sendMessage("§a已保存"); openReceiptConfig(p); return true; } + if (raw == 51) { setSection(sec, getDefaultVal(sec)); openReceiptBuilder(p, sec); return true; } + return true; + } + + // ===== 聊天事件 ===== + @EventHandler + public void onAdminChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); UUID uuid = p.getUniqueId(); + // ★ 在 onAdminChat 方法开头添加 +// ===== 退货审批输入 ===== + for (Map.Entry entry : + new HashMap<>(pendingReturns).entrySet()) { + Long pendingOrderId = entry.getKey(); + PendingReturn pr = entry.getValue(); + if (pr.playerUUID.equals(p.getUniqueId()) + || isAdmin(p)) { + try { + int choice = Integer.parseInt( + e.getMessage().trim()); + pendingReturns.remove(pendingOrderId); + OrderRecord r = findOrder(pendingOrderId); + if (r == null) { + p.sendMessage("§c订单不存在"); + return; + } + + if (choice == 1) { + // ★ 收走盒子并退款 + Player target = Bukkit.getPlayer( + pr.playerUUID); + if (target != null && target.isOnline()) { + for (int i = 0; i < 36; i++) { + ItemStack is = target.getInventory() + .getItem(i); + if (is != null && is.getType().name() + .contains("SHULKER")) { + ItemMeta im = is.getItemMeta(); + if (im != null && im.getLore() != null) { + for (String line : im.getLore()) { + if (line.equals("§8#" + pendingOrderId)) { + target.getInventory().clear(i); + break; + } + } + } + break; + } + } + } + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§a已退货退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(盒子已收走)"); + notifyPlayer(pr.playerUUID, + "§a[订单] 退货退款成功,§6" + + pr.totalPaid + "枚已退还"); + + } else if (choice == 2) { + // ★ 仅退款(不收盒子) + plugin.getBonds().addBonds(pr.playerName, pr.totalPaid); + r.status = 2; + r.refundedAmount = pr.totalPaid; + saveOrders(); + p.sendMessage("§e已仅退款 §e" + pr.playerName + + " §6" + pr.totalPaid + "枚(未收盒子)"); + notifyPlayer(pr.playerUUID, + "§e[订单] 仅退款成功,§6" + + pr.totalPaid + "枚已退还" + + ",盒子无需退回"); + + } else if (choice == 3) { + // ★ 拒绝 + r.status = 0; + r.refundType = null; + saveOrders(); + p.sendMessage("§c已拒绝退款 #" + pendingOrderId); + notifyPlayer(pr.playerUUID, + "§c[订单] 退货退款已被拒绝" + + ",您可以申请仅退款" + + "或把货拿回来后重新申请"); + } else { + p.sendMessage("§c请输入 1、2 或 3"); + pendingReturns.put(pendingOrderId, pr); + } + return; + + } catch (NumberFormatException ignored) { + // 不是数字,跳过 + } + } + } +// ★ 在 onAdminChat 方法开头,处理用户退货确认 + if (returnConfirmWaiting.containsKey(uuid)) { + String msg = e.getMessage().trim(); + if (msg.equals("1")) { + // ★ 确认提交 + Long orderId = returnConfirmWaiting.remove(uuid); + if (orderId != null) { + OrderRecord r = findOrder(orderId); + if (r != null) { + r.status = 1; + r.refundType = "refund_return_apply"; + saveOrders(); + p.sendMessage("§e已提交退货退款申请," + + "等待管理员审批"); + notifyAdmins(p, r); + } + } + e.setCancelled(true); + return; + } else if (msg.equals("2")) { + // ★ 取消 + returnConfirmWaiting.remove(uuid); + p.sendMessage("§7已取消退货退款申请"); + e.setCancelled(true); + return; + } else { + // 输入其他内容:取消等待 + returnConfirmWaiting.remove(uuid); + // 不 return,让消息正常发送 + } + } + + if (partialRefundListening.contains(uuid)) { + e.setCancelled(true); partialRefundListening.remove(uuid); + OrderRecord t = partialRefundTarget.remove(uuid); if (t == null) return; + try { int pct = Integer.parseInt(e.getMessage().trim()); if (pct < 1 || pct > 100) { p.sendMessage("§c1-100"); return; } + int ref = t.totalPaid * pct / 100; if (ref <= 0) ref = 1; + plugin.getBonds().addBonds(t.player, ref); t.status = 3; t.refundedAmount = ref; saveOrders(); + p.sendMessage("§a部分退款 §e" + t.player + " §6" + ref + "枚(" + pct + "%)"); + notifyPlayer(t.uuid, "§a退款 §6" + ref + "枚"); + final UUID tu = adminViewTarget.get(uuid); final int pg = adminViewPage.getOrDefault(uuid, 0); + Bukkit.getScheduler().runTask(plugin, () -> openAdminOrders(p, tu, pg)); + } catch (NumberFormatException ex) { p.sendMessage("§c请输入数字"); } catch (Exception ex) { plugin.getLogger().log(Level.WARNING, "[Order]退款异常", ex); } + return; + } + if (receiptBuilderSection.containsKey(uuid)) { + e.setCancelled(true); String sec = receiptBuilderSection.remove(uuid); if (sec == null) return; + String msg = e.getMessage().trim(); + if ("cancel".equalsIgnoreCase(msg)) { Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); return; } + setSection(sec, msg.replace("\\n", "\n")); p.sendMessage("§a已更新 §e" + sec); + Bukkit.getScheduler().runTask(plugin, () -> openReceiptConfig(p)); + } + } + + // ===== 数据查询 ===== + private List getPlayerAllOrders(UUID uuid) { + List r = new ArrayList<>(); + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && System.currentTimeMillis() - o.timestamp < RETENTION_30D) r.add(o); } + Collections.reverse(r); return r; + } + private List getPlayerOrdersWithin(UUID uuid, long ms) { + List r = new ArrayList<>(); long cut = System.currentTimeMillis() - ms; + synchronized (orders) { for (OrderRecord o : orders) if (o.uuid.equals(uuid) && o.timestamp >= cut) r.add(o); } + Collections.reverse(r); return r; + } + + // ===== 持久化 ===== + private void loadOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); if (!f.exists()) return; + try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)); + OrderRecord cur = null; String line; + while ((line = br.readLine()) != null) { line = line.trim(); + if (line.equals("==")) { cur = new OrderRecord(); cur.items = new ArrayList<>(); } + else if (cur != null && line.contains("=")) { int eq = line.indexOf('='); String k = line.substring(0, eq); String v = line.substring(eq + 1); + switch (k) { case "id": cur.orderId = Long.parseLong(v); cur.timestamp = cur.orderId; break; case "uuid": cur.uuid = UUID.fromString(v); break; + case "player": cur.player = v; break; case "totalOriginal": cur.totalOriginal = Integer.parseInt(v); break; case "totalPaid": cur.totalPaid = Integer.parseInt(v); break; + case "discount": cur.discount = Integer.parseInt(v); break; case "discountType": cur.discountType = v; break; case "couponCode": cur.couponCode = v; break; + case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break; + case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break; + case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break; + case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; } + } else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; } + } if (cur != null && cur.uuid != null) orders.add(cur); br.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); } + } + private void saveOrders() { + File f = new File(plugin.getDataFolder(), "orders.dat"); + try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); + synchronized (orders) { for (OrderRecord r : orders) { pw.println("=="); + pw.println("id=" + r.orderId); pw.println("uuid=" + r.uuid); pw.println("player=" + r.player); + pw.println("totalOriginal=" + r.totalOriginal); pw.println("totalPaid=" + r.totalPaid); + pw.println("discount=" + r.discount); pw.println("discountType=" + nn(r.discountType)); + pw.println("couponCode=" + nn(r.couponCode)); pw.println("packFee=" + r.packFee); + pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor)); + pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount); + pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType)); + for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty); + } } pw.flush(); pw.close(); + } catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); } + } + private void startCleanupTask() { new BukkitRunnable() { public void run() { synchronized (orders) { if (orders.removeIf(OrderManager.this::isExpired)) saveOrders(); } } }.runTaskTimer(plugin, 36000L, 36000L); } + /** + * 将小票书塞入潜影盒 + */ + public void addBookToShulker(ItemStack shulker, ItemStack book) { + try { + org.bukkit.inventory.meta.BlockStateMeta bm = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (bm == null) return; + org.bukkit.block.BlockState st = bm.getBlockState(); + if (st instanceof org.bukkit.block.Container) { + ((org.bukkit.block.Container) st) + .getInventory().addItem(book); + bm.setBlockState(st); + shulker.setItemMeta(bm); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Order] 塞书失败: " + e.getMessage()); + } + } + + // ===== 工具 ===== + private String nn(String s) { return s != null ? s : ""; } + private boolean isExpired(OrderRecord r) { return System.currentTimeMillis() - r.timestamp > RETENTION_30D; } + private String fmtDate(long ts) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts)); } + private int slotIndex(int raw) { for (int i = 0; i < SLOTS.length; i++) if (SLOTS[i] == raw) return i; return -1; } + private String brief(String s) { if (s == null || s.isEmpty()) return "(空)"; String c = sc(s); return c.length() > 30 ? c.substring(0, 30) + "..." : c; } + private String sectionName(String s) { switch(s){case "header":return "头部";case "itemLine":return "商品行";case "discountLine":return "优惠行";case "packLine":return "打包行";case "summary":return "汇总";case "footer":return "页脚";default:return s;} } + private String sc(String s) { + if (s == null) return ""; + return s.replaceAll("§[0-9a-fk-orA-FK-OR]", ""); + } + private String getSection(String s) { switch(s){case "header":return tplHeader;case "itemLine":return tplItemLine;case "discountLine":return tplDiscountLine;case "packLine":return tplPackLine;case "summary":return tplSummary;case "footer":return tplFooter;default:return "";} } + private void setSection(String s, String v) { switch(s){case "header":tplHeader=v;break;case "itemLine":tplItemLine=v;break;case "discountLine":tplDiscountLine=v;break;case "packLine":tplPackLine=v;break;case "summary":tplSummary=v;break;case "footer":tplFooter=v;break;} } + private String getDefaultVal(String s) { switch(s){case "header":return "§6§l{商店名}§r\n购物小票\n订单#{订单号} {订单时间}\n玩家: {玩家}";case "itemLine":return "§e{品名}§r x{数量} {原价}→{执行价} 小计{小计}枚";case "discountLine":return "优惠: §a-{优惠额}枚§r ({优惠类型}{优惠券码})";case "packLine":return "打包: §c+{打包费}枚§r ({打包颜色})";case "summary":return "原价{原价合计}枚 → 实付§6{实付}枚§r 余额{余额}枚";case "footer":return "感谢惠顾,欢迎再来!";default:return "";} } + private String getTokenBySlot(int raw) { switch(raw){case 10:return "{品名}";case 11:return "{原价}";case 12:return "{执行价}";case 13:return "{数量}";case 14:return "{小计}";case 15:return "{商店名}";case 16:return "{玩家}";case 19:return "{订单号}";case 20:return "{订单时间}";case 21:return "{打印时间}";case 22:return "{日期}";case 23:return "{时间}";case 24:return "{原价合计}";case 25:return "{实付}";case 28:return "{优惠额}";case 29:return "{优惠类型}";case 30:return "{优惠券码}";case 31:return "{打包费}";case 32:return "{打包颜色}";case 33:return "{余额}";case 37:return "═══";case 38:return "───";case 39:return "═════════════";case 40:return "\\n";default:return null;} } + private ItemStack mkItem(Material mat, String name, String... lore) { ItemStack it = new ItemStack(mat); ItemMeta im = it.getItemMeta(); if (im != null) { im.setDisplayName(name); if (lore.length > 0) im.setLore(Arrays.asList(lore)); it.setItemMeta(im); } return it; } + private Map loadMap(File f) { Map m = new LinkedHashMap<>(); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), 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) m.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); } br.close(); } catch (Exception ignored) {} return m; } + private void writeLines(File f, List lines) { try { PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)); for (String l : lines) pw.println(l); pw.flush(); pw.close(); } catch (Exception ignored) {} } + private void sendRandomPreview(Player p) { String[] n = {"钻石剑","钻石镐","铁剑","金苹果"}; String[] m = {"DIAMOND_SWORD","DIAMOND_PICKAXE","IRON_SWORD","GOLDEN_APPLE"}; Random r = new Random(); List items = new ArrayList<>(); for (int i = 0; i < 2 + r.nextInt(3); i++) { int idx = r.nextInt(n.length); items.add(new OrderItem(n[idx], m[idx], 100 + r.nextInt(5000), 100 + r.nextInt(5000), 1 + r.nextInt(10))); } OrderRecord o = new OrderRecord(); o.orderId = System.currentTimeMillis(); o.uuid = p.getUniqueId(); o.player = p.getName(); o.items = items; o.totalOriginal = 0; for (OrderItem it : items) o.totalOriginal += it.originalSubtotal(); o.totalPaid = o.totalOriginal; o.timestamp = o.orderId; o.status = 0; p.sendMessage("§6§l===== 小票预览(随机) ====="); sendReceiptChat(p, o); p.sendMessage("§6§l===== 预览结束 ====="); } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/PVPManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PVPManager.java new file mode 100644 index 0000000..326c0fb --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PVPManager.java @@ -0,0 +1,1380 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scoreboard.*; + + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PVPManager implements Listener { + + private final Main plugin; + private Connection pvpDb; + + private final Map + regions = new ConcurrentHashMap<>(); + private final Map + inRegion = new ConcurrentHashMap<>(); + private final Map + sessionKills = + new ConcurrentHashMap<>(); + private final Map + lastKillTime = + new ConcurrentHashMap<>(); + private final Map + selectState = + new ConcurrentHashMap<>(); + private final Map + killSessions = + new ConcurrentHashMap<>(); + private final Set tripleAnnounced = + ConcurrentHashMap.newKeySet(); + private final Map lastScoreboardRefresh = new ConcurrentHashMap<>(); + + + // 边框显示 + private final Set + permanentBorders = + ConcurrentHashMap.newKeySet(); + private final Set + tempBorders = + ConcurrentHashMap.newKeySet(); + + + public static class KillSession { + public int count = 0; + public long firstKill = 0; + public long lastKill = 0; + public boolean tripleAnnounced = + false; + public String region = ""; + } + + public static class PVPRegion { + public String name; + public String world; + public int x1, y1, z1; + public int x2, y2, z2; + public int minX, minY, minZ; + public int maxX, maxY, maxZ; + + public void calcBounds() { + minX = Math.min(x1, x2); + minY = Math.min(y1, y2); + minZ = Math.min(z1, z2); + maxX = Math.max(x1, x2); + maxY = Math.max(y1, y2); + maxZ = Math.max(z1, z2); + } + + public boolean contains( + Location loc) { + if (loc == null) return false; + if (!loc.getWorld().getName() + .equals(world)) + return false; + int x = loc.getBlockX(); + int y = loc.getBlockY(); + int z = loc.getBlockZ(); + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + } + + public PVPManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadRegions(); + } + + // ===== 独立数据库 ===== + + private void initDB() { + try { + Class.forName( + "org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), + "pvp.db"); + pvpDb = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = + pvpDb.createStatement(); + st.execute( + "PRAGMA journal_mode=WAL"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_regions (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "name TEXT NOT NULL " + + "UNIQUE," + + "world TEXT NOT NULL," + + "x1 INTEGER NOT NULL," + + "y1 INTEGER NOT NULL," + + "z1 INTEGER NOT NULL," + + "x2 INTEGER NOT NULL," + + "y2 INTEGER NOT NULL," + + "z2 INTEGER NOT NULL," + + "create_time INTEGER " + + "DEFAULT 0)"); + st.execute( + "CREATE TABLE IF NOT EXISTS " + + "pvp_kills (" + + "player_name TEXT " + + "NOT NULL," + + "region_name TEXT " + + "NOT NULL," + + "kills INTEGER " + + "DEFAULT 0," + + "deaths INTEGER " + + "DEFAULT 0," + + "PRIMARY KEY" + + "(player_name, " + + "region_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException( + "[PVP] DB init failed: " + + e.getMessage(), e); + } + } + + // ===== DB 操作 ===== + + private void saveRegion(String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT OR REPLACE " + + "INTO pvp_regions" + + " (name, world," + + " x1,y1,z1," + + " x2,y2,z2," + + " create_time)" + + " VALUES " + + "(?,?,?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, world); + ps.setInt(3, x1); + ps.setInt(4, y1); + ps.setInt(5, z1); + ps.setInt(6, x2); + ps.setInt(7, y2); + ps.setInt(8, z2); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private Map + getRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT * FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + private List> + getAllRegions() { + List> list = + new ArrayList<>(); + try { + Statement st = + pvpDb.createStatement(); + ResultSet rs = + st.executeQuery( + "SELECT * FROM " + + "pvp_regions"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + private void deleteRegion(String name) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "DELETE FROM " + + "pvp_regions " + + "WHERE name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private int getKills(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT kills FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("kills"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private int getDeaths(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT deaths FROM " + + "pvp_kills " + + "WHERE " + + "player_name=?" + + " AND " + + "region_name=?"); + ps.setString(1, player); + ps.setString(2, region); + ResultSet rs = + ps.executeQuery(); + int v = 0; + if (rs.next()) + v = rs.getInt("deaths"); + rs.close(); + ps.close(); + return v; + } catch (SQLException e) { + return 0; + } + } + + private void addKill(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,1,0) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET kills=" + + "kills+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private void addDeath(String player, + String region) { + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "INSERT INTO pvp_kills" + + " (player_name," + + " region_name," + + " kills,deaths)" + + " VALUES " + + "(?,?,0,1) " + + "ON CONFLICT" + + "(player_name," + + " region_name)" + + " DO UPDATE " + + "SET deaths=" + + "deaths+1"); + ps.setString(1, player); + ps.setString(2, region); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private List> + getTop(String region, int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + pvpDb.prepareStatement( + "SELECT player_name," + + " kills,deaths" + + " FROM " + + "pvp_kills " + + "WHERE " + + "region_name=?" + + " AND kills>0" + + " ORDER BY kills" + + " DESC LIMIT ?"); + ps.setString(1, region); + ps.setInt(2, limit); + ResultSet rs = + ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString( + "player_name")); + row.put("kills", + rs.getInt("kills")); + row.put("deaths", + rs.getInt("deaths")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Set getRegionNames() { + return regions.keySet(); + } + + // ===== 加载 ===== + + private void loadRegions() { + regions.clear(); + List> list = + getAllRegions(); + for (Map row : list) { + PVPRegion r = new PVPRegion(); + r.name = (String) row + .get("name"); + r.world = (String) row + .get("world"); + r.x1 = ((Number) row + .get("x1")).intValue(); + r.y1 = ((Number) row + .get("y1")).intValue(); + r.z1 = ((Number) row + .get("z1")).intValue(); + r.x2 = ((Number) row + .get("x2")).intValue(); + r.y2 = ((Number) row + .get("y2")).intValue(); + r.z2 = ((Number) row + .get("z2")).intValue(); + r.calcBounds(); + regions.put(r.name, r); + } + } + + // ===== 工具 ===== + + public ItemStack createTool() { + ItemStack tool = new ItemStack( + Material.BLAZE_ROD); + ItemMeta m = tool.getItemMeta(); + m.setDisplayName( + "§6§l[PVP] 区域选择工具"); + m.setLore(Arrays.asList( + "§7右键:设置第一个角", + "§7左键:设置第二个角")); + m.getPersistentDataContainer() + .set(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING, + "tool"); + tool.setItemMeta(m); + return tool; + } + + private boolean isPVPTool( + ItemStack item) { + if (item == null) return false; + ItemMeta m = item.getItemMeta(); + if (m == null) return false; + return m.getPersistentDataContainer() + .has(new NamespacedKey( + plugin, "pvp_tool"), + PersistentDataType.STRING); + } + + public void giveTool(Player p) { + boolean has = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (isPVPTool(it)) { + has = true; + break; + } + } + if (!has) { + p.getInventory() + .addItem(createTool()); + } + } + + // ===== 边框粒子 ===== + + private void drawBorder(Player target, + PVPRegion r) { + if (r == null) return; + World w = Bukkit.getWorld(r.world); + if (w == null) return; + int step = 2; + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.maxX, r.minY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.minY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.minX, r.minY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.minY, r.minZ, step); + drawLine(target, w, + r.minX, r.maxY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.minZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.maxX, r.maxY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.maxY, r.maxZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.minX, r.minY, r.minZ, + r.minX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.minZ, + r.maxX, r.maxY, r.minZ, step); + drawLine(target, w, + r.maxX, r.minY, r.maxZ, + r.maxX, r.maxY, r.maxZ, step); + drawLine(target, w, + r.minX, r.minY, r.maxZ, + r.minX, r.maxY, r.maxZ, step); + } + + private void drawLine(Player target, + World w, + int x1, int y1, int z1, + int x2, int y2, int z2, + int step) { + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double dist = Math.sqrt( + dx * dx + dy * dy + dz * dz); + int steps = (int) (dist / step); + if (steps < 1) steps = 1; + for (int i = 0; i <= steps; i++) { + double t = (double) i / steps; + Location loc = new Location(w, + x1 + dx * t, + y1 + dy * t + 0.1, + z1 + dz * t); + w.spawnParticle( + Particle.FLAME, loc, 1, + 0.0, 0.0, 0.0, 0.0); + } + } + + // ===== 每秒 tick ===== + + public void tickRegions() { + long now = + System.currentTimeMillis(); + + // 玩家区域检测 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pn = p.getName(); + Location loc = p.getLocation(); + String inside = null; + for (PVPRegion r : + regions.values()) { + if (r.contains(loc)) { + inside = r.name; + break; + } + } + String was = inRegion.get(pn); + + if (inside != null && !inside.equals(was)) { + inRegion.put(pn, inside); + if (was == null) { + String reg = inside; + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> + updateScoreboard( + pp, reg), + 1L); + } + + } else if (inside == null + && was != null) { + inRegion.remove(pn); + // 不删 sessionKills,等倒计时结束再清 + removeScoreboard(p); + final Player pp = p; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> plugin + .giveMenuSnowball(pp), + 5L); + } + + } + + // 5分钟超时重置连杀session + for (String pname : + new ArrayList<>( + killSessions + .keySet())) { + KillSession ks = + killSessions.get(pname); + if (ks == null) continue; + if (now - ks.lastKill + > 5 * 60 * 1000) { + killSessions.remove(pname); + } + } + + // 5分钟无击杀 → 计数榜切总榜 + // 5分钟无击杀 → 归档并清零 + for (String rName : + new ArrayList<>( + lastKillTime + .keySet())) { + long last = lastKillTime + .getOrDefault(rName, 0L); + if (now - last > 5 * 60 * 1000) { + // 清零该区域所有sessionKills + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + rName + ":")); + // 切换到总榜 + refreshScoreboard(rName); + lastKillTime.remove(rName); + lastScoreboardRefresh.remove(rName); + + } + } + + + // 边框渲染 + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (pr == null) continue; + PVPRegion r = regions.get(pr); + if (r == null) continue; + if (permanentBorders + .contains(pr)) { + drawBorder(p, r); + } + if (tempBorders + .contains(pr)) { + drawBorder(p, r); + } + // 每秒刷新活跃区域的计分板倒计时 + for (String rName : new ArrayList<>(lastKillTime.keySet())) { + long last = lastScoreboardRefresh.getOrDefault(rName, 0L); + if (now - last >= 1000) { + lastScoreboardRefresh.put(rName, now); + for (Player tp : Bukkit.getOnlinePlayers()) { + String inR = inRegion.get(tp.getName()); + if (rName.equals(inR)) { + showCounter(tp, rName); + } + } + } + } + + + + } + } + + private void broadcastWithSound(String msg, Sound sound) { + Bukkit.broadcastMessage(msg); + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), sound, 1.0f, 1.0f); + } + } + + + private void initSession(String player, + String region) { + killSessions.remove(player); + tripleAnnounced.remove( + region + ":" + player); + KillSession s = new KillSession(); + s.region = region; + s.firstKill = + System.currentTimeMillis(); + s.lastKill = + System.currentTimeMillis(); + killSessions.put(player, s); + } + + // ===== 击杀事件 ===== + + @EventHandler + public void onDeath( + EntityDeathEvent event) { + if (!(event.getEntity() + instanceof Player)) return; + Player victim = + (Player) event.getEntity(); + Player killer = + victim.getKiller(); + if (killer == null) return; + if (killer.equals(victim)) return; + + String region = + inRegion.get( + victim.getName()); + if (region == null) return; + + String vName = victim.getName(); + String kName = killer.getName(); + + // 总榜 + addKill(kName, region); + addDeath(vName, region); + + // 计数榜 + String kKey = + region + ":" + kName; + sessionKills.merge(kKey, 1, + Integer::sum); + lastKillTime.put(region, + System.currentTimeMillis()); + + // 终结检测 + // 终结检测 + KillSession victimSession = + killSessions.get(vName); + if (victimSession != null + && victimSession.count >= 3) { + String endMsg = + "§b§l" + kName + + " §e终结了 " + + vName + " §e的 " + + victimSession.count + + "连杀!§a恭喜终结者 " + + kName + + "§e!§7快来挑战!"; + killSessions.remove(vName); + broadcastWithSound(endMsg, Sound.ENTITY_ENDER_DRAGON_GROWL); + + killSessions.remove(vName); + } + + + // 击杀者连杀 + KillSession ks = + killSessions.get(kName); + if (ks == null + || !ks.region.equals(region) + || System.currentTimeMillis() + - ks.lastKill > 5 * 60 * 1000) { + tripleAnnounced.remove( + region + ":" + kName); + KillSession ns = + new KillSession(); + ns.region = region; + ns.count = 1; + ns.firstKill = + System.currentTimeMillis(); + ns.lastKill = + System.currentTimeMillis(); + killSessions.put(kName, ns); + } else { + ks.count++; + ks.lastKill = + System.currentTimeMillis(); + } + + // 多杀通报 + KillSession killerSession = + killSessions.get(kName); + if (killerSession != null) { + plugin.getLogger().info( + "[PVP] checkMultiKill: " + + kName + " count=" + + killerSession.count + + " region=" + region); + checkMultiKill(killer, + region, killerSession); + } + + // 刷新计分板 + for (Player p : + Bukkit.getOnlinePlayers()) { + if (region.equals( + inRegion.get( + p.getName()))) { + updateScoreboard(p, region); + } + } + + victim.sendMessage("§c你在「" + + region + "」被 " + + kName + " 击杀"); + killer.sendMessage("§a你在「" + + region + "」击杀了 " + + vName); + } + + private void checkMultiKill( + Player killer, String region, + KillSession session) { + int count = session.count; + String kName = killer.getName(); + String key = region + ":" + kName; + String msg = null; + + if (count >= 5) { + msg = "§c§l" + kName + + " §e在「" + region + + "」达成 " + count + + "连杀!§7§l快来挑战!"; + } else if (count == 3) { + if (tripleAnnounced.add(key)) { + msg = "§6§l三杀!" + + kName + + " 势不可挡!" + + "§7快来挑战!"; + } + } + + if (msg != null) { + if (count >= 5) { + broadcastWithSound(msg, Sound.ENTITY_ENDER_DRAGON_GROWL); + } else { + broadcastWithSound(msg, Sound.BLOCK_END_PORTAL_SPAWN); + } + } + + } + + + + // ===== 计分板(英文ID) ===== + + private void ensureScoreboard( + Player p, String region) { + // 活跃击杀:每秒强制刷新(更新倒计时) + if (lastKillTime.containsKey(region)) { + showCounter(p, region); + return; + } + // 无活跃击杀:只在没有scoreboard时创建总榜 + Scoreboard sb = p.getScoreboard(); + if (sb == null + || sb.getObjective( + "sdf1pvp") == null) { + showTotal(p, region); + } + } + + + private void updateScoreboard( + Player p, String region) { + if (lastKillTime + .containsKey(region)) { + showCounter(p, region); + } else { + showTotal(p, region); + } + } + + private void showTotal(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = + mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§6§l击杀总榜 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List> top = + getTop(region, 10); + if (top.isEmpty()) { + obj.getScore("§7暂无记录") + .setScore(0); + } else { + int rank = 10; + for (Map row : + top) { + String name = (String) row + .get("player_name"); + int kills = ((Number) row + .get("kills")) + .intValue(); + int deaths = ((Number) row + .get("deaths")) + .intValue(); + String line = + "§e#" + (11 - rank) + + " §f" + name + + " §a" + kills + + "§7/§c" + deaths; + obj.getScore(line) + .setScore(rank); + rank--; + } + } + p.setScoreboard(sb); + } + private void showCounter(Player p, + String region) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + Scoreboard sb = mgr.getNewScoreboard(); + Objective obj = + sb.registerNewObjective( + "sdf1pvp", + Criteria.DUMMY, + "§c§l实时战况 [" + + region + "]"); + obj.setDisplaySlot( + DisplaySlot.SIDEBAR); + + List entries = + new ArrayList<>(); + for (Map.Entry e : + sessionKills.entrySet()) { + if (!e.getKey().startsWith( + region + ":")) + continue; + if (e.getValue() <= 0) continue; + String pName = e.getKey() + .substring( + region.length() + 1); + entries.add(new String[]{ + pName, + String.valueOf( + e.getValue())}); + } + + if (entries.isEmpty()) { + obj.getScore("§7等待击杀...") + .setScore(1); + } else { + entries.sort((a, b) -> + Integer.compare( + Integer.parseInt(b[1]), + Integer.parseInt(a[1]))); + int rank = 10; + for (String[] entry : entries) { + obj.getScore( + "§e" + entry[0] + + " §a" + entry[1]) + .setScore(rank); + rank--; + if (rank < 1) break; + } + } + + Long last = + lastKillTime.get(region); + if (last != null) { + long elapsed = + System.currentTimeMillis() + - last; + long remaining = + Math.max(0, + 5 * 60 * 1000 - elapsed); + long secs = remaining / 1000; + obj.getScore( + "§7冷却: " + secs + "s") + .setScore(-1); + } + + // 关键:强制赋值新scoreboard + p.setScoreboard(sb); + } + + private void removeScoreboard( + Player p) { + ScoreboardManager mgr = + Bukkit.getScoreboardManager(); + p.setScoreboard( + mgr.getNewScoreboard()); + } + + private void refreshScoreboard( + String regionName) { + for (Player p : + Bukkit.getOnlinePlayers()) { + String pr = + inRegion.get(p.getName()); + if (regionName.equals(pr)) { + showTotal(p, regionName); + } + } + } + + // ===== 工具选择事件 ===== + + @EventHandler + public void onInteract( + PlayerInteractEvent event) { + Player p = event.getPlayer(); + ItemStack hand = + p.getInventory() + .getItemInMainHand(); + if (!isPVPTool(hand)) return; + + if (!event.getAction() + .isRightClick() + && !event.getAction() + .isLeftClick()) return; + + Location loc = p.getLocation(); + String[] state = + selectState.get(p.getName()); + if (state == null) { + state = new String[]{ + loc.getWorld() + .getName(), + "", ""}; + selectState.put( + p.getName(), state); + } + + String coord = + loc.getBlockX() + "," + + loc.getBlockY() + "," + + loc.getBlockZ(); + + if (event.getAction() + .isRightClick()) { + state[0] = loc.getWorld() + .getName(); + state[1] = coord; + p.sendMessage( + "§a第一个角: " + coord); + } else { + state[2] = coord; + p.sendMessage( + "§a第二个角: " + coord); + } + event.setCancelled(true); + } + + // ===== 退出清理 ===== + + @EventHandler + public void onQuit( + PlayerQuitEvent event) { + String name = + event.getPlayer().getName(); + inRegion.remove(name); + selectState.remove(name); + } + + // ===== 指令 ===== + + public boolean onCommand(Player p, + String[] args) { + if (args.length == 0) { + return showStats(p, + p.getName()); + } + switch (args[0].toLowerCase()) { + case "create": + return handleCreate( + p, args); + case "stats": + if (args.length >= 2) + return showStats(p, + args[1]); + return showStats(p, + p.getName()); + case "list": + return handleList(p); + case "delete": + if (args.length >= 2) + return handleDelete(p, + args[1]); + p.sendMessage( + "§c用法: " + + "/pvp delete <名字>"); + return true; + case "tool": + giveTool(p); + p.sendMessage( + "§a已发放" + + "PVP区域选择工具"); + return true; + case "on": + return handleBorderOn( + p, args); + case "off": + return handleBorderOff( + p, args); + case "tempban": + return handleTempBan( + p, args); + default: + return showStats(p, + p.getName()); + } + } + + private boolean handleBorderOn( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp on <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + permanentBorders.add(name); + p.sendMessage("§a已开启「" + + name + "」永久边框显示"); + return true; + } + + private boolean handleBorderOff( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp off <区域名>"); + return true; + } + permanentBorders.remove(name); + tempBorders.remove(name); + p.sendMessage("§a已关闭「" + + name + "」边框显示"); + return true; + } + + private boolean handleTempBan( + Player p, String[] args) { + String name = args.length >= 2 + ? args[1] : null; + if (name == null) { + p.sendMessage("§c用法: " + + "/pvp tempban <区域名>"); + return true; + } + if (!regions + .containsKey(name)) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + tempBorders.add(name); + p.sendMessage("§a「" + name + + "」临时边框显示15秒"); + final String rName = name; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> + tempBorders + .remove(rName), + 300L); + return true; + } + + private boolean handleCreate( + Player p, String[] args) { + if (args.length >= 8) { + try { + int x1 = Integer.parseInt( + args[1]); + int y1 = Integer.parseInt( + args[2]); + int z1 = Integer.parseInt( + args[3]); + int x2 = Integer.parseInt( + args[4]); + int y2 = Integer.parseInt( + args[5]); + int z2 = Integer.parseInt( + args[6]); + String rName = args[7]; + return createRegion(p, + rName, + p.getWorld() + .getName(), + x1, y1, z1, + x2, y2, z2); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c坐标格式错误"); + return true; + } + } + + if (args.length >= 2) { + String[] state = + selectState + .get(p.getName()); + if (state == null + || state[1].isEmpty() + || state[2].isEmpty()) { + p.sendMessage( + "§c请先用工具选择两个角"); + return true; + } + String[] p1 = + state[1].split(","); + String[] p2 = + state[2].split(","); + return createRegion(p, + args[1], state[0], + Integer.parseInt(p1[0]), + Integer.parseInt(p1[1]), + Integer.parseInt(p1[2]), + Integer.parseInt(p2[0]), + Integer.parseInt(p2[1]), + Integer.parseInt(p2[2])); + } + + p.sendMessage( + "§c用法: /pvp create " + + " " + + " <名字>"); + p.sendMessage( + "§c或: /pvp create " + + "<名字> (先用工具选择)"); + return true; + } + + private boolean createRegion( + Player p, String name, + String world, + int x1, int y1, int z1, + int x2, int y2, int z2) { + if (getRegion(name) != null) { + p.sendMessage( + "§c区域已存在: " + name); + return true; + } + PVPRegion r = new PVPRegion(); + r.name = name; + r.world = world; + r.x1 = x1; + r.y1 = y1; + r.z1 = z1; + r.x2 = x2; + r.y2 = y2; + r.z2 = z2; + r.calcBounds(); + saveRegion(name, world, + x1, y1, z1, + x2, y2, z2); + regions.put(name, r); + p.sendMessage( + "§a§lPVP区域创建成功!"); + p.sendMessage( + "§7名称: §f" + name); + p.sendMessage("§7范围: §f" + + r.minX + "," + r.minY + + "," + r.minZ + " → " + + r.maxX + "," + r.maxY + + "," + r.maxZ); + selectState.remove(p.getName()); + return true; + } + + private boolean handleList( + Player p) { + List> all = + getAllRegions(); + if (all.isEmpty()) { + p.sendMessage( + "§7暂无PVP区域"); + return true; + } + p.sendMessage( + "§6§lPVP区域列表:"); + for (Map row : + all) { + p.sendMessage("§e- §f" + + row.get("name") + + " §7(" + row.get("world") + + ")"); + } + return true; + } + + private boolean handleDelete( + Player p, String name) { + if (getRegion(name) == null) { + p.sendMessage( + "§c区域不存在: " + name); + return true; + } + deleteRegion(name); + regions.remove(name); + permanentBorders.remove(name); + tempBorders.remove(name); + sessionKills.keySet() + .removeIf(k -> k + .startsWith( + name + ":")); + lastKillTime.remove(name); + lastScoreboardRefresh.remove(name); + p.sendMessage( + "§a已删除: " + name); + return true; + } + + private boolean showStats(Player p, + String target) { + String region = + inRegion.get(target); + if (region == null) { + p.sendMessage("§6§l" + target + + " 的PVP战绩:"); + List> all = + getAllRegions(); + boolean has = false; + for (Map row : + all) { + String rn = (String) row + .get("name"); + int kills = + getKills(target, rn); + int deaths = + getDeaths(target, rn); + if (kills > 0 + || deaths > 0) { + p.sendMessage( + "§e- §f" + rn + + " §a击杀:" + kills + + " §c死亡:" + deaths); + has = true; + } + } + if (!has) + p.sendMessage( + "§7暂无记录"); + } else { + int kills = + getKills(target, region); + int deaths = + getDeaths(target, region); + double kd = deaths > 0 + ? (double) kills / deaths + : kills; + p.sendMessage("§6§l" + target + + " @ " + region); + p.sendMessage( + "§a击杀: " + kills + + " §c死亡: " + deaths + + " §eKD: " + + String.format( + "%.2f", kd)); + } + return true; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/PasswordUtils.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PasswordUtils.java new file mode 100644 index 0000000..4029ed4 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PasswordUtils.java @@ -0,0 +1,72 @@ +package Sdf1_login; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtils { + + private static final SecureRandom RANDOM = + new SecureRandom(); + + public static String generateSalt() { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hash(String password, String salt) { + try { + MessageDigest md = + MessageDigest.getInstance("SHA-256"); + md.update(Base64.getDecoder().decode(salt)); + byte[] hash = + md.digest(password.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean validate(String password) { + if (password == null) return false; + if (password.length() < 6 || password.length() > 20) + return false; + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + return hasUpper && hasLower && hasDigit; + } + + public static String generateTempPassword() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(8); + for (int i = 0; i < 8; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateInviteCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateVerifyCode() { + int code = 100000 + RANDOM.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/PlayerStorage.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PlayerStorage.java new file mode 100644 index 0000000..f93caa3 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PlayerStorage.java @@ -0,0 +1,130 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.logging.Logger; + +public class PlayerStorage { + private Connection conn; + private final Logger logger; + + public PlayerStorage(File dataFolder, + Logger logger) { + this.logger = logger; + File dbFile = new File( + dataFolder, + "quest_data.db"); + try { + Class.forName( + "org.sqlite.JDBC"); + conn = DriverManager + .getConnection( + "jdbc:sqlite:" + + dbFile + .getAbsolutePath()); + conn.setAutoCommit(true); + Statement s = + conn.createStatement(); + s.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_kv (" + + "player TEXT NOT NULL," + + "key TEXT NOT NULL," + + "value TEXT," + + "PRIMARY KEY" + + "(player, key))"); + s.close(); + logger.info( + "[QuestStorage] 初始化: " + + dbFile.getAbsolutePath()); + } catch (Exception e) { + logger.severe( + "[QuestStorage] 失败: " + + e.getMessage()); + } + } + + public Object getCondition( + String player, String key) { + if (conn == null) return null; + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value " + + "FROM quest_kv " + + "WHERE player=? " + + "AND key=?"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ResultSet rs = + ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") + : null; + rs.close(); + ps.close(); + return parseValue(val); + } catch (SQLException e) { + return null; + } + } + + public void setCondition( + String player, String key) { + setConditionRaw( + player, key, "true"); + } + public void debugPlayer(String player) { + + } + public void setConditionRaw( + String player, String key, + String value) { + if (conn == null) return; + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO quest_kv" + + "(player,key,value)" + + " VALUES(?,?,?)"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ps.setString(3, value); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + logger.warning( + "[QuestStorage] 写入失败: " + + key + "=" + value + + " | " + e.getMessage()); + } + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) + conn.close(); + } catch (Exception e) {} + } + + private Object parseValue(String v) { + if (v == null) return null; + try { + return Long.parseLong(v); + } catch (Exception e1) { + try { + return Double.parseDouble(v); + } catch (Exception e2) { + if ("true".equalsIgnoreCase(v)) + return true; + if ("false".equalsIgnoreCase(v)) + return false; + return v; + } + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/PointsManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PointsManager.java new file mode 100644 index 0000000..77fc195 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/PointsManager.java @@ -0,0 +1,374 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +import java.lang.reflect.Method; +import java.util.*; + +public class PointsManager { + + private final Main plugin; + private Object cyInstance = null; + private Method cyActivationMethod = null; + private Method cyGetShopItemsMethod = null; + + private final List> cyItems = + new ArrayList<>(); + + public PointsManager(Main plugin) { + this.plugin = plugin; + discoverCY(); + } + + private void discoverCY() { + try { + org.bukkit.plugin.Plugin p = Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (p != null && p.isEnabled()) { + cyInstance = p; + cyGetShopItemsMethod = p.getClass() + .getMethod("onSdf1GetShopItems"); + cyActivationMethod = p.getClass() + .getMethod("onSdf1Activation", + String.class, + int.class, int.class); + plugin.getLogger().info( + "[Sdf1_login] CY_beibao 联控已连接"); + } + } catch (Exception ignored) { + } + } + + private void refreshCYItems() { + cyItems.clear(); + if (cyInstance == null + || cyGetShopItemsMethod == null) return; + try { + Object result = + cyGetShopItemsMethod.invoke(cyInstance); + if (result == null) return; + String data = result.toString(); + if (data.isEmpty()) return; + String[] items = data.split(";"); + for (String itemStr : items) { + String[] p = itemStr.split("\\|"); + if (p.length < 8) continue; + Map item = + new LinkedHashMap<>(); + item.put("id", p[0]); + item.put("name", p[1]); + item.put("days", + Integer.parseInt(p[2])); + item.put("slots", + Integer.parseInt(p[3])); + item.put("price", + Integer.parseInt(p[4])); + item.put("stock", + Integer.parseInt(p[5])); + item.put("icon", p[6]); + item.put("lifetime", + "1".equals(p[7])); + cyItems.add(item); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 拉取CY商品失败: " + + e.getMessage()); + } + } + + private void activateCY(String name, + int slots, int days) { + try { + if (cyInstance == null + || cyActivationMethod == null) return; + cyActivationMethod.invoke( + cyInstance, name, slots, days); + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] CY激活失败: " + + e.getMessage()); + } + } + + public int getPoints(String name) { + return ((Number) plugin.getDb() + .getField(name, "points")).intValue(); + } + + public Inventory createShopGUI(Player p) { + int points = getPoints(p.getName()); + Inventory inv = Bukkit.createInventory( + null, 54, + "§d§l积分商城 | 当前积分: " + points); + fillBg(inv); + inv.setItem(10, mkItem(Material.EMERALD, + "§a游戏币充值", + "§7消耗积分兑换游戏币", + "§7100积分 = $1000", + "", + "§e点击兑换")); + inv.setItem(12, mkItem(Material.ENDER_CHEST, + "§b背包空间", + "§7消耗积分兑换背包格子", + "§750积分 = 9格永久空间", + "", + "§e点击兑换")); + inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, + "§e经验瓶", + "§7消耗积分兑换经验", + "§720积分 = 10级经验", + "", + "§e点击兑换")); + if (cyInstance != null) { + inv.setItem(31, mkItem(Material.CHEST, + "§6背包商城", + "§7来自CY_beibao的商品", + "", + "§e点击查看")); + } + inv.setItem(49, mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public Inventory createCYShopGUI(Player p) { + refreshCYItems(); + int points = getPoints(p.getName()); + int size = Math.max(54, + ((cyItems.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory inv = Bukkit.createInventory( + null, size, + "§d§lCY背包商城 | 当前积分: " + points); + fillBg(inv); + for (int i = 0; i < cyItems.size() + && i < 45; i++) { + Map item = cyItems.get(i); + String name = (String) item.get("name"); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + List lore = new ArrayList<>(); + lore.add("§7价格: §e" + price + " 积分"); + if (lifetime) { + lore.add("§7格子: §e终身 +" + slots); + } else { + lore.add("§7格子: §e+" + slots); + lore.add("§7时间: §e" + days + "天"); + } + if (stock == 0) { + lore.add("§7库存: §a无限"); + } else if (stock == -1) { + lore.add("§7库存: §c已下架"); + } else { + lore.add("§7库存: §e" + stock); + } + lore.add(""); + if (points >= price && stock != -1) { + lore.add("§e点击购买"); + } else if (stock == -1) { + lore.add("§c已下架"); + } else { + lore.add("§c积分不足"); + } + inv.setItem(i, mkItem(Material.CHEST, + "§a" + name, + lore.toArray(new String[0]))); + } + inv.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + return inv; + } + + public void addItem(String name, + int price, String cmd2) { + // 追加到商品配置文件 + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.io.FileWriter fw = + new java.io.FileWriter(file, true); + fw.write(name + ":" + price + + ":" + cmd2 + "\n"); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void removeItem(int index) { + File file = new File( + plugin.getDataFolder(), + "ShopItems.txt"); + try { + java.util.List lines = + new java.util.ArrayList<>(); + java.io.BufferedReader br = + new java.io.BufferedReader( + new java.io.FileReader( + file)); + String line; + int i = 0; + while ((line = br.readLine()) != null) { + if (line.trim().isEmpty() + || line.startsWith("#")) { + lines.add(line); + continue; + } + if (i != index) { + lines.add(line); + } + i++; + } + br.close(); + java.io.PrintWriter pw = + new java.io.PrintWriter(file); + for (String l : lines) pw.println(l); + pw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void handleClick(Player p, int slot) { + int points = getPoints(p.getName()); + switch (slot) { + case 10: + if (points >= 100) { + plugin.getDb().deductPoints( + p.getName(), 100); + if (plugin.getEconomy() != null) { + plugin.getEconomy() + .depositPlayer(p, 1000); + } + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "100")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 12: + if (points >= 50) { + plugin.getDb().deductPoints( + p.getName(), 50); + activateCY(p.getName(), 9, 0); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "50")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 14: + if (points >= 20) { + plugin.getDb().deductPoints( + p.getName(), 20); + p.setLevel(p.getLevel() + 10); + p.sendMessage(plugin.getConfig2() + .msg("points_purchase_success") + .replace("{count}", "20")); + } else { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + } + break; + case 31: + if (cyInstance != null) { + p.openInventory( + createCYShopGUI(p)); + } + break; + } + } + + public void handleCYClick(Player p, int slot) { + if (slot < 0 || slot >= cyItems.size() + || slot >= 45) return; + Map item = cyItems.get(slot); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + if (stock == -1) { + p.sendMessage("§c该商品已下架"); + return; + } + int points = getPoints(p.getName()); + if (points < price) { + p.sendMessage(plugin.getConfig2() + .msg("points_insufficient") + .replace("{points}", + String.valueOf(points))); + return; + } + + plugin.getDb().deductPoints(p.getName(), price); + int actDays = lifetime ? 0 : days; + activateCY(p.getName(), slots, actDays); + + String tl = lifetime + ? "终身" : (days + "天"); + p.sendMessage("§a购买成功!获得 " + + tl + " +" + slots + "格 背包空间"); + } + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestStorage.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestStorage.java new file mode 100644 index 0000000..45dbabf --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestStorage.java @@ -0,0 +1,138 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class QuestStorage { + + private Connection conn; + + public QuestStorage(File dataFolder) { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + dataFolder, + "quest_data.db"); + conn = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getPath()); + initTable(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void initTable() + throws SQLException { + Statement stmt = + conn.createStatement(); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY " + + "(player, condition_key))"); + stmt.close(); + } + + /** + * 标记条件已完成(value = "1") + */ + public void setCondition( + String player, + String condition) { + setConditionRaw( + player, condition, "1"); + } + + /** + * 写入任意值(计数器等) + */ + public void setConditionRaw( + String player, + String condition, + String value) { + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO " + + "quest_conditions " + + "(player, " + + "condition_key, " + + "value, timestamp) " + + "VALUES " + + "(?, ?, ?, ?)"); + ps.setString(1, player); + ps.setString(2, condition); + ps.setString(3, value); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 查询条件值 + */ + public Object getCondition( + String player, + String condition) { + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value FROM " + + "quest_conditions " + + "WHERE player = ? " + + "AND " + + "condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + String val = + rs.getString("value"); + rs.close(); + ps.close(); + // 尝试解析为数字 + try { + if (val.contains(".")) { + return Double.parseDouble( + val); + } + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestTracker.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestTracker.java new file mode 100644 index 0000000..c147723 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/QuestTracker.java @@ -0,0 +1,3537 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +// 如果 Bonds 在 Sdf1_login 包下: +import Sdf1_login.BondManager; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final QuestStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new QuestStorage( + plugin.getDataFolder()); + + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 岩岸/沙滩 + BIOME_CATEGORY.put("minecraft:stony_shore", + "石岸"); +// 海岸 + BIOME_CATEGORY.put("minecraft:mushroom_fields", + "蘑菇岛"); +// 苍白之园 + BIOME_CATEGORY.put( + "minecraft:pale_garden", "森林"); +// 末地小型岛屿 + BIOME_CATEGORY.put( + "minecraft:small_end_islands", "末地"); +// 主世界结构归类 + BIOME_CATEGORY.put("minecraft:village", "平原"); + BIOME_CATEGORY.put( + "minecraft:desert_pyramid", "沙漠"); + BIOME_CATEGORY.put( + "minecraft:jungle_pyramid", "森林"); + BIOME_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + + + } + + // 所有群系大类列表 + private static final String[] + ALL_CATEGORIES = { + "海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "石岸", "蘑菇岛", "地下", + "下界", "末地" + }; + + // ===== 结构映射(PaperMC 1.21.4 实际结构 key)===== + private static final Map + STRUCTURE_CATEGORY = + new HashMap<>(); + static { + // 主世界结构 + STRUCTURE_CATEGORY.put( + "minecraft:village_plains", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_desert", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_savanna", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_snowy", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_taiga", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:desert_pyramid", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:jungle_pyramid", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:swamp_hut", "沼泽"); + STRUCTURE_CATEGORY.put( + "minecraft:igloo", "雪原"); + STRUCTURE_CATEGORY.put( + "minecraft:mansion", "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:pillager_outpost", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:mineshaft_mesa", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:ancient_city", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:trail_ruins", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:trial_chambers", + "地下"); + STRUCTURE_CATEGORY.put( + "minecraft:monument", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_cold", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_warm", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck_beached", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:buried_treasure", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal", + "平原"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_desert", + "沙漠"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_jungle", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_mountain", + "山地"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_ocean", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_swamp", + "沼泽"); + // 下界结构 + STRUCTURE_CATEGORY.put( + "minecraft:fortress", "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:bastion_remnant", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:nether_fossil", + "下界"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_nether", + "下界"); + // 末地结构 + STRUCTURE_CATEGORY.put( + "minecraft:end_city", "末地"); + } + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + CATEGORY_KEYWORDS.put("石岸", "石岸"); + CATEGORY_KEYWORDS.put("沙滩", "石岸"); + CATEGORY_KEYWORDS.put("海岸", "石岸"); + + // ===== 结构名称关键词映射 ===== + // 主世界结构 + CATEGORY_KEYWORDS.put("村庄", "村庄"); + CATEGORY_KEYWORDS.put("沙漠神殿", "沙漠"); + CATEGORY_KEYWORDS.put("丛林神庙", "森林"); + CATEGORY_KEYWORDS.put("沼泽小屋", "沼泽"); + CATEGORY_KEYWORDS.put("雪屋", "雪原"); + CATEGORY_KEYWORDS.put("林地府邸", "森林"); + CATEGORY_KEYWORDS.put("掠夺者前哨站", "平原"); + CATEGORY_KEYWORDS.put("废弃矿井", "地下"); + CATEGORY_KEYWORDS.put("要塞", "地下"); + CATEGORY_KEYWORDS.put("远古城市", "地下"); + CATEGORY_KEYWORDS.put("古迹废墟", "地下"); + CATEGORY_KEYWORDS.put("试炼密室", "地下"); + CATEGORY_KEYWORDS.put("海底废墟", "海洋"); + CATEGORY_KEYWORDS.put("沉船", "海洋"); + CATEGORY_KEYWORDS.put("埋藏的宝箱", "海洋"); + CATEGORY_KEYWORDS.put("废弃传送门", "主世界"); + CATEGORY_KEYWORDS.put("化石", "主世界"); + + // 下界结构 + CATEGORY_KEYWORDS.put("下界要塞", "下界"); + CATEGORY_KEYWORDS.put("猪灵堡垒", "下界"); + CATEGORY_KEYWORDS.put("玄武岩结构", "下界"); + + // 末地结构 + CATEGORY_KEYWORDS.put("末地城", "末地"); + CATEGORY_KEYWORDS.put("末地船", "末地"); + + // 下界相关别名 + CATEGORY_KEYWORDS.put("地狱要塞", "下界"); + CATEGORY_KEYWORDS.put("下界堡垒", "下界"); + + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 优先用世界名判断维度 + if (worldName != null) { + String w = + worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + + // 用群系映射表 + if (rawKey != null) { + String lower = + rawKey.toLowerCase(); + // 带 minecraft: 前缀的直接查 + String cat = + BIOME_CATEGORY.get(lower); + if (cat != null) + return cat; + + // 不带前缀的尝试补全 + String prefixed = + "minecraft:" + lower; + cat = BIOME_CATEGORY.get(prefixed); + if (cat != null) + return cat; + + // 模糊匹配群系名 + for (Map.Entry entry + : BIOME_CATEGORY.entrySet()) { + if (entry.getKey() + .contains(lower) + || lower.contains( + entry.getKey() + .replace( + "minecraft:", + ""))) { + return entry.getValue(); + } + } + } + + return "主世界"; + } + + // ========== 结构检测 ========== + + /** + * 检测指定位置是否存在自然生成的结构 + * 返回结构分类(如"村庄"、"沙漠"等),未找到返回null + * 使用 PaperMC World.hasStructureAt() + Registry.STRUCTURE API + */ + private String detectStructureAt( + String worldName, double x, + double y, double z) { + try { + World w = Bukkit.getWorld(worldName); + if (w == null) return null; + + // 使用 BlockPosition 提高性能 + io.papermc.paper.math.Position pos = + io.papermc.paper.math.Position.block( + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + + // 遍历所有自然生成的结构 + for (Structure structure : Registry.STRUCTURE) { + if (w.hasStructureAt(pos, structure)) { + // 获取结构 NamespacedKey(如 minecraft:village) + NamespacedKey structureKey = Registry.STRUCTURE.getKey(structure); + String keyStr = (structureKey != null) ? structureKey.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY.get(keyStr); + if (cat != null) { + plugin.getLogger().info( + "[QuestTracker] 检测到结构: " + + keyStr + + " -> " + cat); + return cat; + } + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测失败: " + + ex.getMessage()); + } + return null; + } + + /** + * 将结构中文名映射到对应的大类 + * 用于匹配任务条件中的结构名 + */ + private String mapStructureCategory( + String structureName) { + if (structureName == null) return ""; + + // 主世界结构 + if (structureName.contains("村庄")) + return "村庄"; + if (structureName.contains("沙漠神殿")) + return "沙漠"; + if (structureName.contains("丛林神庙")) + return "森林"; + if (structureName.contains("沼泽小屋")) + return "沼泽"; + if (structureName.contains("雪屋")) + return "雪原"; + if (structureName.contains("林地府邸")) + return "森林"; + if (structureName.contains("掠夺者前哨站")) + return "平原"; + if (structureName.contains("废弃矿井")) + return "地下"; + if (structureName.contains("要塞")) + return "地下"; + if (structureName.contains("远古城市")) + return "地下"; + if (structureName.contains("古迹废墟")) + return "地下"; + if (structureName.contains("试炼密室")) + return "地下"; + if (structureName.contains("海底废墟")) + return "海洋"; + if (structureName.contains("沉船")) + return "海洋"; + if (structureName.contains("埋藏的宝箱")) + return "海洋"; + if (structureName.contains("废弃传送门")) + return "主世界"; + if (structureName.contains("化石")) + return "主世界"; + + // 下界结构 + if (structureName.contains("下界要塞")) + return "下界"; + if (structureName.contains("猪灵堡垒")) + return "下界"; + if (structureName.contains("玄武岩结构")) + return "下界"; + + // 末地结构 + if (structureName.contains("末地城")) + return "末地"; + if (structureName.contains("末地船")) + return "末地"; + + return ""; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API或债券 + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage("§c无可领取的奖励"); + return; + } + + Main main = (Main) plugin; + // ★ 读取配置:economy = Vault,bonds = 债券(默认) + boolean useEconomy = "economy".equalsIgnoreCase( + main.getConfigMgr().rewardChannel); + + // Vault 经济引用(仅 useEconomy 时使用) + net.milkbowl.vault.economy.Economy econ = null; + if (useEconomy) { + econ = main.getEconomy(); + } + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) { + player.sendMessage( + "§7[可选奖励] 需完成所有可选条件: " + rt); + continue; + } + } + + // 按 + 分割子项 + String[] parts = rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试识别经济金额 + double amt = extractEconomyAmount(part); + if (amt > 0) { + // ★ 根据配置分发 + if (useEconomy && econ != null) { + // —— Vault 经济 —— + double before = + econ.getBalance(player); + econ.depositPlayer(player, amt); + double after = + econ.getBalance(player); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(余额: §e" + + String.format("%.2f", before) + + " §7→ §a" + + String.format("%.2f", after) + + "§7)"); + if (plugin instanceof Main) { + ((Main) plugin).getCommission() + .payCommission( + player.getName(), amt); + } + } else { + // —— 债券(默认) —— + int bondAmt = + (int) Math.round(amt); + BondManager bonds = main.getBonds(); + if (bonds != null) { + int before = bonds.getBonds( + player.getName()); + bonds.addBonds( + player.getName(), + bondAmt, + "quest_reward", + qf.displayName, + "任务系统", + "任务奖励:" + qf.displayName); + int after = bonds.getBonds( + player.getName()); + player.sendMessage( + "§a§l[任务奖励] §f" + part + + " §7(债券: §e" + + before + + " §7→ §a" + + after + "§7)"); + } else { + player.sendMessage( + "§c债券系统不可用"); + } + } + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory().addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + player.getName() + + " 预取了 \"" + qf.displayName + + "\" 共 " + sentCount + "项奖励"); + } + + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱") + || t.contains("债券"); // ★ 新增 + + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + public String getProgress( + String player, String rawCond) { + if (player == null || rawCond == null) + return "§7未知"; + String key = stripOptional(rawCond); + + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + + cur + "/1"; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "VILLAGER_TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + + } + + // ===== 击杀泛解析(支持中文列表) ===== +// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等 + if (key.contains("击杀") && key.contains("只")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + String mobPart = key; + java.util.regex.Matcher killM = + java.util.regex.Pattern + .compile("击杀\\d+只(.+)") + .matcher(key); + if (killM.find()) { + mobPart = killM.group(1).trim(); + } + + String[] mobNames = mobPart + .split("[、,,,]+"); + int totalKills = 0; + StringBuilder names = new StringBuilder(); + for (String name : mobNames) { + String trimmed = name.trim(); + if (trimmed.isEmpty()) continue; + String entityType = + chineseMobToEntity(trimmed); + if (!entityType.isEmpty()) { + totalKills += getCounter( + player, + "KILL:" + entityType); + if (names.length() > 0) + names.append("/"); + names.append(trimmed); + } + } + return "§7击杀" + names + ": §e" + + totalKills + "/" + needed; + } + +// 单一KILL:xxx:N 格式(原有逻辑保留) + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + int needed = parseInt(p[2]); + int cur = getCounter(player, + "KILL:" + p[1]); + return "§7击杀" + p[1] + ": §e" + + cur + "/" + needed; + } + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + // ===== 泛解析:击杀类进度 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < mobTypes.size(); i++) { + total += getCounter(player, + "KILL:" + mobTypes.get(i)); + if (i > 0) names.append("/"); + names.append(reverseMobName( + mobTypes.get(i))); + } + return "§7击杀" + names + ": §e" + + total + "/" + needed; + } + } + + // ===== 泛解析:村民交易进度 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "TRADE"); + return "§7村民交易: §e" + + cur + "/" + needed; + } + // ===== 泛解析:采集/挖矿进度 ===== + if (key.contains("采集") + || key.contains("挖")) { + int needed = + parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + StringBuilder names = + new StringBuilder(); + for (int i = 0; + i < matTypes.size(); i++) { + total += getCounter( + player, + "MINE:" + matTypes + .get(i)); + if (i > 0) + names.append("/"); + names.append( + translateMaterialName( + matTypes + .get(i))); + } + return "§7采集" + names + + ": §e" + total + + "/" + needed; + } + } + + // ===== 泛解析:合成进度 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "CRAFT"); + return "§7合成: §e" + cur + "/" + needed; + } + + // ===== 泛解析:钓鱼进度 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, "FISH"); + return "§7钓鱼: §e" + cur + "/" + needed; + } + + return "§7" + key; + } + private String translateMaterialName( + String name) { + switch (name) { + case "COAL_ORE": + return "煤矿石"; + case "DEEPSLATE_COAL_ORE": + return "深层煤矿石"; + case "IRON_ORE": + return "铁矿石"; + case "DEEPSLATE_IRON_ORE": + return "深层铁矿石"; + case "COPPER_ORE": + return "铜矿石"; + case "DEEPSLATE_COPPER_ORE": + return "深层铜矿石"; + case "GOLD_ORE": + return "金矿石"; + case "DEEPSLATE_GOLD_ORE": + return "深层金矿石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "DEEPSLATE_DIAMOND_ORE": + return "深层钻石矿石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "DEEPSLATE_EMERALD_ORE": + return "深层绿宝石矿石"; + case "LAPIS_ORE": + return "青金石矿石"; + case "DEEPSLATE_LAPIS_ORE": + return "深层青金石矿石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "DEEPSLATE_REDSTONE_ORE": + return "深层红石矿石"; + case "NETHER_GOLD_ORE": + return "下界金矿石"; + case "NETHER_QUARTZ_ORE": + return "下界石英矿石"; + case "ANCIENT_DEBRIS": + return "远古残骸"; + case "COAL": + return "煤炭"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "RAW_COPPER": + return "粗铜"; + case "DIAMOND": + return "钻石"; + case "EMERALD": + return "绿宝石"; + case "LAPIS_LAZULI": + return "青金石"; + case "REDSTONE": + return "红石"; + case "QUARTZ": + return "石英"; + default: + return name; + } + } + + /** + * 中文物种名 → Bukkit EntityType 名 + * 支持击杀泛解析的模糊匹配 + */ + private String chineseMobToEntity(String cn) { + String n = cn.toLowerCase().trim(); + + // 直接别名表 + Map mobMap = new HashMap<>(); + mobMap.put("骷髅", "SKELETON"); + mobMap.put("僵尸", "ZOMBIE"); + mobMap.put("苦力怕", "CREEPER"); + mobMap.put("蜘蛛", "SPIDER"); + mobMap.put("洞穴蜘蛛", "CAVE_SPIDER"); + mobMap.put("小白", "SKELETON"); + mobMap.put("末影人", "ENDERMAN"); + mobMap.put("enderman", "ENDERMAN"); + mobMap.put("猪灵", "PIGLIN"); + mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE"); + mobMap.put("恶魂", "GHAST"); + mobMap.put("凋灵骷髅", "WITHER_SKELETON"); + mobMap.put("烈焰人", "BLAZE"); + mobMap.put("史莱姆", "SLIME"); + mobMap.put("岩浆怪", "MAGMA_CUBE"); + mobMap.put("女巫", "WITCH"); + mobMap.put("蠹虫", "SILVERFISH"); + mobMap.put("潜影贝", "SHULKER"); + mobMap.put("守卫者", "GUARDIAN"); + mobMap.put("远古守卫者", "ELDER_GUARDIAN"); + mobMap.put("溺尸", "DROWNED"); + mobMap.put("僵尸村民", "ZOMBIE_VILLAGER"); + mobMap.put("尸壳", "HUSK"); + mobMap.put("流浪者", "STRAY"); + mobMap.put("幻翼", "PHANTOM"); + mobMap.put("掠夺者", "PILLAGER"); + mobMap.put("卫道士", "VINDICATOR"); + mobMap.put("唤魔者", "EVOKER"); + mobMap.put("恼鬼", "VEX"); + mobMap.put("劫掠兽", "RAVAGER"); + mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY"); + mobMap.put("凋灵", "WITHER"); + mobMap.put("末影龙", "ENDER_DRAGON"); + + // 精确匹配 + if (mobMap.containsKey(n)) { + return mobMap.get(n); + } + + // 模糊匹配(包含关系) + for (Map.Entry en + : mobMap.entrySet()) { + if (n.contains(en.getKey()) + || en.getKey().contains(n)) { + return en.getValue(); + } + } + + // 尝试直接用英文名 + try { + return org.bukkit.entity.EntityType + .valueOf(n.toUpperCase() + .replace(" ", "_")) + .name(); + } catch (Exception e) { + return ""; + } + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + // 结构检测(覆盖纯生物群系分类) + // 地下检测:Y低于表面则归类为地下 +// 覆盖废弃矿井/试炼密室/要塞/远古城市等 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + + // ===== 结构检测(覆盖纯生物群系分类)===== + // 使用 PaperMC API 检测玩家所在位置的结构 + String structureCat = + detectStructureAt( + worldName, x, y, z); + if (structureCat != null + && !structureCat.isEmpty()) { + // 结构检测成功,使用结构分类 + // 但保留"地下"分类(如果Y低于表面) + if (!"地下".equals(category)) { + category = structureCat; + } + plugin.getLogger().info( + "[QuestTracker] 结构覆盖: " + + category + + " (结构: " + + structureCat + ")"); + } + + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡")) + continue; + // 支持群系和结构两种打卡条件 + boolean isBiomeCond = + condKey.contains("群系") + || condKey + .contains("风景点"); + boolean isStructureCond = + condKey.contains("结构") + || condKey + .contains("村庄") + || condKey + .contains("神殿") + || condKey + .contains("神庙") + || condKey + .contains("小屋") + || condKey + .contains("雪屋") + || condKey + .contains("府邸") + || condKey + .contains("前哨站") + || condKey + .contains("矿井") + || condKey + .contains("要塞") + || condKey + .contains("城市") + || condKey + .contains("废墟") + || condKey + .contains("密室") + || condKey + .contains("沉船") + || condKey + .contains("宝箱") + || condKey + .contains("传送门") + || condKey + .contains("化石") + || condKey + .contains("堡垒"); + if (!isBiomeCond + && !isStructureCond) + continue; + String target = + extractBiome(condKey); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + /* + * 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录 + */ + public String checkInAtPosition( + String player, String worldName, + double x, double y, double z) { + + List history = + loadCheckInPositions(player); + for (double[] pos : history) { + double dist = Math.sqrt( + Math.pow(x - pos[0], 2) + + Math.pow(y - pos[1], 2) + + Math.pow(z - pos[2], 2)); + if (dist <= 32) { + return "§c距离上次打卡点太近(" + + String.format( + "%.0f", dist) + + "格)"; + } + } + + // ===== 获取群系和分类 ===== + String biome = "minecraft:plains"; + String category = "平原"; + String structureCat = null; + try { + org.bukkit.World w = + Bukkit.getWorld(worldName); + if (w != null) { + org.bukkit.Location loc = + new org.bukkit.Location( + w, + (int) Math.floor(x), + (int) Math.floor(y), + (int) Math.floor(z)); + biome = loc.getBlock() + .getBiome().getKey() + .toString(); + category = mapBiomeCategory( + biome, worldName); + + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + + // ===== 结构检测(覆盖纯生物群系分类)===== + structureCat = + detectStructureAt( + worldName, x, y, z); + if (structureCat != null + && !structureCat.isEmpty() + && !"地下".equals(category)) { + category = structureCat; + } + + if ("下界".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.NETHER) { + category = "下界"; + } else if ("末地".equals(category) + || w.getEnvironment() + == org.bukkit.World + .Environment.THE_END) { + category = "末地"; + } + } + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 群系读取失败: " + + ex.getMessage()); + } + + plugin.getLogger().info( + "[QuestTracker] 打卡: " + + player + + " world=" + worldName + + " biome=" + biome + + " cat=" + category + + " pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + + saveCheckInPosition( + player, x, y, z); + recordCheckIn( + player, biome, category, + worldName, x, y, z); + List matched = + checkInBiome( + player, biome, + worldName, x, y, z); + + StringBuilder sb = new StringBuilder(); + sb.append("§a打卡成功!群系: §e") + .append(category); + if (structureCat != null && !structureCat.isEmpty()) { + sb.append("\n§a结构: §e").append(structureCat); + } + if (!matched.isEmpty()) { + sb.append("\n§a完成任务条件:"); + for (String m : matched) { + String qn = + findQuestName(m); + sb.append("\n§7• "); + if (qn != null) { + sb.append("§e[") + .append(qn) + .append("§7] §f"); + } + sb.append(m); + } + } + return sb.toString(); + } + + + /** + * 根据条件文本查找所属任务名 + */ + private String findQuestName( + String condition) { + for (String cat : Arrays.asList( + "新人任务", + "主线任务", + "支线任务")) { + for (QuestFile qf : + getQuests(cat)) { + if (qf.conditions.contains( + condition)) { + return qf.displayName; + } + } + } + return null; + } + + + /** + * 加载玩家所有历史打卡坐标 + * 格式: x1,y1,z1;x2,y2,z2;... + */ + private List loadCheckInPositions( + String player) { + List list = new ArrayList<>(); + Object val = storage.getCondition( + player, "checkin_all_pos"); + if (val == null) return list; + String data = val.toString(); + if (data.isEmpty()) return list; + String[] records = data.split(";"); + for (String r : records) { + String[] parts = r.split(","); + if (parts.length >= 3) { + try { + list.add(new double[]{ + Double.parseDouble(parts[0]), + Double.parseDouble(parts[1]), + Double.parseDouble(parts[2])}); + } catch (NumberFormatException e) { + // 跳过损坏数据 + } + } + } + return list; + } + + /** + * 保存打卡坐标(追加到已有记录) + */ + private void saveCheckInPosition( + String player, double x, + double y, double z) { + List list = + loadCheckInPositions(player); + list.add(new double[]{x, y, z}); + StringBuilder sb = new StringBuilder(); + for (double[] pos : list) { + if (sb.length() > 0) sb.append(";"); + sb.append(pos[0]).append(",") + .append(pos[1]).append(",") + .append(pos[2]); + } + storage.setConditionRaw( + player, + "checkin_all_pos", + sb.toString()); + } + // ========== 中文生物名 → EntityType 映射 ========== + private static final Map + MOB_NAME_MAP = new HashMap<>(); + static { + MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER"); + MOB_NAME_MAP.put("骷髅", "SKELETON"); + MOB_NAME_MAP.put("僵尸", "ZOMBIE"); + MOB_NAME_MAP.put("苦力怕", "CREEPER"); + MOB_NAME_MAP.put("蜘蛛", "SPIDER"); + MOB_NAME_MAP.put("末影人", "ENDERMAN"); + MOB_NAME_MAP.put("女巫", "WITCH"); + MOB_NAME_MAP.put("烈焰人", "BLAZE"); + MOB_NAME_MAP.put("僵尸猪灵", + "ZOMBIFIED_PIGLIN"); + MOB_NAME_MAP.put("猪灵", "PIGLIN"); + MOB_NAME_MAP.put("恶魂", "GHAST"); + MOB_NAME_MAP.put("凋灵骷髅", + "WITHER_SKELETON"); + MOB_NAME_MAP.put("史莱姆", "SLIME"); + MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE"); + MOB_NAME_MAP.put("溺尸", "DROWNED"); + MOB_NAME_MAP.put("尸壳", "HUSK"); + MOB_NAME_MAP.put("流髑", "STRAY"); + MOB_NAME_MAP.put("幻翼", "PHANTOM"); + MOB_NAME_MAP.put("守卫者", "GUARDIAN"); + MOB_NAME_MAP.put("远古守卫者", + "ELDER_GUARDIAN"); + MOB_NAME_MAP.put("掠夺者", "PILLAGER"); + MOB_NAME_MAP.put("卫道士", "VINDICATOR"); + MOB_NAME_MAP.put("唤魔者", "EVOKER"); + MOB_NAME_MAP.put("恼鬼", "VEX"); + MOB_NAME_MAP.put("劫掠兽", "RAVAGER"); + MOB_NAME_MAP.put("潜影贝", "SHULKER"); + MOB_NAME_MAP.put("末影螨", "ENDERMITE"); + MOB_NAME_MAP.put("凋灵", "WITHER"); + MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON"); + MOB_NAME_MAP.put("监守者", "WARDEN"); + MOB_NAME_MAP.put("嗅探兽", "SNIFFER"); + MOB_NAME_MAP.put("僵尸村民", + "ZOMBIE_VILLAGER"); + } + + /** + * 从条件文本中提取所有匹配的生物类型 + * 按名称长度降序匹配,避免短名误匹配 + */ + private List extractMobTypes( + String text) { + List types = new ArrayList<>(); + List> sorted = + new ArrayList<>( + MOB_NAME_MAP.entrySet()); + sorted.sort((a, b) -> + b.getKey().length() + - a.getKey().length()); + String remaining = text; + for (Map.Entry entry + : sorted) { + if (remaining.contains( + entry.getKey())) { + if (!types.contains( + entry.getValue())) { + types.add(entry.getValue()); + } + remaining = remaining.replace( + entry.getKey(), ""); + } + } + return types; + } + + /** + * EntityType → 中文名 + */ + private String reverseMobName( + String entityType) { + for (Map.Entry entry + : MOB_NAME_MAP.entrySet()) { + if (entry.getValue() + .equals(entityType)) { + return entry.getKey(); + } + } + return entityType; + } + + /** + * 从条件文本中提取矿物名 + * "采集任意矿物(煤炭除外)1组" + * → [COAL_ORE, IRON_ORE, GOLD_ORE, ...] + */ + private List extractMaterialTypes( + String text) { + List types = new ArrayList<>(); + // 排除列表 + List excludes = + new ArrayList<>(); + if (text.contains("煤炭除外")) { + excludes.add("COAL"); + excludes.add("COAL_ORE"); + excludes.add("DEEPSLATE_COAL_ORE"); + } + // 通配符:"矿物" "矿石" + if (text.contains("矿物") + || text.contains("矿石")) { + String[] ores = { + "COAL_ORE", + "DEEPSLATE_COAL_ORE", + "IRON_ORE", + "DEEPSLATE_IRON_ORE", + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE", + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE", + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE", + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE", + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE", + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE", + "NETHER_GOLD_ORE", + "NETHER_QUARTZ_ORE", + "ANCIENT_DEBRIS" + }; + for (String ore : ores) { + if (!excludes.contains(ore)) { + types.add(ore); + } + } + return types; + } + // 单个矿物名 + if (text.contains("煤炭")) + types.add("COAL"); + if (text.contains("铁矿") + || text.contains("铁锭")) + types.add("RAW_IRON"); + if (text.contains("金矿") + || text.contains("金锭")) + types.add("RAW_GOLD"); + if (text.contains("钻石")) + types.add("DIAMOND"); + if (text.contains("绿宝石")) + types.add("EMERALD"); + if (text.contains("青金石")) + types.add("LAPIS_LAZULI"); + if (text.contains("红石")) + types.add("REDSTONE"); + if (text.contains("铜")) + types.add("RAW_COPPER"); + if (text.contains("下界石英") + || text.contains("石英")) + types.add("QUARTZ"); + return types; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + * "打卡村庄" → "村庄" + * "打卡沙漠神殿" → "沙漠" + */ + private String extractBiome(String text) { + if (text == null) return ""; + + // 优先匹配结构名称(更具体) + String structureResult = + mapStructureCategory(text); + if (!structureResult.isEmpty()) { + return structureResult; + } + + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点") + || key.contains("结构") + || key.contains("村庄") + || key.contains("神殿") + || key.contains("神庙") + || key.contains("小屋") + || key.contains("雪屋") + || key.contains("府邸") + || key.contains("前哨站") + || key.contains("矿井") + || key.contains("要塞") + || key.contains("城市") + || key.contains("废墟") + || key.contains("密室") + || key.contains("废墟") + || key.contains("沉船") + || key.contains("宝箱") + || key.contains("传送门") + || key.contains("化石") + || key.contains("堡垒"))) { + String biome = + extractBiome(key); + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; + } + return getCounter( + player, "biome_ALL") >= 1; + } + + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + if (key.contains("村民") + && key.contains("交易")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "VILLAGER_TRADE") + >= needed; + } + + + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter( + player, "REPORT_VILLAGE") + >= needed; + } + + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return streak >= needed; + } + + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + // ===== 泛解析:击杀类中文条件 ===== + if (key.contains("击杀") || key.contains("猎杀") + || key.contains("消灭")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List mobTypes = extractMobTypes(key); + if (!mobTypes.isEmpty()) { + int total = 0; + for (String mobType : mobTypes) { + total += getCounter(player, + "KILL:" + mobType); + } + return total >= needed; + } + } + + // ===== 泛解析:村民交易条件 ===== + if (key.contains("交易") + && (key.contains("村民") + || key.contains("与村民"))) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "TRADE") + >= needed; + } + + // ===== 泛解析:采集/挖矿类条件 ===== + if (key.contains("采集") || key.contains("挖")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + List matTypes = + extractMaterialTypes(key); + if (!matTypes.isEmpty()) { + int total = 0; + for (String mat : matTypes) { + total += getCounter(player, + "MINE:" + mat); + } + return total >= needed; + } + } + + // ===== 泛解析:合成类条件 ===== + if (key.contains("合成") || key.contains("制作")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "CRAFT") + >= needed; + } + + // ===== 泛解析:钓鱼类条件 ===== + if (key.contains("钓鱼") || key.contains("钓")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + return getCounter(player, "FISH") + >= needed; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public void onVillagerTrade(String player) { + String key = "VILLAGER_TRADE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioDownloadListener.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioDownloadListener.java new file mode 100644 index 0000000..d3e744f --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioDownloadListener.java @@ -0,0 +1,144 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player + .PlayerResourcePackStatusEvent; + +import java.util.UUID; +import java.util.concurrent + .ConcurrentHashMap; + +public class RadioDownloadListener + implements Listener { + + private final Main plugin; + private final java.util.logging.Logger log; + + private final ConcurrentHashMap + failCount = new ConcurrentHashMap<>(); + private final ConcurrentHashMap + blocked = new ConcurrentHashMap<>(); + + private static final int MAX_FAILED = 3; + + public RadioDownloadListener(Main plugin) { + this.plugin = plugin; + this.log = plugin.getLogger(); + } + + public void onPlayerQuit(UUID uuid) { + failCount.remove(uuid); + blocked.remove(uuid); + } + + @EventHandler + public void onStatus( + PlayerResourcePackStatusEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + String status = + e.getStatus().name(); + + log.info("[Radio] === 资源包状态 ==="); + log.info("[Radio] 玩家: " + + p.getName()); + log.info("[Radio] UUID: " + uuid); + log.info("[Radio] 状态: " + + status); + log.info("[Radio] 原始状态枚举: " + + e.getStatus().getClass() + .getSimpleName() + + "." + + e.getStatus().name()); + + if (blocked.containsKey(uuid)) { + log.info("[Radio] 已熔断,跳过: " + + p.getName()); + return; + } + + switch (status) { + case "SUCCESS": + log.info("[Radio] " + + p.getName() + + " ✓ 资源包加载成功"); + failCount.remove(uuid); + blocked.remove(uuid); + break; + + case "ACCEPTED": + log.info("[Radio] " + + p.getName() + + " 客户端已接受," + + "等待下载..."); + break; + + case "DECLINED": + log.warning("[Radio] " + + p.getName() + + " ✗ 拒绝了资源包"); + log.warning("[Radio] " + + "可能原因: " + + "1.客户端设置禁用资源包" + + " 2.已缓存拒绝记录" + + " 3.SSL证书不匹配"); + handleFailure(p, uuid); + break; + + case "FAILED_DOWNLOAD": + log.warning("[Radio] " + + p.getName() + + " ✗ 下载失败"); + log.warning("[Radio] " + + "可能原因: " + + "1.URL无法访问" + + " 2.SSL证书错误" + + " 3.网络超时" + + " 4.文件不存在(404)"); + handleFailure(p, uuid); + break; + + default: + log.info("[Radio] " + + p.getName() + + " 未知状态: " + + status); + break; + } + } + + private void handleFailure(Player p, UUID uuid) { + int failNum = failCount.getOrDefault(uuid, 0) + 1; + failCount.put(uuid, failNum); + + log.warning("[Radio] " + p.getName() + + " 失败次数: " + failNum + "/" + MAX_FAILED); + + if (failNum >= MAX_FAILED) { + blocked.put(uuid, true); + log.severe("[Radio] " + p.getName() + + " 已达最大失败次数,停止重试," + + "玩家继续留在服务器"); + // 不踢出,只记录熔断,玩家正常游玩 + p.sendMessage("§e§l[Radio] 资源包加载失败" + + ",已取消下载资源包,部分画面音频可能出现问题" + + "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员"); + + return; + } +// 打了包没测试 + p.sendMessage("§e§l[Radio] 下载失败," + + "重试... (" + failNum + "/" + MAX_FAILED + ")"); + + // 5秒后重试 + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline() && plugin.radio != null) { + plugin.radio.sendResourcePack(p); + } + }, 100L); + } +} \ No newline at end of file diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioManager.java new file mode 100644 index 0000000..6fe5eda --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RadioManager.java @@ -0,0 +1,1040 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.zip.*; +import java.util.Collections; +import org.bukkit.NamespacedKey; +import java.util.Set; + + +public class RadioManager { + + private final Main plugin; + private File radioDir; + private File oggDir; + private File packDir; + private BukkitTask mainRadioTask; + private final Map loginPlayers = + new ConcurrentHashMap<>(); + private String externalUrl = ""; + private int httpPort = 443; + private final Set pendingPack = + Collections.newSetFromMap( + new ConcurrentHashMap<>()); + private String lastPlayedSound = ""; + + private int radioIndex = 0; + + // ========== 登录阶段推送(管理员手动触发) ========== + public void startLoginRadio(Player p) { + loginPlayers.put(p.getName(), true); + } + + public void stopLoginRadio(Player p) { + loginPlayers.remove(p.getName()); + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + } + + private final List mainFiles = new ArrayList<>(); + private final List loginFiles = new ArrayList<>(); + + public RadioManager(Main plugin) { + this.plugin = plugin; + this.radioDir = new File( + plugin.getDataFolder(), "radio"); + this.oggDir = new File( + plugin.getDataFolder(), "radio/ogg"); + this.packDir = new File( + plugin.getDataFolder(), "radiopack"); + radioDir.mkdirs(); + oggDir.mkdirs(); + packDir.mkdirs(); + loadRadioConfig(); + } + // ========== 读取 radio_config.txt ========== + private void loadRadioConfig() { + File cfg = new File( + plugin.getDataFolder(), + "radio_config.txt"); + + // 不存在则创建默认配置 + if (!cfg.exists()) { + createRadioConfig(cfg); + plugin.getLogger().info( + "[Radio] 已生成 radio_config.txt"); + } + + // 读取 + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(cfg), + java.nio.charset + .StandardCharsets + .UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("#") + || line.trim().isEmpty()) + continue; + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq) + .trim(); + String val = line.substring(eq + 1) + .trim(); + if (key.equals("resource-pack-url")) { + externalUrl = val; + } else if (key.equals("http-port")) { + try { + httpPort = Integer.parseInt( + val); + } catch (NumberFormatException ignored) { + } + } + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 读取radio_config.txt失败: " + + e.getMessage()); + } + + // 443是HTTPS端口,内嵌HTTP不能用 + if (httpPort == 443) httpPort = 8080; + + plugin.getLogger().info( + "[Radio] 配置: externalUrl=[" + + externalUrl + "] httpPort=" + + httpPort); + } + + private void createRadioConfig(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + java.nio.charset + .StandardCharsets + .UTF_8))) { + pw.println("# ============ Sdf1 Radio Config ============"); + pw.println("#"); + pw.println("# resource-pack-url"); + pw.println("# 填外部地址(HTTP或HTTPS均可)"); + pw.println("# 例: https://example.com/radiopack.zip"); + pw.println("# 留空 = 使用内置HTTP服务"); + pw.println("#"); + pw.println("# http-port"); + pw.println("# 内置HTTP端口(仅resource-pack-url为空时生效)"); + pw.println("#"); + pw.println("# ============ Settings ==============="); + pw.println(); + pw.println("resource-pack-url="); + pw.println("http-port=8080"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 创建radio_config.txt失败: " + + e.getMessage()); + } + } + + // ========== 初始化 ========== + public void init() { + loadRadioConfig(); + convertNonOgg(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); // 启动HTTP + plugin.getLogger().info( + "[Radio] 初始化完成: 全服=" + + mainFiles.size() + + " 登录=" + loginFiles.size()); + } + + + // ========== 转换非ogg文件 ========== + private void convertNonOgg() { + convertDir(radioDir); + convertDir(new File(radioDir, "login")); + convertDir(new File(radioDir, "reg")); + } + + private void convertDir(File src) { + if (!src.exists() || !src.isDirectory()) return; + File[] files = src.listFiles((d, n) -> { + String l = n.toLowerCase(); + return l.endsWith(".mp3") + || l.endsWith(".wav") + || l.endsWith(".flac"); + }); + if (files == null) return; + + for (File f : files) { + String oggName = f.getName() + .replaceAll("\\.(mp3|wav|flac)$", + ".ogg"); + File oggFile = new File(src, oggName); + if (oggFile.exists() + && oggFile.lastModified() + >= f.lastModified()) { + continue; + } + // 调用ffmpeg转换 + if (convertWithFfmpeg(f, oggFile)) { + plugin.getLogger().info( + "[Radio] 转换成功: " + + f.getName()); + } else { + plugin.getLogger().warning( + "[Radio] 转换失败: " + + f.getName() + + " (需要安装ffmpeg)"); + } + } + } + + private boolean convertWithFfmpeg( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-c:a", "libvorbis", + "-q:a", "5", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean finished = + p.waitFor(30, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + return false; + } + } + + // ========== 加载ogg文件列表 ========== + private void loadAllOggs() { + mainFiles.clear(); + loginFiles.clear(); + loadOggsFlat(radioDir, mainFiles); + loadOggsFlat( + new File(radioDir, "login"), + loginFiles); + loadOggsFlat( + new File(radioDir, "reg"), + loginFiles); + } + + private void loadOggsFlat( + File dir, List list) { + if (!dir.exists() || !dir.isDirectory()) + return; + File[] files = dir.listFiles( + (d, n) -> n.toLowerCase() + .endsWith(".ogg")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) list.add(f); + } + /** + * 解析 pack.png:优先读取插件目录, + * 没有则搜索图片并用ffmpeg转码 + */ + private File resolvePackIcon() { + File dataDir = plugin.getDataFolder(); + File packPng = new File(dataDir, "pack.png"); + + if (packPng.exists() + && packPng.length() > 0) { + plugin.getLogger().info( + "[Radio] 找到 pack.png (" + + packPng.length() + + " bytes)"); + return packPng; + } + + // 搜索可转换的图片文件 + String[] exts = {"png", "jpg", "jpeg", + "bmp", "webp", "tiff", "gif"}; + File[] found = dataDir.listFiles( + (dir, name) -> { + String low = name.toLowerCase(); + for (String ext : exts) { + if (low.endsWith("." + ext) + && !name.equals( + "pack.png")) { + return true; + } + } + return false; + }); + + if (found != null && found.length > 0) { + File src = found[0]; + plugin.getLogger().info( + "[Radio] 找到图片: " + + src.getName() + + " → 转码为 pack.png"); + if (convertToPackPng(src, packPng)) { + return packPng; + } + plugin.getLogger().warning( + "[Radio] 转码失败"); + return null; + } + + plugin.getLogger().warning( + "[Radio] 未找到 pack.png 或可转换图片"); + return null; + } + + /** + * ffmpeg 将任意图片转为 256x256 PNG + */ + private boolean convertToPackPng( + File input, File output) { + try { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), + "-vf", + "scale='min(256,iw)'" + + ":'min(256,ih)':" + + "force_original_aspect_ratio" + + "=decrease," + + "pad=256:256:" + + "(ow-iw)/2:" + + "(oh-ih)/2:" + + "color=0x00000000", + "-c:v", "png", + output.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + boolean ok = p.waitFor( + 30, TimeUnit.SECONDS); + if (!ok) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] ffmpeg转码失败: " + + e.getMessage()); + return false; + } + } + + // ========== 资源包构建 ========== + private void buildResourcePack() { + deleteDir(packDir); + packDir.mkdirs(); + + List allOggs = new ArrayList<>(); + collectOggs(radioDir, allOggs); + + if (allOggs.isEmpty()) { + plugin.getLogger().warning( + "[Radio] radio/ 目录下" + + "无 .ogg 文件,跳过资源包"); + return; + } + + // 正确路径:assets/minecraft/sounds/custom/ + File soundsDir = new File(packDir, + "assets/minecraft/sounds/custom"); + soundsDir.mkdirs(); + + // 生成 sounds.json + StringBuilder json = + new StringBuilder("{\n"); + for (int i = 0; i < allOggs.size(); i++) { + File ogg = allOggs.get(i); + String simpleName = + ogg.getName() + .replace(".ogg", ""); + File dest = new File(soundsDir, + simpleName + ".ogg"); + try { + Files.copy(ogg.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception e) { + // ignore + } + // 音频事件名: custom.xxx + // 文件路径: custom/xxx + json.append(" \"custom.") + .append(simpleName) + .append("\": {\n"); + json.append(" \"sounds\": [{\n"); + json.append(" \"name\":" + + " \"custom/") + .append(simpleName) + .append("\",\n"); + json.append( + " \"stream\": true\n"); + json.append(" }]\n"); + json.append(" }"); + if (i < allOggs.size() - 1) + json.append(","); + json.append("\n"); + } + json.append("}"); + + // sounds.json 放在 assets/minecraft/ 下 + try { + Files.write(new File(packDir, + "assets/minecraft/sounds.json") + .toPath(), + json.toString() + .getBytes("UTF-8")); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] sounds.json写入失败"); + } +// sounds.json 写入后... + +// ===== 新增:打包 pack.png 作为资源包图标 ===== + File iconPng = resolvePackIcon(); + if (iconPng != null && iconPng.exists()) { + try { + Files.copy(iconPng.toPath(), + new File(packDir, "pack.png") + .toPath(), + StandardCopyOption + .REPLACE_EXISTING); + plugin.getLogger().info( + "[Radio] pack.png 已加入资源包"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] pack.png复制失败: " + + e.getMessage()); + } + } + + // pack.mcmeta(pack_format=54) + try { + Files.write(new File(packDir, + "pack.mcmeta").toPath(), + ("{\n \"pack\": {\n" + + " \"pack_format\": 54,\n" + + " \"description\":" + + " \"§d§l草原探险服务器资源包,请勿拒绝资源包\"\n }\n}") + .getBytes("UTF-8")); + } catch (Exception e) { + // ignore + } + + try { + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + packZip(packDir, zipFile); + plugin.getLogger().info( + "[Radio] 资源包生成: " + + allOggs.size() + " 个音频"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] zip打包失败: " + + e.getMessage()); + } + } + + private void deleteDir(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) deleteDir(f); + else f.delete(); + } + } + dir.delete(); + } + + private void collectOggs( + File dir, List list) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) + collectOggs(f, list); + else if (f.getName().endsWith(".ogg")) + list.add(f); + } + } + + private void packZip(File root, + File zipFile) throws Exception { + try (ZipOutputStream zos = + new ZipOutputStream( + new FileOutputStream( + zipFile))) { + zipDir(root, root, zos); + } + } + + private void zipDir(File root, + File current, + ZipOutputStream zos) + throws Exception { + File[] files = current.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + zipDir(root, f, zos); + } else { + String path = root.toURI() + .relativize(f.toURI()) + .getPath(); + zos.putNextEntry(new ZipEntry(path)); + zos.write(Files.readAllBytes( + f.toPath())); + zos.closeEntry(); + } + } + } + + // ========== 资源包发送 ========== + + /** + * 发送资源包(带限流,不禁重试) + */ + private final Map lastSendTime = + new ConcurrentHashMap<>(); + + public void sendResourcePack(Player p) { + UUID uuid = p.getUniqueId(); + + // 最短间隔 5秒(不是10秒的全锁死) + Long last = lastSendTime.get(uuid); + if (last != null + && System.currentTimeMillis() - last + < 5000L) { + plugin.getLogger().info( + "[Radio] 跳过(5秒内): " + + p.getName()); + return; + } + lastSendTime.put(uuid, + System.currentTimeMillis()); + + String url = getResourcePackUrl(); + if (url == null || url.isEmpty()) { + plugin.getLogger().warning( + "[Radio] URL为空,跳过: " + + p.getName()); + return; + } + + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) { + plugin.getLogger().warning( + "[Radio] radiopack.zip不存在," + + "跳过: " + p.getName()); + return; + } + + try { + byte[] data = + Files.readAllBytes(zip.toPath()); + String hash = sha1(data); + long fileSize = zip.length(); + + plugin.getLogger().info( + "[Radio] 发送: " + p.getName() + + " url=" + url + + " size=" + fileSize + + " hash=" + hash); + + sendPackWithHash(p, url, hash); + + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] 发送异常: " + + p.getName()); + e.printStackTrace(); + } + } + + + + private void sendPackWithHash( + Player p, + String url, + String hash) { + + UUID uuid = UUID.fromString( + "a1b2c3d4-e5f6-7890-abcd-" + + "ef1234567890"); + + // 尝试5参 + Component + try { + Object component = + net.kyori.adventure.text + .Component.text( + "§d§l请下载自定义资源包"); + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + net.kyori.adventure + .text.Component + .class, + boolean.class) + .invoke( + p, uuid, url, + hash, component, + true); + plugin.getLogger().info( + "[Radio] 成功(5参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试5参 + String描述 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + String.class, + boolean.class) + .invoke( + p, uuid, url, + hash, + "Sdf1 Radio", + true); + plugin.getLogger().info( + "[Radio] 成功(5参String): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 尝试3参 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class) + .invoke( + p, uuid, url, + hash); + plugin.getLogger().info( + "[Radio] 成功(3参): " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // ★ 全部失败 → 通知玩家 + 熔断 + plugin.getLogger().severe( + "[Radio] 所有签名失败: " + + p.getName() + + " 客户端版本: " + + p.getProtocolVersion()); + p.sendMessage( + "§e§l[Radio] §f资源包发送失败," + + "§7部分功能可能异常"); + p.sendMessage( + "§7可尝试: §e/rejoin §7或重启客户端"); + } + + + + private void sendPackFromLocal( + Player p, String url) { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) return; + try { + byte[] data = + Files.readAllBytes( + zip.toPath()); + String hash = sha1(data); + sendPackWithHash(p, url, hash); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 降级发送失败"); + } + } + + // 从URL下载字节 + private byte[] downloadBytes(String urlStr) + throws Exception { + java.net.URL url = + new java.net.URL(urlStr); + java.net.HttpURLConnection conn = + (java.net.HttpURLConnection) + url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + try (java.io.InputStream is = + conn.getInputStream()) { + return is.readAllBytes(); + } + } + + // SHA-1计算 + private String sha1(byte[] data) + throws Exception { + java.security.MessageDigest md = + java.security.MessageDigest + .getInstance("SHA-1"); + md.update(data); + byte[] hash = md.digest(); + StringBuilder sb = + new StringBuilder(); + for (byte b : hash) + sb.append(String.format( + "%02x", b)); + return sb.toString(); + } + + public void clearPending(UUID uuid) { + lastSendTime.remove(uuid); + } + + + + // ========== 内嵌HTTP服务 ========== + private com.sun.net.httpserver.HttpServer + httpServer; + + private void startHttpServer() { + // 如果配置了外部地址则不启动 + if (plugin.getConfig2() != null + && !plugin.getConfig2() + .broadcastServerUrl + .isEmpty()) { + return; + } + try { + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + httpServer = + com.sun.net.httpserver + .HttpServer.create( + new java.net + .InetSocketAddress( + port), 0); + httpServer.createContext( + "/radiopack.zip", + exchange -> { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (zip.exists()) { + byte[] data = + Files.readAllBytes( + zip.toPath()); + exchange.getResponseHeaders() + .set("Content-Type", + "application/zip"); + exchange.getResponseHeaders() + .set("Access-Control" + + "-Allow-Origin", + "*"); + exchange.sendResponseHeaders( + 200, data.length); + exchange.getResponseBody() + .write(data); + exchange.getResponseBody() + .close(); + } else { + byte[] empty = + "not found".getBytes(); + exchange.sendResponseHeaders( + 404, empty.length); + exchange.getResponseBody() + .write(empty); + exchange.getResponseBody() + .close(); + } + }); + httpServer.setExecutor(null); + httpServer.start(); + plugin.getLogger().info( + "[Radio] HTTP服务: http://0.0.0.0:" + + port + "/radiopack.zip"); + } catch (Exception e) { + plugin.getLogger().severe( + "[Radio] HTTP启动失败: " + + e.getMessage()); + } + } + + private void stopHttpServer() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + } + + public String getResourcePackUrl() { + // 有外部配置 → 随机生成URL + if (externalUrl != null + && !externalUrl.trim().isEmpty()) { + return generateRandomUrl(externalUrl); + } + // 无配置 → 用内置HTTP + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + return "http://127.0.0.1:" + + port + "/radiopack.zip"; + } + + + /** + * 从配置的域名列表中随机选一个 + * 生成随机、带时间戳的完整资源包URL + * + * 规则: + * *.example.com + * → 随机子域+时间戳 + * 例: a3f7k2.example.com/pack.zip?t=xxx&v=b9c1e4 + * + * cdn.example.com (无*) + * → 直接使用+时间戳 + * 例: cdn.example.com/pack.zip?t=xxx + */ + private String generateRandomUrl( + String raw) { + String[] parts = raw.split("\\|"); + String pick = + parts[(int)(Math.random() + * parts.length)].trim(); + + // 去掉协议 https:// http:// + String cleaned = pick + .replaceFirst("^https?://", ""); + // 去掉路径 /path/a?v=1 + if (cleaned.contains("/")) { + cleaned = cleaned.substring( + 0, cleaned.indexOf('/')); + } + + // 随机子域 + String hex = Long.toHexString( + Double.doubleToLongBits( + Math.random())); + String rand = hex.length() > 6 + ? hex.substring(2, 8) : hex; + rand = rand.replaceAll( + "[^a-z0-9]", "x"); + + String host; + if (cleaned.startsWith("*")) { + host = rand + + cleaned.substring(1); + } else { + host = cleaned; + } + + String ts = String.valueOf( + System.currentTimeMillis()); + String token = Integer.toHexString( + (int)(Math.random() + * 0xFFFFFF)); + + String url = "https://" + host + + "/radiopack.zip" + + "?t=" + ts + + "&v=" + token; + + plugin.getLogger().info( + "[Radio] 生成URL: " + + pick + " → " + url); + return url; + } + + + + // ========== 全服广播 ========== + public void startMainRadio() { + stopMainRadio(); + if (mainFiles.isEmpty()) { + plugin.getLogger().warning( + "[Radio] 无 .ogg 文件"); + return; + } + plugin.getLogger().info( + "[Radio] 启动全服广播 (" + + mainFiles.size() + " 个文件)"); + mainRadioTask = new BukkitRunnable() { + @Override + public void run() { + playNext(); + } + }.runTaskTimer(plugin, 100L, 2400L); + } + + private void playNext() { + if (mainFiles.isEmpty()) return; + File file = mainFiles.get( + radioIndex % mainFiles.size()); + String name = file.getName() + .replace(".ogg", ""); + lastPlayedSound = "custom." + name; + String key = "minecraft:" + lastPlayedSound; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + radioIndex++; + } + + // ========== 按名称播放 ========== + public void playToAll(String name) { + String key = findSongKey(name); + if (key == null) return; + lastPlayedSound = "custom." + name; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + } + + + public void playToPlayer( + Player p, String name) { + String key = findSongKey(name); + if (key == null) { + p.sendMessage("§c未找到: " + name); + return; + } + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + + public void playToLoginPlayers(String name) { + String key = findSongKey(name); + if (key == null) return; + for (String pName + : loginPlayers.keySet()) { + Player lp = Bukkit.getPlayer(pName); + if (lp != null && lp.isOnline()) { + lp.playSound(lp.getLocation(), + key, + SoundCategory.RECORDS, + 1.5f, 1.0f); + } + } + } + + + private String findSongKey(String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + return "minecraft:custom." + name; + } + } + plugin.getLogger().warning( + "[Radio] 未找到歌曲: " + name); + return null; + } + + + public boolean hasSong(String name) { + return findSongKey(name) != null; + } + + // ========== 手动播放 ========== + public boolean playSpecific( + Player sender, String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + String key = "sdf1radio:" + name; + for (Player p + : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + + name); + return true; + } + } + if (sender != null) + sender.sendMessage("§c未找到: " + name); + return false; + } + public void stopMainRadio() { + if (mainRadioTask != null) { + mainRadioTask.cancel(); + mainRadioTask = null; + } + if (lastPlayedSound != null + && !lastPlayedSound.isEmpty()) { + for (Player p : Bukkit.getOnlinePlayers()) { + p.stopSound(lastPlayedSound, + SoundCategory.RECORDS); + } + lastPlayedSound = ""; + } + } + +// 在 init() 或 registerEvents 中保存引用: +// this.listener = new RadioDownloadListener(plugin); +private RadioDownloadListener radioListener; + + + + // ========== 重载 ========== + public void reload() { + stopMainRadio(); + stopAllLoginRadio(); + stopHttpServer(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); + loadRadioConfig(); + } +} \ No newline at end of file diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/RefundRecord.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RefundRecord.java new file mode 100644 index 0000000..c0a3070 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RefundRecord.java @@ -0,0 +1,62 @@ +package Sdf1_login; + +import org.bukkit.Material; +import java.util.UUID; + +public class RefundRecord { + public final UUID playerUUID; + public final String playerName; + public final String itemId; + public final String itemName; + public final Material material; + public final Material shulkerMaterial; + public int amount; + public int totalPaid; + public final long timestamp; + + // 普通购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, int amount, + int totalPaid, long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = null; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + // 盒装购买 + public RefundRecord(UUID uuid, String name, + String itemId, String itemName, + Material material, + Material shulkerMat, + int amount, int totalPaid, + long timestamp) { + this.playerUUID = uuid; + this.playerName = name; + this.itemId = itemId; + this.itemName = itemName; + this.material = material; + this.shulkerMaterial = shulkerMat; + this.amount = amount; + this.totalPaid = totalPaid; + this.timestamp = timestamp; + } + + public boolean isExpired() { + return System.currentTimeMillis() + - timestamp > 300000L; + } + + public int getSecondsLeft() { + long left = 300000L + - (System.currentTimeMillis() + - timestamp); + return (int) Math.max(0, left / 1000); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/RiskControlManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RiskControlManager.java new file mode 100644 index 0000000..4ddedcc --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/RiskControlManager.java @@ -0,0 +1,248 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警 + */ +public class RiskControlManager { + + private final Main plugin; + + // 每个玩家名 -> 连续失败次数 + private final Map failAttempts = + new ConcurrentHashMap<>(); + + // 每个IP -> 关联的可疑玩家列表 + private final Map> ipSuspicious = + new ConcurrentHashMap<>(); + + // 每个玩家 -> 上次验证时间(用于冷却) + private final Map verifyCooldown = + new ConcurrentHashMap<>(); + + // 配置项 + private static final int MAX_FAIL_ATTEMPTS = 5; + private static final int MAX_ACCOUNTS_PER_IP = 5; + private static final long COOLDOWN_MS = 60000L; + + public RiskControlManager(Main plugin) { + this.plugin = plugin; + } + + /** + * 记录一次失败的登录尝试 + * + * @param playerName 玩家名 + * @return 剩余尝试次数 + */ + public int recordFailAttempt(String playerName) { + int current = failAttempts + .getOrDefault(playerName, 0) + 1; + failAttempts.put(playerName, current); + return Math.max(0, MAX_FAIL_ATTEMPTS - current); + } + + /** + * 重置指定玩家的失败计数(登录成功时调用) + */ + public void resetFailAttempts(String playerName) { + failAttempts.remove(playerName); + } + + /** + * 获取指定玩家的失败次数 + */ + public int getFailAttempts(String playerName) { + return failAttempts + .getOrDefault(playerName, 0); + } + + /** + * 判断玩家是否已被锁定(失败次数达到上限) + */ + public boolean isLocked(String playerName) { + return failAttempts + .getOrDefault(playerName, 0) + >= MAX_FAIL_ATTEMPTS; + } + + /** + * 检查IP是否达到注册上限 + * 直接查数据库,不依赖 DatabaseManager 的方法 + */ + public boolean canRegisterFromIP(String ip) { + if (ip == null || ip.isEmpty()) return true; + try { + java.sql.Connection conn = + plugin.getDb().getDb(); + if (conn == null) return true; + java.sql.PreparedStatement ps = + conn.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address = ?"); + ps.setString(1, ip); + java.sql.ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count < 5; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + + /** + * 记录IP关联的可疑行为 + */ + public void recordSuspiciousIP(String ip, + String playerName) { + if (ip == null || ip.isEmpty()) return; + ipSuspicious + .computeIfAbsent(ip, + k -> ConcurrentHashMap.newKeySet()) + .add(playerName); + } + + /** + * 获取某IP关联的所有可疑玩家 + */ + public Set getSuspiciousAccounts(String ip) { + return ipSuspicious + .getOrDefault(ip, Collections.emptySet()); + } + + /** + * 检查是否在验证冷却期内 + */ + public boolean isOnCooldown(String playerName) { + Long last = verifyCooldown.get(playerName); + if (last == null) return false; + return System.currentTimeMillis() - last + < COOLDOWN_MS; + } + + /** + * 设置验证冷却时间 + */ + public void setCooldown(String playerName) { + verifyCooldown.put(playerName, + System.currentTimeMillis()); + } + + /** + * 当检测到可疑登录时触发报警 + * - 全服广播 + * - 有邮箱则发邮件 + * - 记录到数据库 + */ + public void triggerAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + + // 记录到数据库 + plugin.getDb().recordSecurityAlert( + name, suspiciousIP); + + // 记录IP可疑行为 + recordSuspiciousIP(suspiciousIP, name); + + // 全服广播 + String serverMsg = "§c§l[安全警告] §e" + name + + " §f的账号疑似遭到 §c" + suspiciousIP + + " §f的盗号尝试, 请各位玩家注意账号安全!"; + Bukkit.broadcastMessage(serverMsg); + + // 有邮箱则发邮件 + String emailAddr = (String) plugin.getDb() + .getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String subject = + "[安全警告] 您的账号疑似遭到盗号尝试"; + String body = "尊敬的玩家 " + name + + ":\n\n" + + "您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + suspiciousIP + + " 的盗号尝试。\n\n" + + "建议您:\n" + + "1. 立即修改密码" + + "(使用 /sdf1_login pw)\n" + + "2. 如非本人操作," + + "请联系管理员处理\n\n" + + "--Sdf1_login 安全系统"; + plugin.getEmail().sendTempPassword( + emailAddr, name, body); + } + } + + /** + * 处理登录失败时的风控逻辑 + * + * @param player 玩家 + * @param suspiciousIP 可疑来源IP + * @return true=已被锁定(应踢出), false=仍可继续尝试 + */ + public boolean handleLoginFailure(Player player, + String suspiciousIP) { + String name = player.getName(); + int remaining = recordFailAttempt(name); + + if (remaining <= 0) { + // 达到上限,触发报警 + triggerAlert(player, suspiciousIP); + player.kickPlayer( + "§c§l[风控] §f登录失败次数过多," + + " 账号已被临时锁定, 请联系管理员"); + return true; + } + + // 未达上限,提示剩余次数 + player.sendMessage( + "§c§l[风控] §f密码错误! " + + "剩余" + remaining + "次机会"); + return false; + } + + /** + * 登录成功后重置风控状态 + */ + public void onLoginSuccess(String playerName) { + resetFailAttempts(playerName); + } + + /** + * 清理过期的冷却记录(可由定时任务调用) + */ + public void cleanup() { + long now = System.currentTimeMillis(); + verifyCooldown.entrySet().removeIf( + e -> now - e.getValue() > COOLDOWN_MS * 10); + } + + /** + * 获取所有玩家的失败尝试记录(调试用) + */ + public Map getAllFailAttempts() { + return Collections.unmodifiableMap(failAttempts); + } + + /** + * 清除所有风控数据(管理员调试用) + */ + public void clearAll() { + failAttempts.clear(); + ipSuspicious.clear(); + verifyCooldown.clear(); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/SalesStatsManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/SalesStatsManager.java new file mode 100644 index 0000000..9d06a22 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/SalesStatsManager.java @@ -0,0 +1,170 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class SalesStatsManager { + + private final Main plugin; + + private final Map sessionSales = + new ConcurrentHashMap<>(); + private int sessionRevenue = 0; + private int sessionOrders = 0; + private int sessionItems = 0; + private int sessionRefunds = 0; + private int sessionRefundAmount = 0; + private long sessionStart = System.currentTimeMillis(); + + // 状态 + private boolean waitingForEmpty = false; + private boolean hasReported = false; + private BukkitRunnable waitTask = null; + + public SalesStatsManager(Main plugin) { + this.plugin = plugin; + startMonitor(); + } + + // ★ 每10秒检测一次 + private void startMonitor() { + new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 有玩家在线 → 重置等待状态 + if (waitingForEmpty) { + // 刚有人进来了,清除已汇报标记 + // 下次没人时会重新汇报 + } + waitingForEmpty = false; + hasReported = false; + cancelWaitTask(); + } else if (!waitingForEmpty && !hasReported) { + // 没人了,且还没汇报过 → 开始1分钟倒计时 + waitingForEmpty = true; + startWaitCountdown(); + } + } + }.runTaskTimer(plugin, 200L, 200L); + } + + // ★ 1分钟等待 + private void startWaitCountdown() { + cancelWaitTask(); + waitTask = new BukkitRunnable() { + @Override + public void run() { + int online = Bukkit.getOnlinePlayers().size(); + + if (online > 0) { + // 等待期间有人进来 → 取消,等下次没人 + waitingForEmpty = false; + hasReported = false; + waitTask = null; + cancel(); + return; + } + + // 1分钟没人 → 汇报一次 + if (!hasReported) { + hasReported = true; + report(); + } + waitingForEmpty = false; + waitTask = null; + cancel(); + } + }; + waitTask.runTaskLater(plugin, 1200L); // 60秒 + } + + private void cancelWaitTask() { + if (waitTask != null) { + waitTask.cancel(); + waitTask = null; + } + } + + // ===== 记录 ===== + + public void recordSale(String itemName, int price) { + sessionSales.merge(itemName, 1, Integer::sum); + sessionRevenue += price; + sessionOrders++; + sessionItems++; + } + + public void recordRefund(int amount) { + sessionRefunds++; + sessionRefundAmount += amount; + } + + // ===== 汇报 ===== + + public void report() { + if (sessionItems == 0 && sessionRefunds == 0) { + // plugin.getLogger().info("[Sales] 无数据,跳过"); + return; + } + + long minutes = + (System.currentTimeMillis() - sessionStart) + / 60000; + if (minutes < 1) minutes = 1; + + StringBuilder sb = new StringBuilder(); + sb.append("§6§l========== 销量汇总 =========="); + sb.append("\n§7时长: §f").append(minutes).append("分钟"); + sb.append("\n§7订单: §f").append(sessionOrders).append("笔"); + sb.append("\n§7商品: §f").append(sessionItems).append("件"); + sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚"); + sb.append("\n§7退款: §c").append(sessionRefunds) + .append("次(§c").append(sessionRefundAmount) + .append("枚§7)"); + sb.append("\n§6§l------------------------------"); + + if (!sessionSales.isEmpty()) { + sb.append("\n§7销量排行:"); + List> sorted = + new ArrayList<>(sessionSales.entrySet()); + sorted.sort((a, b) -> + b.getValue() - a.getValue()); + int rank = 1; + for (Map.Entry e : sorted) { + if (rank > 10) break; + sb.append("\n§e").append(rank).append(". ") + .append(e.getKey()) + .append(" §7x").append(e.getValue()); + rank++; + } + } + sb.append("\n§6§l=============================="); + + String msg = sb.toString(); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.isOp()) { + for (String line : msg.split("\n")) + p.sendMessage(line); + } + } + plugin.getLogger().info("[Sales] 汇报完成"); + resetSession(); + } + + public void resetSession() { + sessionSales.clear(); + sessionRevenue = 0; + sessionOrders = 0; + sessionItems = 0; + sessionRefunds = 0; + sessionRefundAmount = 0; + sessionStart = System.currentTimeMillis(); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopCategory.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopCategory.java new file mode 100644 index 0000000..715e9ad --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopCategory.java @@ -0,0 +1,32 @@ +package Sdf1_login; + +import java.util.ArrayList; +import java.util.List; + +public class ShopCategory { + private final String name; + private final String fileName; + private final List items; + + public ShopCategory(String name, String fileName) { + this.name = name; + this.fileName = fileName; + this.items = new ArrayList<>(); + } + + public String getName() { return name; } + public String getFileName() { return fileName; } + public List getItems() { return items; } + + public ShopItem getItem(String id) { + for (ShopItem item : items) { + if (item.getId().equalsIgnoreCase(id)) + return item; + } + return null; + } + + public void addItem(ShopItem item) { + items.add(item); + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopItem.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopItem.java new file mode 100644 index 0000000..d336c7c --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopItem.java @@ -0,0 +1,49 @@ +package Sdf1_login; + +import org.bukkit.Material; + +public class ShopItem { + private final String id; + private final String displayName; + private final Material material; + private final int buyPrice; + private final int sellPrice; + private int stock; + private int hourlySales; + private int totalSales; + public ShopItem(String id, String displayName, + Material material, int buyPrice, + int sellPrice, int stock, + int hourlySales, int totalSales) { + this.id = id; + this.displayName = displayName; + this.material = material; + this.buyPrice = buyPrice; + this.sellPrice = sellPrice; + this.stock = stock; + this.hourlySales = hourlySales; + this.totalSales = totalSales; + } + + public int getEffectiveSellPrice() { + if (sellPrice <= 0) return 0; + return sellPrice; + } + + public String getId() { return id; } + public String getDisplayName() { return displayName; } + public Material getMaterial() { return material; } + public int getBuyPrice() { return buyPrice; } + public int getSellPrice() { return sellPrice; } + public int getStock() { return stock; } + public int getHourlySales() { return hourlySales; } + public int getTotalSales() { return totalSales; } + + public void setStock(int s) { this.stock = s; } + public void setHourlySales(int s) { this.hourlySales = s; } + public void setTotalSales(int s) { this.totalSales = s; } + public void addSales(int n) { + this.hourlySales += n; + this.totalSales += n; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopManager.java new file mode 100644 index 0000000..cdcc045 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/ShopManager.java @@ -0,0 +1,2610 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Base64; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.inventory.meta.PotionMeta; + + + + +public class ShopManager implements Listener { + + private final Main plugin; + private final List categories = + new ArrayList<>(); + private final List refundRecords = + new ArrayList<>(); + private final Map activeDiscount = + new HashMap<>(); + private final Map viewCat = + new HashMap<>(); + private final Map viewPage = + new HashMap<>(); + private final Map> shoppingCarts = + new HashMap<>(); + private final Set cartModeEnabled = + new HashSet<>(); + private final java.util.Random rng = + new java.util.Random(); + private final Map> pendingCheckout = + new HashMap<>(); + private final Set couponListening = + new HashSet<>(); + private final Map pendingCouponCode = + new HashMap<>(); + // ===== 物品NBT数据 ===== + private final Map itemNbtData = + new HashMap<>(); + private final Map itemStackCache = + new HashMap<>(); + // 替换字段 + private final Map couponApplyTime = new HashMap<>(); + + + + private static final Material[] SHULKER_COLORS = { + Material.SHULKER_BOX, + Material.WHITE_SHULKER_BOX, + Material.ORANGE_SHULKER_BOX, + Material.MAGENTA_SHULKER_BOX, + Material.LIGHT_BLUE_SHULKER_BOX, + Material.YELLOW_SHULKER_BOX, + Material.LIME_SHULKER_BOX, + Material.PINK_SHULKER_BOX, + Material.GRAY_SHULKER_BOX, + Material.LIGHT_GRAY_SHULKER_BOX, + Material.CYAN_SHULKER_BOX, + Material.PURPLE_SHULKER_BOX, + Material.BLUE_SHULKER_BOX, + Material.BROWN_SHULKER_BOX, + Material.GREEN_SHULKER_BOX, + Material.RED_SHULKER_BOX, + Material.BLACK_SHULKER_BOX + }; + private static final Map EFFECT_MAP = new HashMap<>(); + static { + // ===== 基础效果 ===== + EFFECT_MAP.put("夜视", PotionEffectType.NIGHT_VISION); + EFFECT_MAP.put("隐身", PotionEffectType.INVISIBILITY); + EFFECT_MAP.put("迅捷", PotionEffectType.SPEED); + EFFECT_MAP.put("速度", PotionEffectType.SPEED); + EFFECT_MAP.put("力量", PotionEffectType.STRENGTH); // 1.21+ 改名 + EFFECT_MAP.put("治疗", PotionEffectType.INSTANT_HEALTH); // 1.21+ 改名 + EFFECT_MAP.put("治愈", PotionEffectType.INSTANT_HEALTH); + EFFECT_MAP.put("伤害", PotionEffectType.INSTANT_DAMAGE); // 1.21+ 改名 + EFFECT_MAP.put("剧毒", PotionEffectType.POISON); + EFFECT_MAP.put("中毒", PotionEffectType.POISON); + EFFECT_MAP.put("回复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("再生", PotionEffectType.REGENERATION); + EFFECT_MAP.put("生命恢复", PotionEffectType.REGENERATION); + EFFECT_MAP.put("抗性", PotionEffectType.RESISTANCE); // 1.21+ 改名 + EFFECT_MAP.put("抗火", PotionEffectType.FIRE_RESISTANCE); + EFFECT_MAP.put("水下呼吸", PotionEffectType.WATER_BREATHING); + EFFECT_MAP.put("急迫", PotionEffectType.HASTE); // 1.21+ 改名 + EFFECT_MAP.put("挖掘疲劳", PotionEffectType.MINING_FATIGUE); // 1.21+ 改名 + EFFECT_MAP.put("虚弱", PotionEffectType.WEAKNESS); + EFFECT_MAP.put("失明", PotionEffectType.BLINDNESS); + EFFECT_MAP.put("飘浮", PotionEffectType.LEVITATION); + EFFECT_MAP.put("缓降", PotionEffectType.SLOW_FALLING); // ★ 补充 + EFFECT_MAP.put("迟缓", PotionEffectType.SLOWNESS); + EFFECT_MAP.put("饱和", PotionEffectType.SATURATION); + EFFECT_MAP.put("伤害吸收", PotionEffectType.ABSORPTION); + EFFECT_MAP.put("幸运", PotionEffectType.LUCK); + EFFECT_MAP.put("不祥", PotionEffectType.UNLUCK); + EFFECT_MAP.put("发光", PotionEffectType.GLOWING); + EFFECT_MAP.put("黑暗", PotionEffectType.DARKNESS); + EFFECT_MAP.put("生命加成", PotionEffectType.HEALTH_BOOST); + EFFECT_MAP.put("饥饿", PotionEffectType.HUNGER); + EFFECT_MAP.put("反胃", PotionEffectType.NAUSEA); + EFFECT_MAP.put("海豚关怀", PotionEffectType.DOLPHINS_GRACE); + EFFECT_MAP.put("跳跃", PotionEffectType.JUMP_BOOST); + // ★ 神龟药水是双效果,特殊处理,这里只做名称匹配入口 + EFFECT_MAP.put("神龟", PotionEffectType.RESISTANCE); + } + + public static class CartItem { + String itemId; + String displayName; + Material material; + int unitPrice; + int quantity; + } + + + private static final int PAGE_SIZE = 28; + private static final int[] ITEM_SLOTS = { + 10,11,12,13,14,15,16, + 19,20,21,22,23,24,25, + 28,29,30,31,32,33,34, + 37,38,39,40,41,42,43 + }; + + public ShopManager(Main plugin) { + this.plugin = plugin; + loadCategories(); + startHourlyTask(); + loadItemNbtData(); + startCleanupTask(); + } + // ===== 切换购物车模式 ===== + // ===== 切换购物车模式 ===== + public boolean toggleCartMode(Player p) { + UUID uuid = p.getUniqueId(); + if (cartModeEnabled.contains(uuid)) { + cartModeEnabled.remove(uuid); + p.sendMessage("§7购物车模式已关闭,恢复正常购买"); + return false; + } else { + cartModeEnabled.add(uuid); + p.sendMessage("§a购物车模式已开启,点击商品加入购物车"); + return true; + } + } + + public boolean isCartMode(Player p) { + return cartModeEnabled.contains(p.getUniqueId()); + } + private String itemToBase64(ItemStack item) { + try { + ByteArrayOutputStream os = + new ByteArrayOutputStream(); + BukkitObjectOutputStream bos = + new BukkitObjectOutputStream(os); + bos.writeObject(item); + bos.close(); + return Base64.getEncoder() + .encodeToString(os.toByteArray()); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToItem(String b64) { + try { + byte[] data = + Base64.getDecoder().decode(b64); + ByteArrayInputStream is = + new ByteArrayInputStream(data); + BukkitObjectInputStream bis = + new BukkitObjectInputStream(is); + ItemStack item = + (ItemStack) bis.readObject(); + bis.close(); + return item; + } catch (Exception e) { + return null; + } + } + + private void loadItemNbtData() { + itemNbtData.clear(); + itemStackCache.clear(); + File f = new File(plugin.getDataFolder(), + "shop/item_nbt.properties"); + if (!f.exists()) return; + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + 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 id = + line.substring(0, eq).trim(); + String b64 = + line.substring(eq + 1).trim(); + itemNbtData.put(id, b64); + ItemStack stack = base64ToItem(b64); + if (stack != null) { + itemStackCache.put(id, stack); + } + } + br.close(); + // plugin.getLogger().info("[Shop] NBT数据: " + // + itemStackCache.size() + "条"); + } catch (Exception ignored) {} + } + + private void saveItemNbtData() { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, "item_nbt.properties"); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# 商品完整数据(自动生成)"); + for (Map.Entry e + : itemNbtData.entrySet()) { + pw.println(e.getKey() + "=" + + e.getValue()); + } + pw.flush(); + pw.close(); + } catch (Exception ignored) {} + } + + // ===== 加入/移除购物车 ===== + public boolean addToCart(Player p, ShopItem item) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts + .computeIfAbsent( + uuid, k -> new ArrayList<>()); + // ★ 始终用原价,折扣在结算时统一应用 + int price = item.getBuyPrice(); + for (CartItem ci : cart) { + if (ci.itemId.equals(item.getId())) { + ci.quantity += 1; + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + } + CartItem ci = new CartItem(); + ci.itemId = item.getId(); + ci.displayName = item.getDisplayName(); + ci.material = item.getMaterial(); + ci.unitPrice = price; + ci.quantity = 1; + cart.add(ci); + p.sendMessage("§a+1 §e" + + item.getDisplayName() + + " §7购物车共§f" + + getCartCount(p) + + "件 §6需" + + getCartTotal(p) + "枚"); + return true; + } + + + public int getCartCount(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int c = 0; + for (CartItem ci : cart) c += ci.quantity; + return c; + } + + public int getCartTotal(Player p) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null) return 0; + int t = 0; + for (CartItem ci : cart) + t += ci.unitPrice * ci.quantity; + return t; + } + + public void cartAddOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + cart.get(index).quantity += 1; + } + + public void cartRemoveOne(Player p, int index) { + List cart = + shoppingCarts.get(p.getUniqueId()); + if (cart == null || index < 0 + || index >= cart.size()) return; + CartItem ci = cart.get(index); + ci.quantity -= 1; + if (ci.quantity <= 0) cart.remove(index); + } + + public void clearCart(Player p) { + shoppingCarts.remove(p.getUniqueId()); + } + + // ===== 打开购物车页面 ===== + public void openCart(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§a§l购物车"); + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + for (int i = 0; i < cart.size() && i < 25; i++) { + CartItem ci = cart.get(i); + ItemStack is = new ItemStack(ci.material); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + ci.displayName + + " §7x" + ci.quantity); + im.setLore(java.util.Arrays.asList( + "§7单价: §f" + ci.unitPrice + "枚", + "§7小计: §6" + + (ci.unitPrice * ci.quantity) + + "枚", + "", + "§a左键+1 §c右键-1")); + is.setItemMeta(im); + } + g.setItem(10 + i, is); + } + if (cart.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7购物车为空")); + } + int total = getCartTotal(p); + int bal = plugin.getBonds().getBonds(p.getName()); + g.setItem(49, mkItem(Material.EMERALD, + "§6§l结算: §e" + total + " §6枚债券", + "§7余额: §f" + bal + "枚", + bal >= total ? "§a余额充足" : "§c余额不足", + "", "§a点击结算")); + g.setItem(48, mkItem( + Material.LIME_STAINED_GLASS_PANE, + "§7清空购物车")); + g.setItem(53, mkItem(Material.ARROW, "§7返回商店")); + + p.openInventory(g); + } + + // ===== 购物车点击处理 ===== + public boolean handleCartClick(Player p, int raw, + boolean left, + boolean right) { + if (raw == 53) { openShopMain(p); return true; } + if (raw == 48) { + clearCart(p); + p.sendMessage("§7购物车已清空"); + openCart(p); + return true; + } + if (raw == 49) { + checkoutCartInit(p); + return true; + } + if (raw >= 10 && raw < 35) { + int idx = raw - 10; + List cart = shoppingCarts + .getOrDefault(p.getUniqueId(), + new ArrayList<>()); + if (idx < cart.size()) { + if (right) cartRemoveOne(p, idx); + else cartAddOne(p, idx); + openCart(p); + } + return true; + } + return true; + } + + // ===== 结算入口:判断打包方式 ===== + private void checkoutCartInit(Player p) { + UUID uuid = p.getUniqueId(); + List cart = shoppingCarts.get(uuid); + if (cart == null || cart.isEmpty()) { + p.sendMessage("§c购物车为空"); + return; + } + int total = getCartTotal(p); + int bal = plugin.getBonds() + .getBonds(p.getName()); + if (bal < total) { + p.sendMessage("§c余额不足,需要 §e" + + total + "§c枚"); + return; + } + int totalQty = 0; + for (CartItem ci : cart) totalQty += ci.quantity; + if (cart.size() == 1 && totalQty <= 64) { + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + deductAndGive(p, cart, + (int) Math.round(total * factor), + false, null, "", 0, + ecoPct, couponPct); + return; + } + pendingCheckout.put(uuid, + new ArrayList<>(cart)); + openPackingConfirm(p, total); + } + + + + // ===== 执行扣款+发货 ===== + private boolean deductAndGive(Player p, + List cart, int baseTotal, + boolean useShulker, Material color, + String packNote, int packFee, + int ecoPct, int couponPct) { + + UUID uuid = p.getUniqueId(); + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + String[] names = new String[cart.size()]; + int[] subs = new int[cart.size()]; + for (int i = 0; i < cart.size(); i++) { + CartItem ci = cart.get(i); + names[i] = ci.displayName; + subs[i] = ci.unitPrice * ci.quantity; + } + + if (baseTotal < originalTotal) { + double ratio = + (double) baseTotal / originalTotal; + int sum = 0; + for (int i = 0; i < subs.length; i++) { + if (i == subs.length - 1) { + subs[i] = baseTotal - sum; + } else { + subs[i] = (int) (subs[i] * ratio); + sum += subs[i]; + } + } + } + + boolean ok = plugin.getBonds().checkoutCart( + p.getName(), names, subs); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + if (packFee > 0) { + plugin.getBonds().deductBonds( + p.getName(), packFee, + "shop_packing", "", + "商店系统", "打包费"); + } + + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + if (si != null) { + if (si.getStock() >= 0) + si.setStock(si.getStock() + - ci.quantity); + si.addSales(ci.quantity); + } + } + + int newBal = plugin.getBonds() + .getBonds(p.getName()); + + + // ===== 优惠券码 ===== + String usedCouponCode = + pendingCouponCode.get(uuid); + + // ===== 记录订单 ===== + OrderManager.OrderRecord latestOrder = null; + try { + List orderItems = + new ArrayList<>(); + for (CartItem ci : cart) { + orderItems.add( + new OrderManager.OrderItem( + ci.displayName, + ci.material.name(), + ci.unitPrice, + ci.unitPrice, + ci.quantity)); + } + String dType = "none"; + String cCode = ""; + if (usedCouponCode != null) { + dType = "coupon"; + cCode = usedCouponCode; + } else if (ecoPct > 0) { + dType = "eco"; + } + plugin.getOrderManager().recordOrder( + p, orderItems, originalTotal, + baseTotal + packFee, + originalTotal - baseTotal, + dType, cCode, packFee, + useShulker ? (packFee > 0 + ? "custom" : "default") + : "none", + color != null ? color.name() : ""); + // 获取最新订单 + synchronized (plugin.getOrderManager() + .getOrders()) { + List all = + plugin.getOrderManager() + .getOrders(); + if (!all.isEmpty()) + latestOrder = + all.get(all.size() - 1); + } + // 散装打印小票 + if (!useShulker && latestOrder != null) { + plugin.getOrderManager() + .sendReceiptChat(p, latestOrder); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + // + ex.getMessage()); + } + +// ===== 发货 ===== + if (!useShulker) { + for (CartItem ci : cart) { + ShopItem si = findItemById(ci.itemId); + ItemStack give = (si != null) + ? getShopStack(si, ci.quantity) + : new ItemStack(ci.material, ci.quantity); + HashMap left2 = + p.getInventory().addItem(give); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } else { + List shulkers = + packShulkers(cart, color); + + // ★ 给潜影盒打订单ID标签 + if (latestOrder != null) { + for (ItemStack sh : shulkers) { + ItemMeta shm = sh.getItemMeta(); + if (shm != null) { + List lore = shm.getLore() != null + ? new ArrayList<>(shm.getLore()) + : new ArrayList<>(); + lore.add("§8#" + latestOrder.orderId); + shm.setLore(lore); + sh.setItemMeta(shm); + } + } + } + + // 小票书塞入 + if (!shulkers.isEmpty() + && latestOrder != null) { + ItemStack book = plugin.getOrderManager() + .createReceiptBook(latestOrder); + plugin.getOrderManager() + .addBookToShulker(shulkers.get(0), book); + } + // 发货 + for (ItemStack sh : shulkers) { + HashMap left2 = + p.getInventory().addItem(sh); + for (ItemStack drop : left2.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + } + } + + + // ===== 退款记录 ===== + long now = System.currentTimeMillis(); + if (!useShulker) { + for (CartItem ci : cart) { + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + ci.itemId, ci.displayName, + ci.material, null, + ci.quantity, + ci.unitPrice * ci.quantity, + now)); + } + } else { + String summary = ""; + for (CartItem ci : cart) { + if (!summary.isEmpty()) summary += "+"; + summary += ci.displayName + + "x" + ci.quantity; + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + "cart_" + now, summary, + Material.SHULKER_BOX, color, + 1, baseTotal, now)); + } + + // ===== 核销优惠券 ===== + if (usedCouponCode != null) { + activeDiscount.remove(uuid); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCouponCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCouponCode + + " §7已核销"); + } + + shoppingCarts.remove(uuid); + pendingCheckout.remove(uuid); + return true; + } + + @EventHandler + public void onBlockPlace(org.bukkit.event.block.BlockPlaceEvent e) { + ItemStack item = e.getItemInHand(); + if (item == null) return; + if (!item.getType().name().contains("SHULKER")) return; + markShulkerUsed(item); + } + + // ★ 公共方法:标记潜影盒为已使用 + private void markShulkerUsed(ItemStack item) { + if (item == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null || meta.getLore() == null) return; + for (String line : meta.getLore()) { + if (line.startsWith("§8#")) { + try { + long orderId = Long.parseLong( + line.replace("§8#", "").trim()); + // shulkerUsedOrders.add(orderId); + } catch (NumberFormatException ignored) {} + } + } + } + + @EventHandler + public void onPlayerDrop(org.bukkit.event.player.PlayerDropItemEvent e) { + ItemStack item = e.getItemDrop().getItemStack(); + if (item != null && item.getType().name().contains("SHULKER")) { + markShulkerUsed(item); + } + } + + + // ===== 潠影盒打包(指定颜色)===== + private List packShulkers( + List items, Material color) { + List result = new ArrayList<>(); + List all = new ArrayList<>(); + for (CartItem ci : items) { + int rem = ci.quantity; + while (rem > 0) { + int sz = Math.min(rem, 64); + all.add(new ItemStack(ci.material, sz)); + rem -= sz; + } + } + while (!all.isEmpty()) { + ItemStack shulker = new ItemStack(color); + org.bukkit.inventory.meta.BlockStateMeta meta = + (org.bukkit.inventory.meta.BlockStateMeta) + shulker.getItemMeta(); + if (meta != null) { + org.bukkit.block.BlockState state = + meta.getBlockState(); + if (state instanceof org.bukkit.block.Container) { + org.bukkit.block.Container c = + (org.bukkit.block.Container) state; + List remain = new ArrayList<>(); + for (ItemStack stack : all) { + HashMap left = + c.getInventory().addItem(stack); + for (ItemStack l : left.values()) + remain.add(l); + } + all = remain; + meta.setBlockState(c); + shulker.setItemMeta(meta); + } + } + result.add(shulker); + } + + return result; + } + + // ===== 打包确认GUI ===== + public void openPackingConfirm(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 27, "§6§l选择包装方式"); + g.setItem(10, mkItem(Material.CHEST, + "§a§l不打包", + "§7直接放入背包", + "§7适合少量物品")); + g.setItem(13, mkItem(Material.SHULKER_BOX, + "§6§l默认打包", + "§7随机颜色潜影盒", + "§7加收 §e5 §6枚债券")); + g.setItem(16, mkItem(Material.ENDER_CHEST, + "§b§l自选颜色", + "§7选择你喜欢的潜影盒颜色", + "§7加收 §e5 §6枚债券")); + g.setItem(22, mkItem(Material.PAPER, + "§7消费金额: §e" + baseTotal + "枚")); + + p.openInventory(g); + } + + public boolean handlePackingConfirm( + Player p, int raw) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + + int originalTotal = 0; + for (CartItem ci : cart) + originalTotal += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + + if (raw == 10) { + ecoPct = 2; + } + + double factor = 1.0; + if (ecoPct > 0) + factor *= (100 - ecoPct) / 100.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + originalTotal * factor); + + int totalQty = 0; + for (CartItem ci : cart) + totalQty += ci.quantity; + boolean multiAndBig = (cart.size() > 1 + && originalTotal >= 100); + + if (raw == 10) { + deductAndGive(p, cart, discounted, + false, null, "", 0, + ecoPct, couponPct); + return true; + } + if (raw == 13) { + Material c = Material.SHULKER_BOX; + if (multiAndBig) { + deductAndGive(p, cart, discounted, + true, c, "", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, + true, c, "默认打包", 5, + ecoPct, couponPct); + } + return true; + } + if (raw == 16) { + pendingCheckout.put(uuid, cart); + openColorSelect(p, discounted); + return true; + } + return true; + } + + + // ===== 自选颜色GUI ===== + public void openColorSelect(Player p, int baseTotal) { + Inventory g = Bukkit.createInventory( + null, 54, "§b§l选择潜影盒颜色"); + String[] colorNames = { + "白色","橙色","品红","淡蓝", + "黄色","淡绿","粉色","灰色", + "淡灰","青色","紫色","蓝色", + "棕色","绿色","红色","黑色","原色" + }; + for (int i = 0; i < SHULKER_COLORS.length; i++) { + ItemStack is = new ItemStack(SHULKER_COLORS[i]); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName("§e" + colorNames[i] + + "§6潜影盒"); + im.setLore(java.util.Arrays.asList( + "§7加收 §e5 §6枚债券", + "", + "§a点击选择")); + is.setItemMeta(im); + } + g.setItem(i, is); + } + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + g.setItem(45, mkItem(Material.PAPER, + "§7消费: §e" + baseTotal + + "枚 + §e5§6打包费")); + + p.openInventory(g); + } + + public boolean handleColorSelect( + Player p, int raw) { + if (raw == 49) { + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.get(uuid); + if (cart != null) { + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + openPackingConfirm(p, total); + } + return true; + } + if (raw < 0 || raw >= SHULKER_COLORS.length) + return false; + UUID uuid = p.getUniqueId(); + List cart = + pendingCheckout.remove(uuid); + if (cart == null) return false; + int total = 0; + for (CartItem ci : cart) + total += ci.unitPrice * ci.quantity; + + int couponPct = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int ecoPct = 0; + double factor = 1.0; + if (couponPct > 0) + factor *= (100 - couponPct) / 100.0; + int discounted = (int) Math.round( + total * factor); + + Material selectedColor = SHULKER_COLORS[raw]; + if (selectedColor == Material.SHULKER_BOX) { + deductAndGive(p, cart, discounted, true, + Material.SHULKER_BOX, + "原色打包", 0, + ecoPct, couponPct); + } else { + deductAndGive(p, cart, discounted, true, + selectedColor, "自选颜色", 5, + ecoPct, couponPct); + } + return true; + } + + + + // ===== 加载分类 ===== + + public void loadCategories() { + // plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); // 过滤调试日志 + categories.clear(); + viewCat.clear(); // ★ 清缓存 + viewPage.clear(); + + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + + convertTxtToMd(dir); + + File[] allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + if (allMd == null || allMd.length == 0) { + createDefaultCategories(dir); + allMd = dir.listFiles( + (d, n) -> n.endsWith(".md") + && !n.equals("coupons.md")); + } + if (allMd == null) { + // plugin.getLogger().warning( + // "[Shop] shop目录为空"); + return; + } + + /* plugin.getLogger().info("[Shop] === 开始加载 ==="); + plugin.getLogger().info("[Shop] 扫描到 " + + allMd.length + " 个md文件"); +*/ + for (File f : allMd) { + /* plugin.getLogger().info("[Shop] 读取: " + + f.getName());*/ + ShopCategory cat = parseFile(f); + if (cat != null) { + /* plugin.getLogger().info("[Shop] 分类: " + + cat.getName() + + " 商品数: " + + cat.getItems().size());*/ + if (!cat.getItems().isEmpty()) { + categories.add(cat); + } + } + } + + int totalItems = 0; + for (ShopCategory c : categories) + totalItems += c.getItems().size(); + + plugin.getLogger().info("[Shop] === 加载完成 ===" + + " 分类=" + categories.size() + + " 商品=" + totalItems); + } + + +// ===== txt → md 自动迁移 ===== + + private void convertTxtToMd(File dir) { + File[] txtFiles = dir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("coupons.txt")); + if (txtFiles == null) return; + for (File txt : txtFiles) { + String mdName = txt.getName() + .replace(".txt", ".md"); + File mdFile = new File(dir, mdName); + if (mdFile.exists()) continue; + + try { + List lines = new ArrayList<>(); + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(txt), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) + lines.add(line); + br.close(); + + String catName = txt.getName() + .replace(".txt", ""); + List out = new ArrayList<>(); + out.add("# " + catName); + out.add(""); + out.add("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + out.add("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + + for (String l : lines) { + l = l.trim(); + if (l.isEmpty() || l.startsWith("#")) + continue; + // 旧格式: ID | 名称 | 材质 | ... + // 保持原样转换为 md 表格行 + out.add("| " + l.replace("|", "|") + + " |"); + } + + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(mdFile), + StandardCharsets.UTF_8)); + for (String o : out) + pw.println(o); + pw.flush(); + pw.close(); + + txt.delete(); + // plugin.getLogger().info( + // "[Shop] 已迁移: " + // + txt.getName() + // + " → " + mdName); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 迁移失败: " + // + txt.getName()); + } + } + } + +// ===== 解析 md 文件 ===== + + private ShopCategory parseFile(File f) { + String name = f.getName() + .replace(".md", ""); + ShopCategory cat = + new ShopCategory(name, f.getName()); + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + boolean headerPassed = false; + boolean separatorPassed = false; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + // 空行跳过 + if (line.isEmpty()) continue; + + // 标题行:# 分类名 + if (line.startsWith("#")) { + String t = line.replaceFirst( + "^#+\\s*", "").trim(); + if (!t.isEmpty()) { + // 更新分类名(用文件内标题) + // cat.name 是 final,不改 + } + continue; + } + + // 表格分隔行:| --- | --- | + if (line.contains("---")) { + separatorPassed = true; + continue; + } + + // 表头行:| ID | 品名 | ... + if (line.startsWith("|")) { + if (!separatorPassed) { + headerPassed = true; + continue; + } + // 数据行 + ShopItem item = + parseTableRow(line); + if (item != null) + cat.addItem(item); + } + } + br.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 解析失败: " + f.getName() + // + " 原因: " + e.getMessage()); + return null; + } + return cat; + } + +// ===== 解析表格行 ===== + + private ShopItem parseTableRow(String row) { + // plugin.getLogger().info("[Shop] 行: " + row); // 过滤调试日志 + + String trimmed = row.trim(); + if (trimmed.startsWith("|")) + trimmed = trimmed.substring(1); + if (trimmed.endsWith("|")) + trimmed = trimmed.substring(0, + trimmed.length() - 1); + + String[] p = trimmed.split("\\|"); + // plugin.getLogger().info("[Shop] 列数=" + p.length); // 过滤调试日志 + + if (p.length < 6) { + // plugin.getLogger().warning( + // "[Shop] ✗ 列数不足, 跳过"); + return null; + } + + String rawId = p[0].trim(); + String rawName = p[1].trim(); + String rawMat = p[2].trim().toUpperCase(); + String rawBuy = p[3].trim(); + String rawSell = p[4].trim(); + String rawStock = p[5].trim(); + + // plugin.getLogger().info("[Shop] ID=" + // + rawId + " 名=" + rawName + // + " 材质=" + rawMat + // + " 购=" + rawBuy + " 售=" + rawSell + // + " 库=" + rawStock); // 过滤调试日志 + + // 检查材质是否存在 + Material mat = Material.matchMaterial(rawMat); + if (mat == null) { + // plugin.getLogger().warning( + // "[Shop] ✗ 无效材质: " + rawMat + // + " (行: " + row + ")"); + return null; + } + + try { + ShopItem item = new ShopItem( + rawId, rawName, mat, + Integer.parseInt(rawBuy), + Integer.parseInt(rawSell), + Integer.parseInt(rawStock), + p.length > 6 + ? Integer.parseInt( + p[6].trim()) : 0, + p.length > 7 + ? Integer.parseInt( + p[7].trim()) : 0); + // plugin.getLogger().info("[Shop] ✓ 解析成功: " + rawId); // 过滤调试日志 + return item; + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] ✗ 数字解析失败: " + // + e.getMessage()); + return null; + } + } + + +// ===== 保存为 md ===== + + public void saveCategory(ShopCategory cat) { + File dir = new File( + plugin.getDataFolder(), "shop"); + if (!dir.exists()) dir.mkdirs(); + File f = new File(dir, cat.getFileName()); + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.println("# " + cat.getName()); + pw.println(""); + pw.println("| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |"); + pw.println("| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |"); + for (ShopItem item : cat.getItems()) { + pw.println("| " + + item.getId() + + " | " + item.getDisplayName() + + " | " + item.getMaterial().name() + + " | " + item.getBuyPrice() + + " | " + item.getSellPrice() + + " | " + item.getStock() + + " | " + item.getHourlySales() + + " | " + item.getTotalSales() + + " |"); + } + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 保存失败: " + // + cat.getFileName()); + } + } + +// ===== 创建示例分类 ===== + + private void createDefaultCategories(File dir) { + writeMd(dir, "建筑材料.md", + "# 建筑材料\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| STONE | 石头 | STONE" + + " | 10 | -1 | -1 | 0 | 0 |\n" + + "| COBBLESTONE | 圆石 | COBBLESTONE" + + " | 5 | -1 | -1 | 0 | 0 |\n" + + "| OAK_PLANKS | 橡木木板 | OAK_PLANKS" + + " | 8 | 3 | -1 | 0 | 0 |\n" + + "| BRICKS | 砖块 | BRICKS" + + " | 12 | -1 | -1 | 0 | 0 |\n" + + "| GLASS | 玻璃 | GLASS" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK" + + " | 20 | 8 | -1 | 0 | 0 |\n"); + + writeMd(dir, "装备工具.md", + "# 装备工具\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| IRON_PICKAXE | 铁镐 | IRON_PICKAXE" + + " | 50 | 20 | -1 | 0 | 0 |\n" + + "| DIAMOND_PICKAXE | 钻石镐 | DIAMOND_PICKAXE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| IRON_SWORD | 铁剑 | IRON_SWORD" + + " | 40 | 16 | -1 | 0 | 0 |\n" + + "| DIAMOND_SWORD | 钻石剑 | DIAMOND_SWORD" + + " | 400 | 160 | -1 | 0 | 0 |\n" + + "| ELYTRA | 鞘翅 | ELYTRA" + + " | 2000 | 800 | -1 | 0 | 0 |\n"); + + writeMd(dir, "食物补给.md", + "# 食物补给\n" + + "\n" + + "| ID | 品名 | 材质" + + " | 购入价 | 售出价" + + " | 库存" + + " | 本小时销量 | 总销量 |\n" + + "| --- | --- | ---" + + " | --- | --- | ---" + + " | --- | --- |\n" + + "| COOKED_BEEF | 熟牛排 | COOKED_BEEF" + + " | 15 | 6 | -1 | 0 | 0 |\n" + + "| GOLDEN_APPLE | 金苹果 | GOLDEN_APPLE" + + " | 500 | 200 | -1 | 0 | 0 |\n" + + "| BREAD | 面包 | BREAD" + + " | 8 | 3 | -1 | 0 | 0 |\n"); + + // plugin.getLogger().info( + // "[Shop] 已创建3个示例分类(md)"); + } + + private void writeMd(File dir, String name, + String content) { + File f = new File(dir, name); + if (f.exists()) return; + try { + PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8)); + pw.print(content); + pw.flush(); + pw.close(); + } catch (Exception e) { + // plugin.getLogger().warning( + // "[Shop] 创建失败: " + name); + } + } + + /** + * 从商品名称+材质智能推断完整物品 + * + * "夜视药水" + POTION + * → PotionEffect(NIGHT_VISION, 600, 0) + * + * "喷溅型治疗药水" + SPLASH_POTION + * → PotionEffect(HEALING, 600, 0) + * + * "长效夜视之箭" + TIPPED_ARROW + * → PotionEffect(NIGHT_VISION, 1800, 0) + * + * "强效力量喷溅药水" + SPLASH_POTION + * → PotionEffect(STRENGTH, 600, 1) + */ + private ItemStack inferFromName( + String name, Material mat) { + // 只处理药水和箭 + if (mat != Material.POTION + && mat != Material.SPLASH_POTION + && mat != Material.LINGERING_POTION + && mat != Material.TIPPED_ARROW) { + return null; + } + + // 1. 找效果类型 + PotionEffectType effectType = null; + for (Map.Entry e + : EFFECT_MAP.entrySet()) { + if (name.contains(e.getKey())) { + effectType = e.getValue(); + break; + } + } + if (effectType == null) return null; + + // 2. 找时长 + int duration = 600; + if (name.contains("长效") || name.contains("长时") + || name.contains("持久") || name.contains("长命")) { + duration = 1800; + } +// ★ 瞬间效果药水:duration = 1 + if (name.contains("治疗") || name.contains("治愈") + || name.contains("伤害")) { + duration = 1; + } + + // 3. 找等级 + int amplifier = 0; + if (name.contains("强效") + || name.contains("强化") + || name.contains("加强")) { + amplifier = 1; + } + + // 4. 构造完整物品 + try { + ItemStack item = new ItemStack(mat); + PotionMeta meta = + (PotionMeta) item.getItemMeta(); + + // ★ 神龟药水:双效果 + if (name.contains("神龟")) { + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.RESISTANCE, + duration, amplifier + 2), + true); + meta.addCustomEffect( + new PotionEffect( + PotionEffectType.SLOWNESS, + duration, amplifier + 3), + true); + } else { + // 普通药水:单效果 + meta.addCustomEffect( + new PotionEffect(effectType, + duration, amplifier), + true); + } + + item.setItemMeta(meta); + return item; + } catch (Exception e) { + return null; + } + } + + public void saveAll() { + for (ShopCategory cat : categories) + saveCategory(cat); + } + private ItemStack getShopStack( + ShopItem item, int amount) { + // 优先级1: 手动捕获的NBT数据 + ItemStack cached = + itemStackCache.get(item.getId()); + if (cached != null) { + ItemStack copy = cached.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级2: 根据名称智能推断 + ItemStack inferred = inferFromName( + item.getDisplayName(), + item.getMaterial()); + if (inferred != null) { + ItemStack copy = inferred.clone(); + copy.setAmount(amount); + return copy; + } + // 优先级3: 纯材质(默认) + return new ItemStack( + item.getMaterial(), amount); + } + + // ===== 购买 ===== + + public boolean buyItem(Player p, ShopItem item, + int amount) { + // plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); + // plugin.getLogger().info("buyItem被调用"); + if (item == null) return false; + if (item.getStock() >= 0 + && item.getStock() < amount) { + p.sendMessage("§c库存不足,剩余: " + + item.getStock()); + return false; + } + if (p.getInventory().firstEmpty() == -1 + && !p.getInventory().contains( + item.getMaterial(), 1)) { + p.sendMessage("§c背包已满"); + return false; + } + + int unit = item.getBuyPrice(); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + int total = Math.max(0, + unit * amount * (100 - disc) / 100); + + int balance = plugin.getBonds() + .getBonds(p.getName()); + if (balance < total) { + p.sendMessage("§c债券不足,需要 §e" + + total + "§c枚,当前 §e" + + balance + "§c枚"); + return false; + } + + boolean ok = plugin.getBonds().deductBonds( + p.getName(), total, + "shop_buy", item.getId(), + "商店系统", "购买" + + item.getDisplayName()); + if (!ok) { + p.sendMessage("§c扣款失败"); + return false; + } + + ItemStack give = getShopStack(item, amount); + HashMap left = + p.getInventory().addItem(give); + for (ItemStack drop : left.values()) + p.getWorld().dropItemNaturally( + p.getLocation(), drop); + + if (item.getStock() >= 0) + item.setStock( + item.getStock() - amount); + item.addSales(amount); + + mergeOrAddRefund(p, item, amount, total); + Long applyTime = couponApplyTime.get(p.getUniqueId()); + if (applyTime != null + && System.currentTimeMillis() - applyTime > 300000L) { + // 超过5分钟,自动失效 + pendingCouponCode.remove(p.getUniqueId()); + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + p.sendMessage("§7优惠券已过期,请重新输入"); + disc = 0; // 重置折扣 + } + + + String discMsg = disc > 0 + ? " §7(优惠" + disc + "%)" : ""; + p.sendMessage("§a购买成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a-" + total + "枚债券" + + discMsg); + if (disc > 0) + activeDiscount.remove( + p.getUniqueId()); + // ★ 直购成功后核销优惠券 + String usedCode = pendingCouponCode + .remove(p.getUniqueId()); + if (usedCode != null) { + activeDiscount.remove(p.getUniqueId()); + couponApplyTime.remove(p.getUniqueId()); + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + m.invoke(sdf1, usedCode); + } + } catch (Exception ignored) {} + p.sendMessage("§7优惠券 §e" + usedCode + + " §7已核销"); + couponApplyTime.remove(p.getUniqueId()); + } + // ★ 记录订单(包裹try-catch,失败不影响购买) + try { + String usedCouponCode = + pendingCouponCode.get(p.getUniqueId()); + List orderItems = + new ArrayList<>(); + orderItems.add(new OrderManager.OrderItem( + item.getDisplayName(), + item.getMaterial().name(), + item.getBuyPrice(), + item.getBuyPrice(), amount)); + plugin.getOrderManager().recordOrder( + p, orderItems, + item.getBuyPrice() * amount, total, + item.getBuyPrice() * amount - total, + usedCouponCode != null ? "coupon" : "none", + usedCouponCode != null ? usedCouponCode : "", + 0, "none", ""); + // 打印小票 + List all = + plugin.getOrderManager().getOrders(); + if (!all.isEmpty()) { + plugin.getOrderManager().sendReceiptChat( + p, all.get(all.size() - 1)); + } + } catch (Exception ex) { + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + ex.getMessage()); + } + + return true; + } + + + public void handleRefundItemClick(Player p, int raw) { + if (raw < 10 || raw >= 45) return; + int target = raw - 10; + int cnt = 0; + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { it.remove(); continue; } + if (cnt == target) { + requestRefund(p, r); + return; + } + cnt++; + } + } + } + + + // ===== 出售 ===== + + public boolean sellItem(Player p, ShopItem item, + int amount) { + if (item == null) return false; + + // ★ 先判断售价,禁止出售的不收物品 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return false; + } + + if (!p.getInventory().containsAtLeast( + getShopStack(item, 1), amount)) { + p.sendMessage("§c你没有足够的 §e" + + item.getDisplayName()); + return false; + } + + int total = item.getEffectiveSellPrice() * amount; + + p.getInventory().removeItem( + getShopStack(item, amount)); + plugin.getBonds().addBonds( + p.getName(), total, "shop_sell", + item.getId(), "商店系统", + "出售" + item.getDisplayName()); + if (item.getStock() >= 0) + item.setStock(item.getStock() + amount); + p.sendMessage("§a出售成功: §e" + + item.getDisplayName() + + " x" + amount + + " §a+" + total + "枚债券"); + return true; + } + + // ===== 退款合并:30秒内同商品合并为一个订单 ===== + private boolean mergeOrAddRefund(Player p, ShopItem item, + int amount, int total) { + long now = System.currentTimeMillis(); + synchronized (refundRecords) { + for (int i = refundRecords.size() - 1; i >= 0; i--) { + RefundRecord r = refundRecords.get(i); + boolean same = r.playerUUID.equals(p.getUniqueId()) + && r.itemId.equals(item.getId()) + && (now - r.timestamp) < 30000L; + if (same) { + // final 字段不能改,只能删旧建新 + int newAmount = r.amount + amount; + int newTotal = r.totalPaid + total; + refundRecords.remove(i); + refundRecords.add(new RefundRecord( + r.playerUUID, r.playerName, + r.itemId, r.itemName, + r.material, newAmount, + newTotal, now)); + return true; + } + } + } + refundRecords.add(new RefundRecord( + p.getUniqueId(), p.getName(), + item.getId(), item.getDisplayName(), + item.getMaterial(), amount, total, now)); + return false; + } + + + + // ===== 退款 ===== + + public boolean requestRefund(Player p, RefundRecord rec) { + if (rec == null) return false; + if (!rec.playerUUID.equals(p.getUniqueId())) { + p.sendMessage("§c这不是你的购买记录"); + return false; + } + if (rec.isExpired()) { + p.sendMessage("§c已超过5分钟退款期限"); + synchronized (refundRecords) { + refundRecords.remove(rec); + } + return false; + } + + // 检测物品/潜影盒是否还在 + synchronized (refundRecords) { + if (rec.shulkerMaterial != null) { + int boxCount = countShulkers(p, rec.shulkerMaterial); + if (boxCount < 1) { + p.sendMessage("§c潜影盒已移出背包,无法退款"); + refundRecords.remove(rec); + return false; + } + } else { + int have = countDirect(p, rec.material); + if (have < rec.amount) { + p.sendMessage("§c物品已转移或消耗,无法退款"); + refundRecords.remove(rec); + return false; + } + } + } + + // 退款:扣物品/潜影盒 + if (rec.shulkerMaterial != null) { + // 盒装:扣一个潜影盒 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == rec.shulkerMaterial) { + p.getInventory().clear(i); + break; + } + } + } else { + // 普通:扣商品 + p.getInventory().removeItem( + new ItemStack(rec.material, rec.amount)); + } + + // 退款:加债券 + plugin.getBonds().addBonds( + p.getName(), rec.totalPaid, + "refund", rec.itemId, + "商店系统", "退款" + rec.itemName); + + // 恢复库存 + ShopItem si = findItemById(rec.itemId); + if (si != null && si.getStock() >= 0) + si.setStock(si.getStock() + rec.amount); + + synchronized (refundRecords) { + refundRecords.remove(rec); + } + + p.sendMessage("§a退款成功: §e" + rec.itemName + + " §a+" + rec.totalPaid + "枚债券"); + return true; + } + + + + + public void setPlayerDiscount(Player p, + int pct) { + activeDiscount.put( + p.getUniqueId(), pct); + p.sendMessage("§a已设置 " + pct + + "% 折扣,下次购买生效"); + } + + // ===== 管理员操作 ===== + + public boolean addItemToCategory( + String catName, String id, + String displayName, Material mat, + int buyPrice, int sellPrice, + int stock) { + ShopCategory cat = + findCategory(catName); + if (cat == null) return false; + if (cat.getItem(id) != null) return false; + cat.addItem(new ShopItem(id, displayName, + mat, buyPrice, sellPrice, + stock, 0, 0)); + saveCategory(cat); + return true; + } + + public boolean removeItem(String id) { + for (ShopCategory cat : categories) { + ShopItem item = cat.getItem(id); + if (item != null) { + cat.getItems().remove(item); + saveCategory(cat); + return true; + } + } + return false; + } + + public boolean setStock(String id, int stock) { + ShopItem item = findItemById(id); + if (item == null) return false; + item.setStock(stock); + for (ShopCategory cat : categories) { + if (cat.getItem(id) != null) { + saveCategory(cat); + return true; + } + } + return false; + } + + public ShopCategory findCategory(String n) { + for (ShopCategory c : categories) + if (c.getName().equals(n)) return c; + return null; + } + + public ShopItem findItemById(String id) { + for (ShopCategory c : categories) { + ShopItem item = c.getItem(id); + if (item != null) return item; + } + return null; + } + + public List getCategories() { + return categories; + } + + // ===== 命令处理 ===== + + public boolean handleCommand( + CommandSender s, String[] a) { + if (a.length <= 1) { + if (s instanceof Player) { + openShopMain((Player) s); + } else { + s.sendMessage("§c仅玩家可使用"); + } + return true; + } + + String sub = a[1].toLowerCase(); + + if (sub.equals("reload")) { + loadCategories(); + s.sendMessage("§a商店已重载,共 " + + categories.size() + + " 个分类"); + return true; + } + + if (sub.equals("add")) { + if (a.length < 7) { + s.sendMessage( + "§e用法: shop add <分类>" + + " <名称> <材质>" + + " <购入价>" + + " [售出价-1自动]" + + " [库存-1无限]"); + return true; + } + Material mat; + try { + mat = Material.valueOf( + a[5].toUpperCase()); + } catch (Exception e) { + s.sendMessage( + "§c无效材质: " + a[5]); + return true; + } + int price = Integer.parseInt(a[6]); + int sell = a.length > 7 + ? Integer.parseInt(a[7]) : -1; + int stock = a.length > 8 + ? Integer.parseInt(a[8]) : -1; + if (addItemToCategory(a[2], a[3], + a[4], mat, price, sell, stock)) { + s.sendMessage("§a已添加: " + + a[4] + " → " + a[2]); + } else { + s.sendMessage("§c添加失败" + + "(分类不存在或ID重复)"); + } + return true; + } + + if (sub.equals("remove")) { + if (a.length < 3) { + s.sendMessage( + "§e用法: shop remove "); + return true; + } + if (removeItem(a[2])) { + s.sendMessage( + "§a已移除: " + a[2]); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("set")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop set " + + " <库存数(-1无限)>"); + return true; + } + int stock = Integer.parseInt(a[3]); + if (setStock(a[2], stock)) { + s.sendMessage("§a已设置 " + + a[2] + " 库存为 " + + stock); + } else { + s.sendMessage( + "§c未找到: " + a[2]); + } + return true; + } + + if (sub.equals("pro")) { + if (a.length < 4) { + s.sendMessage( + "§e用法: shop pro" + + " <玩家名> <折扣%>"); + return true; + } + Player t = + Bukkit.getPlayerExact(a[2]); + if (t == null) { + s.sendMessage("§c玩家不在线"); + return true; + } + int pct = Integer.parseInt(a[3]); + setPlayerDiscount(t, pct); + s.sendMessage("§a已对 " + a[2] + + " 设置 " + pct + "% 折扣"); + return true; + } + + + s.sendMessage("§e商店子命令:" + + " reload / add / remove" + + " / set / pro "); + return true; + } + @EventHandler(priority = EventPriority.LOW) + public void onCouponChat( + org.bukkit.event.player.AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + if (!couponListening.contains(uuid)) return; + e.setCancelled(true); + couponListening.remove(uuid); + String code = e.getMessage().trim(); + if (code.isEmpty()) { + p.sendMessage("§c已取消输入"); + return; + } + // ★ 预处理:标准化输入 + String normalized = normalizeCouponInput(code); + if (normalized.isEmpty()) { + p.sendMessage("§c优惠券码无效(输入为空或仅含特殊字符)"); + couponListening.remove(uuid); + return; + } + + p.sendMessage("§7正在验证优惠券: §e" + code); + + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "validateCoupon", + String.class); + Object result = m.invoke(sdf1, normalized); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception ex) { + p.sendMessage("§c验证异常: " + + ex.getMessage()); + return; + } + if (discount <= 0) { + p.sendMessage("§c优惠券无效或已使用"); + return; + } + int offPct = 100 - discount; + int current = activeDiscount + .getOrDefault(uuid, 0); + if (offPct > current) { + pendingCouponCode.put(uuid, normalized); + activeDiscount.put(uuid, offPct); + couponApplyTime.put(uuid, System.currentTimeMillis()); + String zhe; + if (discount % 10 == 0) + zhe = (discount / 10) + "折"; + else + zhe = (discount / 10.0) + "折"; + p.sendMessage("§6§l[优惠券] §a验证通过!"); + p.sendMessage("§7折扣: §e" + zhe + + " §7(减" + offPct + "%)"); + p.sendMessage("§7结算时自动核销"); + final UUID u = uuid; + Bukkit.getScheduler().runTask(plugin, () -> { + Player pl = Bukkit.getPlayer(u); + if (pl != null && pl.isOnline()) { + openShopMain(pl); + } + }); + } else { + String curZhe; + if ((100 - current) % 10 == 0) + curZhe = ((100 - current) / 10) + "折"; + else + curZhe = ((100 - current) / 10.0) + "折"; + p.sendMessage("§c当前已有更优折扣: " + + curZhe + ",本次不生效"); + } + } + + /** + * 优惠券输入标准化 + * 全角→半角、去分隔符、转小写 + * "AAA-BBB-CCCC" → "aaaabbbbcccc" + * "AAA-BBB" → "aaabbb" + * "aaa_bbb_cccc" → "aaabbbcccc" + */ + private String normalizeCouponInput(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // 全角数字→半角 + if (c >= '\uFF10' && c <= '\uFF19') { + sb.append((char)(c - 0xFF10 + '0')); + } + // 全角字母→半角 + else if (c >= '\uFF21' && c <= '\uFF3A') { + sb.append((char)(c - 0xFF21 + 'A')); + } + else if (c >= '\uFF41' && c <= '\uFF5A') { + sb.append((char)(c - 0xFF41 + 'a')); + } + // ★ 保留中文字符(Unicode CJK统一汉字范围) + else if ((c >= '\u4E00' && c <= '\u9FFF') + || (c >= '\u3400' && c <= '\u4DBF')) { + sb.append(c); + } + // 保留半角字母和数字 + else if ((c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')) { + sb.append(c); + } + // 分隔符(- _ 空格等)丢弃 + } + return sb.toString().toLowerCase(); + } + + + + + + private String formatZhe(int discount) { + if (discount % 10 == 0) { + return (discount / 10) + "折"; + } + return (discount / 10.0) + "折"; + } + + public boolean redeemCoupon(Player p, String code) { + int discount = -1; + try { + Plugin sdf1 = Bukkit.getPluginManager() + .getPlugin("SDF1"); + if (sdf1 != null && sdf1.isEnabled()) { + java.lang.reflect.Method m = + sdf1.getClass().getMethod( + "redeemCoupon", + String.class); + Object result = m.invoke(sdf1, code); + discount = (result != null) + ? (Integer) result : -1; + } + } catch (Exception e) { + return false; + } + if (discount <= 0) return false; + int offPct = 100 - discount; + int cur = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (offPct > cur) { + activeDiscount.put(p.getUniqueId(), offPct); + } + return true; + } + + /** + * 商店主界面点击"优惠券"按钮后调用 + * 开启聊天监听,等待玩家输入优惠券码 + */ + public void openCouponInput(Player p) { + UUID uuid = p.getUniqueId(); + // 先取消已有的待核销优惠券 + pendingCouponCode.remove(uuid); + couponListening.add(uuid); + // 关闭UI + Bukkit.getScheduler().runTask(plugin, () -> { + if (p.isOnline()) p.closeInventory(); + }); + p.sendMessage("§6§l[优惠券] §e请在聊天栏输入优惠券码"); + p.sendMessage("§715秒内输入,过期自动取消"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (couponListening.remove(uuid)) { + Player pl = Bukkit.getPlayer(uuid); + if (pl != null && pl.isOnline()) { + pl.sendMessage("§c优惠券输入已超时取消"); + } + } + }, 300L); + } + + + // ===== GUI: 主界面 ===== + + public void openShopMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l商店"); + int si = 0; + for (ShopCategory cat : categories) { + if (si >= ITEM_SLOTS.length) break; + ItemStack is = new ItemStack( + Material.CHEST); + ItemMeta im = is.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l" + cat.getName()); + im.setLore(Arrays.asList( + "§7商品: §f" + + cat.getItems().size() + + " 种", + "", + "§a点击进入")); + is.setItemMeta(im); + } + g.setItem(ITEM_SLOTS[si], is); + si++; + } + // ★ 改为跳转订单中心 + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看订单记录、申请退款", + "", "§a点击进入")); + + // 在 openShopMain 方法中,原来的 slot 49/53 附近加: + + g.setItem(48, mkItem( + isCartMode(p) + ? Material.LIME_WOOL + : Material.GRAY_WOOL, + isCartMode(p) + ? "§a§l购物车模式 §7[开]" + : "§7§l购物车模式 §7[关]", + isCartMode(p) + ? "§a当前: 点击商品加入购物车" + : "§7当前: 直接购买", + "§7点击切换")); + g.setItem(50, mkItem(Material.CHEST, + "§6§l购物车 §7[" + getCartCount(p) + "件]", + "§7总价: §6" + getCartTotal(p) + "枚", + "", + "§a点击打开购物车")); + + // 在现有按钮之后,p.openInventory(g) 之前加: + g.setItem(49, mkItem(Material.CLOCK, + "§6§l订单中心", + "§7查看购物订单记录", + "", "§a点击进入")); + + // 优惠券按钮 - slot 47 + String discStatus = "§7"; + int curDisc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (curDisc > 0) { + discStatus = "§a当前: " + formatZhe(100 - curDisc); + } + g.setItem(47, mkItem(Material.NAME_TAG, + "§6§l优惠券", + discStatus, + "", + "§a点击输入优惠券码", + "§7一次性使用,核销后失效")); + + g.setItem(53, mkItem(Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + // ===== GUI: 分类 ===== + public void openCategory(Player p, + ShopCategory cat, + int page) { + // ★ 每次重新获取最新对象 ★ + ShopCategory fresh = null; + for (ShopCategory c : categories) { + if (c.getName().equals(cat.getName())) { + fresh = c; + break; + } + } + if (fresh == null) { + openShopMain(p); + return; + } + cat = fresh; + viewCat.put(p.getUniqueId(), cat); + + int total = (int) Math.ceil( + (double) cat.getItems().size() + / PAGE_SIZE); + if (total < 1) total = 1; + if (page < 0) page = 0; + if (page >= total) page = total - 1; + viewPage.put(p.getUniqueId(), page); + + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l" + cat.getName()); + + + int start = page * PAGE_SIZE; + int end = Math.min(start + PAGE_SIZE, + cat.getItems().size()); + + for (int i = start; i < end; i++) { + int idx = i - start; + if (idx >= ITEM_SLOTS.length) break; + + // ★ item 保留原名,代表 ShopItem 数据 + ShopItem item = cat.getItems().get(i); + + // ★ is 是带完整元数据的 ItemStack(用于GUI图标) + ItemStack is = getShopStack(item, 1); + ItemMeta im = is.getItemMeta(); + + if (im != null) { + im.setDisplayName("§e§l" + + item.getDisplayName()); + List lore = new ArrayList<>(); + lore.add("§7购入: §a" + + item.getBuyPrice() + + "§7枚债券"); + lore.add("§7售出: §e" + + item.getEffectiveSellPrice() + + "§7枚债券"); + if (item.getStock() >= 0) + lore.add("§7库存: §f" + + item.getStock()); + else + lore.add("§7库存: §a无限"); + lore.add(""); + int disc = activeDiscount + .getOrDefault(p.getUniqueId(), 0); + if (disc > 0) { + int dis = item.getBuyPrice() + * (100 - disc) / 100; + lore.add("§7优惠价: §6" + + dis + "枚" + + " §7(-" + disc + "%)"); + } + lore.add("§a左键买1 §6右键卖1"); + lore.add("§aShift+左键买64" + + " §6Shift+右键卖64"); + im.setLore(lore); + is.setItemMeta(im); + } + + // ★ 用 is(ItemStack)放入GUI格子 + g.setItem(ITEM_SLOTS[idx], is); + } + + + g.setItem(4, mkItem(Material.BOOK, + "§e§l" + cat.getName(), + "§7共 " + cat.getItems().size() + + " 种商品")); + g.setItem(48, page > 0 + ? mkItem(Material.ARROW, + "§7上一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(49, mkItem(Material.PAPER, + "§e" + (page + 1) + "/" + total)); + g.setItem(50, page < total - 1 + ? mkItem(Material.ARROW, + "§7下一页") + : mkItem(Material + .BLACK_STAINED_GLASS_PANE, " ")); + g.setItem(53, mkItem(Material.ARROW, + "§7返回商店")); + + p.openInventory(g); + } + + // ===== GUI: 退款 ===== + + public void openRefundPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l退款中心"); + + // 位移检测:比对上次记录的背包内容 + validateRefunds(p); + + // 过期清理 + synchronized (refundRecords) { + refundRecords.removeIf(RefundRecord::isExpired); + } + + int idx = 0; + synchronized (refundRecords) { + for (RefundRecord r : refundRecords) { + if (idx >= 45) break; + if (!r.playerUUID.equals(p.getUniqueId())) continue; + if (r.isExpired()) continue; + + // 盒装用潜影盒图标,普通用商品图标 + Material iconMat = (r.shulkerMaterial != null) + ? r.shulkerMaterial : r.material; + + ItemStack is = new ItemStack(iconMat); + ItemMeta im = is.getItemMeta(); + if (im != null) { + int secs = r.getSecondsLeft(); + im.setDisplayName("§e" + r.itemName + + (r.shulkerMaterial != null + ? " §7[盒装]" : " x" + r.amount)); + + List lore = new ArrayList<>(); + lore.add("§7订单: §f#" + r.timestamp); + lore.add("§7支付: §a" + r.totalPaid + + "§7枚债券"); + + if (r.shulkerMaterial != null) { + int boxCount = countShulkers(p, + r.shulkerMaterial); + lore.add("§7潜影盒: §f" + + (boxCount >= 1 + ? "§a在背包中" + : "§c已移出")); + } else { + int have = countDirect(p, r.material); + lore.add("§7物品: §f" + have + + "/" + r.amount); + } + + lore.add("§7剩余: §e" + secs + "秒"); + lore.add(""); + lore.add("§c点击退款"); + im.setLore(lore); + } + is.setItemMeta(im); + g.setItem(10 + idx, is); + idx++; + } + } + + if (idx == 0) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无可退款记录")); + } + + g.setItem(49, mkItem(Material.ARROW, "§7返回")); + + p.openInventory(g); + } + // 统计背包中某物品数量 + private int countInInventory(Player p, Material mat) { + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == mat) { + count += is.getAmount(); + } + } + // 潜影盒也要检查里面 + for (ItemStack is : p.getInventory().getContents()) { + if (is != null + && is.getType().name() + .endsWith("_SHULKER_BOX")) { + count += is.getAmount(); + } + } + return count; + } + + // 只统计背包中直接可见的数量(不含盒内) + // 新代码:增加按完整物品匹配的方法 + private int countDirect(Player p, Material mat) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.getType() == mat) + count += is.getAmount(); + } + return count; + } + + private int countDirectByStack(Player p, + ItemStack target) { + int count = 0; + for (ItemStack is : + p.getInventory().getContents()) { + if (is != null && is.isSimilar(target)) + count += is.getAmount(); + } + return count; + } + // 统计背包中潜影盒数量 + private int countShulkers(Player p, Material shulker) { + if (shulker == null) return Integer.MAX_VALUE; + int count = 0; + for (ItemStack is : p.getInventory().getContents()) { + if (is != null && is.getType() == shulker) + count += is.getAmount(); + } + return count; + } + + // 退款检测:位移 OR 消耗 → 不能退 + private void validateRefunds(Player p) { + synchronized (refundRecords) { + Iterator it = + refundRecords.iterator(); + while (it.hasNext()) { + RefundRecord r = it.next(); + if (!r.playerUUID.equals(p.getUniqueId())) + continue; + if (r.isExpired()) { + it.remove(); + continue; + } + + if (r.shulkerMaterial != null) { + // 盒装:检测潜影盒是否还在背包 + int now = countShulkers( + p, r.shulkerMaterial); + if (now < 1) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 潜影盒已移出背包,无法退款"); + } + } else { + // 普通:检测商品是否还在背包 + int now = countDirect(p, r.material); + if (now < r.amount) { + it.remove(); + p.sendMessage("§c[退款] " + r.itemName + + " 物品已被使用或移出,无法退款"); + } + } + } + } + } + + + public int getPlayerPage(java.util.UUID uuid) { + return viewPage.getOrDefault(uuid, 0); + } + + public ShopCategory getPlayerCategory(java.util.UUID uuid) { + return viewCat.get(uuid); + } + + // ===== 供GUIManager调用:商店主界面点击 ===== + public boolean handleMainShopClick(Player p, int raw) { + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true; } + + if (raw == 48) { + toggleCartMode(p); + openShopMain(p); + return true; + } + if (raw == 50) { + openCart(p); + return true; + } + if (raw == 53) { + plugin.getGui().openMain(p); + return true; + } + + // ★ 新增:优惠券按钮 + if (raw == 47) { + openCouponInput(p); + return true; + } + if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true;} + int idx = slotToIndex(raw); + if (idx < 0 || idx >= categories.size()) + return true; + openCategory(p, categories.get(idx), 0); + return true; + } + + // ===== 供GUIManager调用:退款中心点击 ===== + public boolean handleRefundClick(Player p, int raw) { + if (raw == 49) { openShopMain(p); return true; } + return true; + } + + // ===== 供GUIManager调用:分类页点击 ===== + public boolean handleCategoryClick(Player p, String title, + int raw, boolean left, + boolean shift) { + // plugin.getLogger().info("[Shop] ★ handleCategoryClick raw=" + raw); + + ShopManager sm = this; + ShopCategory match = null; + for (ShopCategory c : categories) { + if (title.equals("§6§l" + c.getName())) { + match = c; + break; + } + } + if (match == null) { + // plugin.getLogger().info("[Shop] ★ 匹配失败, title=[" + title + "]"); + return false; + } + // plugin.getLogger().info("[Shop] ★ 匹配到: " + match.getName()); + + viewCat.put(p.getUniqueId(), match); + + if (raw == 53) { openShopMain(p); return true; } + int pg = getPlayerPage(p.getUniqueId()); + if (raw == 48) { openCategory(p, match, pg - 1); return true; } + if (raw == 50) { openCategory(p, match, pg + 1); return true; } + + int itemIdx = slotToIndex(raw); + if (itemIdx < 0) { + // plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); + return true; + } + int actual = itemIdx + pg * PAGE_SIZE; + if (actual < 0 || actual >= match.getItems().size()) { + // plugin.getLogger().info("[Shop] ★ 超出范围 actual=" + actual + " size=" + match.getItems().size()); + return true; + } + ShopItem si = match.getItems().get(actual); + // plugin.getLogger().info("[Shop] ★ 准备购买: " + si.getId() + " left=" + left); + + int amount = shift ? 64 : 1; + if (left) { + if (shift && isCartMode(p)) { addToCart(p, si); } + else if (shift) { buyItem(p, si, 64); } + else if (isCartMode(p)) { addToCart(p, si); } + else { buyItem(p, si, 1); } + } else { + if (shift) { sellAllOf(p, si); } + else { sellItem(p, si, 1); } + } + openCategory(p, match, pg); + return true; + } + + // ===== 批量出售某商品全部数量 ===== + private void sellAllOf(Player p, ShopItem item) { + if (item == null) return; + // ★ 补上禁止出售检查 + if (item.getEffectiveSellPrice() <= 0) { + p.sendMessage("§c该商品不可出售"); + return; + } + if (item == null) return; + Material mat = item.getMaterial(); + int total = 0; + int count = 0; + // 遍历背包清点 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + total += is.getAmount(); + count++; + } + } + if (total <= 0) { + p.sendMessage("§c你没有可出售的 §e" + + item.getDisplayName()); + return; + } + // 清除背包中该物品 + for (int i = 0; i < p.getInventory().getSize(); i++) { + ItemStack is = p.getInventory().getItem(i); + if (is != null && is.getType() == mat) { + p.getInventory().clear(i); + } + } + int money = item.getEffectiveSellPrice() * total; + plugin.getBonds().addBonds( + p.getName(), money, "shop_sell", + item.getId(), "商店系统", + "批量出售" + item.getDisplayName() + + " x" + total); + if (item.getStock() >= 0) + item.setStock(item.getStock() + total); + p.sendMessage("§a批量出售: §e" + + item.getDisplayName() + + " x" + total + + " §a+" + money + "枚债券"); + } + + + // ===== 定时 ===== + + private void startHourlyTask() { + new BukkitRunnable() { + @Override + public void run() { + // ★ 汇报交给 SalesStatsManager + if (plugin.getSalesStats() != null) { + plugin.getSalesStats().report(); + } + // 重置小时销量 + for (ShopCategory cat : categories) + for (ShopItem item : cat.getItems()) + item.setHourlySales(0); + saveAll(); + } + }.runTaskTimer(plugin, 72000L, 72000L); + } + + + private void startCleanupTask() { + new BukkitRunnable() { + @Override + public void run() { + synchronized (refundRecords) { + refundRecords.removeIf( + RefundRecord::isExpired); + } + } + }.runTaskTimer(plugin, 1200L, 1200L); + } + + // ===== 工具 ===== + + private int slotToIndex(int slot) { + for (int i = 0; i < ITEM_SLOTS.length; i++) + if (ITEM_SLOTS[i] == slot) return i; + return -1; + } + + private ItemStack mkItem(Material mat, + String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/TicketManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/TicketManager.java new file mode 100644 index 0000000..c21edc2 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/TicketManager.java @@ -0,0 +1,1644 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TicketManager { + + private final Main plugin; + + public static final String T_TICKET_MAIN = + "§6§l工单中心"; + public static final String T_TICKET_CREATE = + "§e§l创建工单"; + public static final String T_TICKET_MY = + "§b§l我的工单"; + public static final String T_TICKET_ADMIN = + "§c§l工单管理"; + public static final String T_TICKET_DETAIL = + "§a§l工单详情"; + + public static final String TYPE_BUG = + "bug反馈"; + public static final String TYPE_HELP = + "求助"; + public static final String TYPE_REPORT = + "举报"; + public static final String TYPE_APPLY = + "申请"; + public static final String TYPE_OTHER = + "其他"; + public static final String TYPE_DELETE = + "删号"; + public static final String TYPE_PASSWORD = + "修改密码"; + + + + private static final Map + PRICES = new LinkedHashMap<>(); + static { + PRICES.put(TYPE_BUG, 10); + PRICES.put(TYPE_HELP, 5); + PRICES.put(TYPE_REPORT, 8); + PRICES.put(TYPE_APPLY, 15); + PRICES.put(TYPE_OTHER, 5); + PRICES.put(TYPE_DELETE, 0); + PRICES.put(TYPE_PASSWORD, 0); + } + + + public TicketManager(Main plugin) { + this.plugin = plugin; + } + + // ========== GUI ========== + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_MAIN); + fillBg(g); + g.setItem(10, mk(Material.PAPER, + "§e创建工单", + "§7提交bug、求助、举报等")); + g.setItem(12, mk(Material.BOOK, + "§b我的工单", + "§7查看已提交的工单")); + g.setItem(14, mk(Material.CHEST, + "§6工单记录", + "§7查看历史工单")); + if (adm(p)) { + g.setItem(16, mk(Material.REDSTONE, + "§c工单管理", + "§7管理员专用")); + } + if (plugin.getDb() + .isServiceProvider( + p.getName())) { + g.setItem(18, mk(Material.EMERALD, + "§a服务商面板", + "§7接单、处理工单")); + } + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openCreate(Player p) { + Inventory g = Bukkit.createInventory( + null, 36, T_TICKET_CREATE); + fillBg(g); + g.setItem(10, mk(Material.BOOK, + "§cbug反馈")); + g.setItem(11, mk(Material.EMERALD, + "§a求助")); + g.setItem(12, mk(Material.BLAZE_POWDER, + "§6举报")); + g.setItem(13, mk(Material.NAME_TAG, + "§e申请")); + g.setItem(14, mk(Material.PAPER, + "§7其他")); + g.setItem(20, mk(Material.BARRIER, + "§c申请删号", + "§7申请删除账号")); + g.setItem(21, mk(Material.NETHER_WART, + "§d申请改密", + "§7请求管理员协助改密")); + g.setItem(31, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyTickets(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_MY); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByPlayer( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openDetail(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_DETAIL); + fillBg(g); + + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = sv(t, "requester"); + String title = sv(t, "title"); + String desc = sv(t, "description"); + String assigned = sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + int confirmed = nv(t, "admin_confirmed"); + + g.setItem(10, mk(Material.PAPER, + "§e§l" + (title.isEmpty() + ? type : title), + "§7类型: §f" + type, + "§7状态: " + stxt(status), + "§7提交者: §f" + requester, + "§7奖励: §e" + reward + " 积分")); + + g.setItem(12, mk(Material.BOOK, + "§7详细描述", + desc.isEmpty() ? "§7无描述" + : "§7" + (desc.length() > 40 + ? desc.substring(0, 40) + + "..." : desc))); + + if (!assigned.isEmpty()) { + g.setItem(14, mk(Material.PLAYER_HEAD, + "§7处理人: §f" + assigned, + confirmed == 1 + ? "§7评分: §e" + score + : "§7处理中")); + } else { + g.setItem(14, mk(Material.BARRIER, + "§7未分配")); + } + + // 驳回原因 + if ("closed".equals(status)) { + String reason = sv(t, "reject_reason"); + if (!reason.isEmpty()) { + g.setItem(13, mk(Material.BARRIER, + "§c驳回原因", + "§7" + reason)); + } + } + + // 回复按钮(非关闭/已完结状态) + if (!"closed".equals(status) + && !"withdrawn".equals(status) + && !"resolved".equals(status)) { + g.setItem(15, mk( + Material.WRITABLE_BOOK, + "§b回复工单", + "§7向对方发送消息")); + g.setItem(16, mk(Material.BOOK, + "§7查看回复记录")); + } + + // ===== 管理员按钮 ===== + if (adm(p)) { + if ("pending".equals(status)) { + // 待分配:可分配给服务商 + g.setItem(19, mk(Material.EMERALD, + "§a分配工单", + "§7指定服务商处理")); + // 也可自己处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && !assigned.equals("admin") + && !assigned.isEmpty()) { + // 服务商在处理:可取消指派收回 + g.setItem(19, mk(Material.BARRIER, + "§c取消指派", + "§7取消服务商指派,收回工单")); + // 也可自己接手处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && assigned.equals("admin")) { + // 管理员自己在处理:可标记完结 + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + if ("replied".equals(status) + && assigned.equals("admin")) { + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + // 驳回(非已关闭/已完结状态) + if (!"closed".equals(status) + && !"resolved".equals(status) + && !"withdrawn".equals(status)) { + g.setItem(21, mk(Material.RED_DYE, + "§c驳回工单", + "§7驳回并关闭此工单")); + } + } + + // ===== 服务商按钮 ===== + if (plugin.getDb() + .isServiceProvider(p.getName()) + && !TYPE_DELETE.equals(type) + && !TYPE_PASSWORD.equals(type)) { + if ("pending".equals(status) + && assigned.isEmpty()) { + // 待分配无人:抢单 + g.setItem(19, mk( + Material.GOLD_INGOT, + "§6抢单", + "§7接取此工单")); + } + if (assigned.equals(p.getName()) + && ("processing".equals(status) + || "replied".equals(status))) { + // 我在处理:标记完结/上报异常 + g.setItem(19, mk( + Material.LIME_DYE, + "§a标记完结", + "§7上报处理完成")); + g.setItem(20, mk( + Material.REDSTONE, + "§c上报异常", + "§7报告处理中的问题")); + } + } + + g.setItem(22, mk(Material.ARROW, + "§7返回")); + + ItemStack info = g.getItem(10); + if (info != null) { + ItemMeta im = info.getItemMeta(); + if (im != null) { + im.setCustomModelData(tid); + info.setItemMeta(im); + } + } + p.openInventory(g); + } + + + public void openAdminList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_ADMIN); + fillBg(g); + g.setItem(45, mk(Material.EMERALD, + "§a邀请服务商")); + g.setItem(46, mk(Material.BARRIER, + "§c移除服务商")); + g.setItem(47, mk(Material.CLOCK, + "§e服务商列表")); + g.setItem(48, mk(Material.NETHER_STAR, + "§e直接派单", + "§7管理员直接创建并派单")); + List> all = + new ArrayList<>(); + all.addAll(plugin.getDb() + .getTicketsByStatus("pending")); + all.addAll(plugin.getDb() + .getTicketsByStatus("processing")); + all.addAll(plugin.getDb() + .getTicketsByStatus("replied")); + all.addAll(plugin.getDb() + .getTicketsByStatus("completed")); + all.addAll(plugin.getDb() + .getTicketsByStatus("resolved")); + all.addAll(plugin.getDb() + .getTicketsByStatus("closed")); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + + public void openProviderPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§d§l服务商面板"); + fillBg(g); + g.setItem(10, mk(Material.CHEST, + "§e抢单大厅", + "§7查看可接工单")); + g.setItem(12, mk(Material.CLOCK, + "§b我的工单", + "§7我正在处理的工单")); + g.setItem(14, mk(Material.PAPER, + "§6历史工单", + "§7已完成的工单")); + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openGrabList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l抢单大厅"); + fillBg(g); + List> pending = + plugin.getDb() + .getTicketsByStatus( + "pending"); + List> list = + new ArrayList<>(); + for (Map t : pending) { + if (sv(t, "assigned_to").isEmpty()) { + list.add(t); + } + } + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyAssigned(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l我处理中的工单"); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByProvider( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + // ========== 业务 ========== + + public int submitTicket(Player p, + String type, String title, + String desc) { + int price = PRICES + .getOrDefault(type, 5); + int id = plugin.getDb().createTicket( + type, "pending", "normal", + p.getName(), title, desc, + price, ""); + if (id > 0) { + if (TYPE_DELETE.equals(type) + || TYPE_PASSWORD.equals(type)) { + plugin.getDb().updateTicketField( + id, "assigned_to", "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage( + "§a§l[工单] §f工单 #" + + id + " 已提交," + + "等待管理员处理"); + } else { + p.sendMessage( + "§a§l[工单] §f创建成功!" + + " #§e" + id + + " 奖励: §e" + + price + "积分"); + } + notifyAdmins(p.getName(), id); + } else { + p.sendMessage("§c创建失败"); + } + return id; + } + + + public void adminCreateAndAssign( + Player p, String type, + String title, String desc, + String provider, int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + int id = plugin.getDb().createTicket( + type, "processing", "normal", + p.getName(), title, desc, + price, ""); + if (id <= 0) { + p.sendMessage("§c创建失败"); + return; + } + plugin.getDb().assignTicket( + id, provider); + if (price > 0) { + plugin.getDb().updateTicketField( + id, "reward_amount", price); + } + p.sendMessage("§a工单 #" + id + + " 已创建并分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f管理员指派工单 #" + + id + " 给您"); + playAssignedSound(tp); + } + } + + public void grabTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!"pending".equals( + sv(t, "status"))) { + p.sendMessage("§c该工单已被接单"); + return; + } + if (!sv(t, "assigned_to").isEmpty()) { + p.sendMessage("§c该工单已被分配"); + return; + } + plugin.getDb().assignTicket( + tid, p.getName()); + p.sendMessage("§a抢单成功!工单 #" + tid); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f您的工单 #" + + tid + " 已被 " + + p.getName() + + " 接单"); + } + } + + public void assignToProvider(Player p, + int tid, String provider, + int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + plugin.getDb().assignTicket( + tid, provider); + if (price > 0) { + plugin.getDb().updateTicketStatus( + tid, "processing"); + plugin.getDb().updateTicketField( + tid, "reward_amount", price); + } + p.sendMessage("§a工单 #" + tid + + " 已分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f您有新工单: #" + + tid); + playAssignedSound(tp); + } + } + + public void confirmAndScore(Player p, + int tid, int score) { + if (score < 1 || score > 5) { + p.sendMessage("§c评分1-5"); + return; + } + plugin.getDb().confirmTicket( + tid, score); + p.sendMessage("§a工单 #" + tid + + " 已确认,评分: " + score); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已确认完结"); + } + } + settleTicket(tid); + } + + public void completeAsProvider(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "assigned_to") + .equals(p.getName())) { + p.sendMessage("§c这不是你的工单"); + return; + } + plugin.getDb().completeTicket(tid); + p.sendMessage("§a工单 #" + tid + + " 已标记完结,等待确认"); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已处理完成"); + } + } + + public void rejectTicket(Player p, + int tid, String reason) { + plugin.getDb().updateTicketStatus( + tid, "closed"); + plugin.getDb().updateTicketField( + tid, "reject_reason", reason); + p.sendMessage("§c工单 #" + tid + + " 已驳回并关闭"); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 被驳回: " + + reason); + } + } + } + + public void replyTicket(Player p, + int tid, String message) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + String status = sv(t, "status"); + if ("closed".equals(status) + || "withdrawn".equals(status) + || "resolved".equals(status)) { + p.sendMessage("§c该工单已关闭"); + return; + } + plugin.getDb().addTicketReply( + tid, p.getName(), message); + p.sendMessage("§a回复成功"); + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + String sender = p.getName(); + if (!requester.equals(sender)) { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + if (!assigned.isEmpty() + && !assigned.equals(sender)) { + Player ap = Bukkit.getPlayer( + assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + } + + public void showReplies(Player p, + int tid) { + List> replies = + plugin.getDb() + .getTicketReplies(tid); + if (replies.isEmpty()) { + p.sendMessage("§7暂无回复"); + return; + } + p.sendMessage("§e§l=== 回复记录 ==="); + SimpleDateFormat sdf = + new SimpleDateFormat( + "MM-dd HH:mm"); + for (Map r : replies) { + String sender = sv(r, "sender"); + String msg2 = sv(r, "message"); + long time = ((Number) r.get( + "create_time")).longValue(); + p.sendMessage("§7[" + sdf.format( + new Date(time)) + "] §f" + + sender + "§7: " + msg2); + } + p.sendMessage( + "§e§l================="); + } + + public void withdrawTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "requester") + .equals(p.getName()) + && !adm(p)) { + p.sendMessage("§c无权操作"); + return; + } + plugin.getDb().updateTicketStatus( + tid, "withdrawn"); + p.sendMessage("§a工单 #" + tid + + " 已撤回"); + } + + public void showProviderList(Player p) { + List> list = + plugin.getDb() + .getAllServiceProviders(); + if (list.isEmpty()) { + p.sendMessage("§7暂无服务商"); + return; + } + p.sendMessage( + "§e§l=== 服务商列表 ==="); + for (Map pro : list) { + p.sendMessage("§7- §f" + + pro.get("player_name") + + " §7角色: §e" + + pro.get("role")); + } + p.sendMessage( + "§e§l==================="); + } + + public void autoProcessCompleted() { + long timeoutMs = 10 * 60 * 1000L; + + // 自动处理删号工单(5分钟自动通过) + List> deleteTickets = + plugin.getDb() + .getTicketsByStatus( + "processing"); + for (Map t : deleteTickets) { + String dtype = sv(t, "type"); + if (!TYPE_DELETE.equals(dtype)) continue; + int did = nv(t, "id"); + int dconf = nv(t, "admin_confirmed"); + // 已经处理过了跳过 + if (dconf != 0) continue; + String dasg = sv(t, "assigned_to"); + // 只处理分配给admin的 + if (!"admin".equals(dasg)) continue; + long createTime = ((Number) t.get( + "create_time")).longValue(); + if (System.currentTimeMillis() + - createTime >= 5 * 60 * 1000L) { + processDeleteAccount(did, t); + } + } + + + // 已完结未确认→10分钟自动完结 + List> list = + plugin.getDb() + .getUnconfirmedCompleted( + timeoutMs); + for (Map t : list) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 已自动完结"); + String assigned = + sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) + pp.sendMessage( + "§e§l[工单] §f工单 #" + + id + + " 已自动完结"); + } + settleTicket(id); + } + + // 已回复但10分钟无互动→自动完结 + List> stale = + plugin.getDb() + .getRepliedStaleTickets( + timeoutMs); + for (Map t : stale) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 超时自动完结"); + settleTicket(id); + } + } + + + + // ========== GUI点击 ========== + public boolean handleClick(Player p, + String title, int slot) { + + if (title.equals(T_TICKET_MAIN)) { + if (slot == 10) openCreate(p); + else if (slot == 12) + openMyTickets(p); + else if (slot == 14) + openMyTickets(p); + else if (slot == 16) { + if (adm(p)) openAdminList(p); + else p.sendMessage("§c权限不足"); + } + else if (slot == 18) { + if (plugin.getDb() + .isServiceProvider(p.getName())) { + openProviderPanel(p); + } else { + p.sendMessage("§c权限不足"); + } + } + else if (slot == 22) + plugin.getGui().openMain(p); + return true; + } + + if (title.equals(T_TICKET_CREATE)) { + if (slot == 31) { + openMain(p); + return true; + } + String type = null; + if (slot == 10) type = TYPE_BUG; + else if (slot == 11) type = TYPE_HELP; + else if (slot == 12) type = TYPE_REPORT; + else if (slot == 13) type = TYPE_APPLY; + else if (slot == 14) type = TYPE_OTHER; + else if (slot == 20) type = TYPE_DELETE; + else if (slot == 21) type = TYPE_PASSWORD; + if (type != null) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_TITLE; + plugin.getChatInput() + .getState(p) + .ticketType = type; + if (TYPE_DELETE.equals(type)) { + p.sendMessage("§e请描述删号原因:"); + } else if (TYPE_PASSWORD.equals(type)) { + p.sendMessage("§e请描述需要协助的内容:"); + } else { + p.sendMessage("§e请输入工单标题:"); + } + } + return true; + } + + + if (title.equals(T_TICKET_MY)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals(T_TICKET_DETAIL)) { + return handleClickDetail(p, slot); + } + + if (title.equals(T_TICKET_ADMIN)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot == 45) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_INVITE_PROVIDER; + p.sendMessage("§e输入服务商玩家名:"); + return true; + } + if (slot == 46) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REMOVE_PROVIDER; + p.sendMessage("§e输入要移除的服务商:"); + return true; + } + if (slot == 47) { + showProviderList(p); + return true; + } + if (slot == 48) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ADMIN_TYPE; + p.sendMessage("§e选择工单类型:"); + p.sendMessage("§7bug/求助/举报/申请/其他"); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§d§l服务商面板")) { + if (slot == 22) { + plugin.getGui().openMain(p); + return true; + } + if (slot == 10) openGrabList(p); + else if (slot == 12) openMyAssigned(p); + else if (slot == 14) openMyTickets(p); + return true; + } + + if (title.equals("§6§l抢单大厅")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§b§l我处理中的工单")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + return false; + } + + private boolean handleClickDetail(Player p, + int slot) { + if (slot == 22) { + openMyTickets(p); + return true; + } + int id = gtFromDetail(p); + if (id <= 0) return true; + Map t = + plugin.getDb().getTicket(id); + if (t == null) return true; + + String st = sv(t, "status"); + int conf = nv(t, "admin_confirmed"); + String asg = sv(t, "assigned_to"); + + // 回复按钮 + if (slot == 15) { + if (!"closed".equals(st) + && !"withdrawn".equals(st) + && !"resolved".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPLY; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入回复内容:"); + } + return true; + } + + // 查看回复记录 + if (slot == 16) { + showReplies(p, id); + return true; + } + + // slot 19 + if (slot == 19) { + // 管理员:分配工单 + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ASSIGN; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入: 服务商名 价格"); + return true; + } + // 管理员:取消指派(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + cancelTicket(p, id); + openDetail(p, id); + return true; + } + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); + return true; + } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; + } + } + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && "pending".equals(st) + && asg.isEmpty()) { + grabTicket(p, id); + openDetail(p, id); + return true; + } + // 服务商:标记完结 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + completeAsProvider(p, id); + openDetail(p, id); + return true; + } + return true; + } + + // slot 20 + if (slot == 20) { + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:自己接手(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 服务商:上报异常 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPORT_ISSUE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入异常描述:"); + return true; + } + return true; + } + + // slot 21:驳回 + if (slot == 21) { + if (adm(p) && !"closed".equals(st) + && !"resolved".equals(st) + && !"withdrawn".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REJECT; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入驳回原因:"); + } + return true; + } + + return true; + } + + // ========== 聊天输入 ========== + + public boolean handleChatInput(Player p, + String msg, + ChatInputManager.InputType type) { + switch (type) { + case TICKET_TITLE: { + plugin.getChatInput() + .getState(p) + .ticketTitle = msg; + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return true; + } + case TICKET_DESC: { + String tt = plugin.getChatInput() + .getState(p).ticketType; + String tl = plugin.getChatInput() + .getState(p).ticketTitle; + plugin.getChatInput().reset(p); + submitTicket(p, tt, tl, msg); + return true; + } + case TICKET_SCORE: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + Map tk = + plugin.getDb() + .getTicket(tid2); + String st = tk != null + ? sv(tk, "status") + : ""; + if ("completed".equals(st)) { + plugin.getChatInput() + .reset(p); + confirmAndScore( + p, tid2, sc); + } else { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c工单尚未处理完成"); + } + } catch ( + NumberFormatException e) { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return true; + } + case TICKET_REJECT: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + plugin.getChatInput().reset(p); + rejectTicket(p, tid2, msg); + return true; + } + case TICKET_ASSIGN: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + plugin.getChatInput().reset(p); + assignToProvider(p, tid2, + prov, price); + runSync(() -> + openDetail(p, tid2)); + return true; + } + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return true; + } + plugin.getDb() + .addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return true; + } + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .isServiceProvider( + tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return true; + } + plugin.getDb() + .removeServiceProvider( + tgt); + p.sendMessage( + "§a已移除 " + tgt); + return true; + } + default: + return false; + } + } + + // ========== 私有 ========== + + private void settleTicket(int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) return; + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + ConfigManager cfg = + plugin.getConfig2(); + if (reward > 0) { + plugin.getDb().addPoints( + requester, reward); + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 积分奖励"); + } + } + if (!assigned.isEmpty()) { + int pts = (int) (score + * cfg.providerPointsPerScore); + if (pts > 0) { + plugin.getDb().addPoints( + assigned, pts); + } + double money = + cfg.baseEconomyReward + + score + * cfg.providerEconomyPerScore; + boolean ok = false; + try { + if (plugin.getEconomy() != null) { + OfflinePlayer op = + Bukkit.getOfflinePlayer( + assigned); + plugin.getEconomy() + .depositPlayer( + op, money); + ok = true; + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 经济失败: " + + e.getMessage()); + } + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (pts > 0) + pp.sendMessage("§7积分: §e" + + pts); + if (ok) + pp.sendMessage("§7经济: §a$" + + String.format( + "%.0f", money)); + } + } + } + + private void notifyAdmins(String creator, + int tid) { + String tag = + plugin.getConfig2().adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§e§l[工单] §f" + creator + + " 提交了新工单 #" + + tid); + } + } + } + + private void playAssignedSound(Player p) { + for (int i = 0; i < 3; i++) { + final int d = i * 15; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline()) { + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + }, d); + } + } + + private void runSync(Runnable task) { + Bukkit.getScheduler() + .runTask(plugin, task); + } + + private boolean adm(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory g) { + ItemStack glass = mk( + Material.GRAY_STAINED_GLASS_PANE, + " "); + for (int i = 0; i < g.getSize(); i++) { + if (g.getItem(i) == null + || g.getItem(i).getType() + == Material.AIR) { + g.setItem(i, glass); + } + } + } + + private void fillList(Inventory g, + List> list) { + if (list.isEmpty()) { + g.setItem(22, mk(Material.BARRIER, + "§7暂无工单")); + return; + } + int slot = 0; + for (Map t : list) { + if (slot >= 45) break; + g.setItem(slot, makeItem(t)); + slot++; + } + } + /** + * 执行删号流程 + */ + /** + * 执行删号流程 + */ + public void processDeleteAccount(int tid, + Map t) { + String requester = sv(t, "requester"); + plugin.getDb().updateTicketStatus( + tid, "resolved"); + plugin.getDb().updateTicketField(tid, + "admin_confirmed", 1); + Player target = + Bukkit.getPlayer(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + // 延迟执行删号 + final String name = requester; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + plugin.getDb().deleteUser(name); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 删号工单已执行"); + }, 100L); + } + + /** + * 管理员手动批准删号 + */ + public void approveDeleteTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!TYPE_DELETE.equals(sv(t, "type"))) { + p.sendMessage("§c这不是删号工单"); + return; + } + p.sendMessage("§a工单 #" + tid + + " 已批准,正在执行删号..."); + processDeleteAccount(tid, t); + } + + /** + * 管理员取消工单(服务商异常后) + */ + /** + * 管理员取消工单(解除服务商绑定,管理员可自行处理) + */ + public void cancelTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + // 解除服务商绑定,状态回退到pending + plugin.getDb().updateTicketField( + tid, "assigned_to", ""); + plugin.getDb().updateTicketStatus( + tid, "pending"); + p.sendMessage("§a工单 #" + tid + + " 已取消指派,回到待分配状态"); + // 通知原服务商 + String assigned = sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player ap = Bukkit.getPlayer(assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 已被管理员取消指派"); + } + } + } + + + /** + * 服务商查看历史工单 + */ + public void openProviderHistory(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l我的历史工单"); + fillBg(g); + List> all = + plugin.getDb() + .getTicketsByProviderAll( + p.getName()); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private ItemStack makeItem( + Map t) { + int id = nv(t, "id"); + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = + sv(t, "requester"); + String title = sv(t, "title"); + int reward = nv(t, "reward_amount"); + Material mat; + switch (status) { + case "processing": + mat = Material.YELLOW_DYE; + break; + case "replied": + mat = Material.CYAN_DYE; + break; + case "completed": + mat = Material.LIME_DYE; + break; + case "resolved": + mat = Material.LIME_DYE; + break; + case "closed": + mat = Material.GRAY_DYE; + break; + case "rejected": + mat = Material.RED_DYE; + break; + case "withdrawn": + mat = Material.GRAY_DYE; + break; + default: + mat = Material.ORANGE_DYE; + break; + } + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName("§e#" + id + + " " + (title.isEmpty() + ? type : title)); + List lore = + new ArrayList<>(); + lore.add("§7类型: §f" + type); + lore.add("§7状态: " + + stxt(status)); + lore.add("§7提交者: §f" + + requester); + lore.add("§7奖励: §e" + reward + + " 积分"); + lore.add(""); + lore.add("§7点击查看详情"); + im.setLore(lore); + im.setCustomModelData(id); + item.setItemMeta(im); + } + return item; + } + + private int gt(ItemStack item) { + if (item == null) return -1; + ItemMeta im = item.getItemMeta(); + if (im == null) return -1; + if (im.hasCustomModelData()) + return im.getCustomModelData(); + return -1; + } + + private int gtFromDetail(Player p) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(10); + return gt(it); + } + + private String stxt(String status) { + switch (status) { + case "pending": + return "§e待处理"; + case "processing": + return "§b处理中"; + case "replied": + return "§9已回复"; + case "completed": + return "§a待确认"; + case "resolved": + return "§a已完结"; + case "closed": + return "§7已关闭"; + case "rejected": + return "§c已驳回"; + case "withdrawn": + return "§7已撤回"; + default: + return "§7" + status; + } + } + + private String sv(Map m, + String key) { + Object v = m.get(key); + return v != null + ? String.valueOf(v) : ""; + } + + private int nv(Map m, + String key) { + Object v = m.get(key); + return v instanceof Number + ? ((Number) v).intValue() : 0; + } + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) { + if (!l.isEmpty()) + list.add(l); + } + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/UpdateChecker.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/UpdateChecker.java new file mode 100644 index 0000000..c5c01bb --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/UpdateChecker.java @@ -0,0 +1,523 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = + "ypsdf1"; + private static final String GITHUB_REPO = + "sdf1_login"; + private static final String GITEE_OWNER = + "nihaoshidifu"; + private static final String GITEE_REPO = + "sdf1_login"; + + + // API 地址 + private static final String GITHUB_API = + "https://api.github.com/repos/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + + "/tags"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + + GITEE_OWNER + "/" + + GITEE_REPO + + "/tags"; + + // 仓库页面链接(给用户看的) + private static final String GITHUB_LINK = + "https://github.com/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + + GITEE_OWNER + "/" + + GITEE_REPO + "/releases"; + + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 3000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = + false; + private volatile int failCount = 0; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = + false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = + plugin.getDescription() + .getVersion(); + this.disabledFile = new File( + plugin.getDataFolder(), + "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = + new BufferedReader( + new FileReader( + disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = + "true".equals( + line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = + new PrintWriter( + new FileWriter( + disabledFile))) { + pw.println(autoCheckDisabled + ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) return; + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doCheck(null, true)); + } + + /** + * /sdf1_login update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + * 只有拿到版号才解除熔断 + */ + public void checkUpdate( + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> + doCheck(sender, + false)); + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, + boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled + || circuitTripped) { + return; + } + } + + // 通道1:GitHub + CheckResult gh = + tryChannel(GITHUB_API, + "GitHub", GITHUB_LINK); + + // 通道2:Gitee + CheckResult ge = + tryChannel(GITEE_API, + "Gitee", GITEE_LINK); + + // 选择成功的结果 + CheckResult chosen = null; + String failMsg = null; + + if (gh.isSuccess()) { + chosen = gh; + } else if (ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + // 拼接两个通道的失败原因 + StringBuilder sb = + new StringBuilder(); + if (gh.error != null) { + sb.append("§7GitHub: §c") + .append(gh.error); + } + if (ge.error != null) { + if (sb.length() > 0) + sb.append("\n"); + sb.append("§7Gitee: §c") + .append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_login] 更新检测" + + "已熔断,连续失败" + + failCount + "次"); + } + if (sender != null) { + final String msg = + sb.toString(); + final boolean tripped = + circuitTripped; + final int fc = failCount; + Bukkit.getScheduler() + .runTask(plugin, () -> { + sender.sendMessage( + "§c§l[更新] " + + "检测失败"); + sender.sendMessage(msg); + sender.sendMessage( + "§e失败次数: " + + fc + "/" + + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage( + "§c已熔断," + + "请重启服务器" + + "重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_login] 远程版本信号" + + " \"" + chosen.version + + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler() + .runTask(plugin, () -> + sender.sendMessage( + "§e[更新] 远程已" + + "禁用自动" + + "更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + Bukkit.getScheduler() + .runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage( + "§e[更新] 检测成功" + + " via: §a" + src); + sender.sendMessage( + "§e[更新] 本地版本:" + + " §f" + + currentVersion); + sender.sendMessage( + "§e[更新] 远程版本:" + + " §f" + rv); + + if (isNewer(rv, + currentVersion)) { + sender.sendMessage( + "§c[更新] 发现新" + + "版本: " + rv); + sender.sendMessage( + "§a请前往仓库下载" + + "更新:"); + sender.sendMessage( + "§a" + lnk); + } else { + sender.sendMessage( + "§a[更新] 已是" + + "最新版本"); + } + + // 显示另一通道的失败原因 + // (仅信息,不影响结果) + CheckResult other = + (src.equals("GitHub")) + ? ge : gh; + if (!other.isSuccess() + && other.error != null) { + sender.sendMessage( + "§7[更新] " + + other.source + + "不可用: " + + other.error); + } + }); + } + + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal( + String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) + || "关闭".equals(v) + || "false".equals(v) + || "禁用".equals(v); + } + + // ========== HTTP 请求(带超时) ========== + + // ========== 检测结果 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, + String source, + String link, + String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + +// ========== HTTP 请求 ========== + + private CheckResult tryChannel( + String apiUrl, + String sourceName, + String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) + url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; " + + "Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty( + "Accept", + "*/*"); + conn.setRequestProperty( + "Accept-Language", + "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": HTTP " + code); + } + + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn + .getInputStream())); + StringBuilder sb = + new StringBuilder(); + String line; + while ((line = reader.readLine()) + != null) { + sb.append(line); + } + reader.close(); + + String tag = + parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 无法解析版本号"); + } + + return new CheckResult(tag, + sourceName, link, null); + + } catch (java.net + .SocketTimeoutException e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 连接被拒绝"); + } catch (java.net + .UnknownHostException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": " + + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + + + /** + * 从 JSON 中提取 tag_name + */ + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + // 尝试 tag_name(releases格式) + int idx = + json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + // 尝试 "name"(tags格式) + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") + || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + + // ========== 版本比较 ========== + + private boolean isNewer(String remote, + String local) { + String[] r = remote.split("\\."); + String[] l = local.split("\\."); + int len = Math.max(r.length, l.length); + for (int i = 0; i < len; i++) { + int ri = i < r.length + ? parseNum(r[i]) : 0; + int li = i < l.length + ? parseNum(l[i]) : 0; + if (ri > li) return true; + if (ri < li) return false; + } + return false; + } + + private int parseNum(String s) { + try { + return Integer.parseInt( + s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/UserGroupManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/UserGroupManager.java new file mode 100644 index 0000000..adaf907 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/UserGroupManager.java @@ -0,0 +1,370 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.util.*; +import java.util.Base64; +import java.util.concurrent.ConcurrentHashMap; + +public class UserGroupManager { + + private final Main plugin; + private Connection db; + private final Map groupConfigs = new ConcurrentHashMap<>(); + + public UserGroupManager(Main plugin) { + this.plugin = plugin; + initDB(); + loadGroupConfigs(); + startExpiryChecker(); + } + + // ===== DB ===== + + private void initDB() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(plugin.getDataFolder(), "usergroup.db"); + db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_config (" + + "group_name TEXT PRIMARY KEY," + + "display_name TEXT DEFAULT ''," + + "display_color TEXT DEFAULT '§f'," + + "display_emoji TEXT DEFAULT ''," + + "priority INTEGER DEFAULT 0," + + "is_permanent INTEGER DEFAULT 1," + + "duration_minutes INTEGER DEFAULT 0," + + "pack_url TEXT DEFAULT ''," + + "pack_hash TEXT DEFAULT ''," + + "pack_prompt TEXT DEFAULT 'Custom Resource Pack'," + + "pack_size INTEGER DEFAULT 0)"); + st.execute("CREATE TABLE IF NOT EXISTS user_group_member (" + + "player_name TEXT NOT NULL," + + "group_name TEXT NOT NULL," + + "added_by TEXT DEFAULT 'system'," + + "added_time INTEGER DEFAULT 0," + + "expiry_time INTEGER DEFAULT 0," + + "PRIMARY KEY(player_name, group_name))"); + st.close(); + } catch (Exception e) { + throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e); + } + } + + // ===== 配置加载 ===== + + public void loadGroupConfigs() { + groupConfigs.clear(); + File dir = new File(plugin.getDataFolder(), "用户组"); + if (!dir.exists()) dir.mkdirs(); + File[] files = dir.listFiles((d, n) -> n.endsWith(".txt")); + if (files == null) return; + + for (File f : files) { + try { + List lines = java.nio.file.Files.readAllLines( + f.toPath(), StandardCharsets.UTF_8); + UserGroupConfig cfg = parseGroupConfig(lines); + if (cfg != null) { + groupConfigs.put(cfg.name, cfg); + saveGroupConfigToDB(cfg); + } + } catch (Exception e) { + plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName()); + } + } + } + + private UserGroupConfig parseGroupConfig(List lines) { + UserGroupConfig cfg = new UserGroupConfig(); + boolean inBlock = false; + + for (String raw : lines) { + String l = raw; + + // 块注释 + if (l.contains("")) { inBlock = false; continue; } + if (l.contains("/*")) { inBlock = true; continue; } + if (l.contains("*/")) { inBlock = false; continue; } + if (inBlock) continue; + + // 行注释 + l = l.trim(); + if (l.isEmpty()) continue; + if (l.startsWith("#") || l.startsWith("//")) continue; + + // 行内注释 + int h = l.indexOf('#'); + int s = l.indexOf("//"); + if (h >= 0) l = l.substring(0, h).trim(); + if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim(); + if (l.isEmpty()) continue; + + // 解析键值对 + String key = l; + String val = ""; + int idx = l.indexOf('\uff1a'); + if (idx < 0) idx = l.indexOf(':'); + if (idx >= 0) { + key = l.substring(0, idx).trim(); + val = l.substring(idx + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) + val = val.substring(1, val.length() - 1); + } + + switch (key) { + case "组名": + case "name": cfg.name = val; break; + case "显示名": + case "display": cfg.displayName = val; break; + case "颜色": + case "color": cfg.displayColor = val; break; + case "符号": + case "emoji": cfg.displayEmoji = val; break; + case "优先级": + case "priority": + try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "类型": + case "type": + cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent"); + break; + case "时长": + case "duration": + try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "资源包URL": + case "pack_url": cfg.packUrl = val; break; + case "资源包Hash": + case "pack_hash": cfg.packHash = val; break; + case "资源包提示": + case "pack_prompt": cfg.packPrompt = val; break; + case "资源包大小": + case "pack_size": + try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {} + break; + case "本地文件": + case "local_file": cfg.localFile = val; break; + } + } + + if (cfg.name == null || cfg.name.isEmpty()) return null; + return cfg; + } + + private void saveGroupConfigToDB(UserGroupConfig cfg) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_config " + + "(group_name, display_name, display_color, display_emoji," + + " priority, is_permanent, duration_minutes," + + " pack_url, pack_hash, pack_prompt, pack_size)" + + " VALUES (?,?,?,?,?,?,?,?,?,?,?)"); + ps.setString(1, cfg.name); + ps.setString(2, cfg.displayName); + ps.setString(3, cfg.displayColor); + ps.setString(4, cfg.displayEmoji); + ps.setInt(5, cfg.priority); + ps.setInt(6, cfg.permanent ? 1 : 0); + ps.setInt(7, cfg.durationMinutes); + ps.setString(8, cfg.packUrl); + ps.setString(9, cfg.packHash); + ps.setString(10, cfg.packPrompt); + ps.setInt(11, cfg.packSize); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 玩家操作 ===== + + public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) { + UserGroupConfig cfg = groupConfigs.get(groupName); + if (cfg == null) return false; + long now = System.currentTimeMillis(); + long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000; + if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000; + else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000; + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO user_group_member " + + "(player_name, group_name, added_by, added_time, expiry_time)" + + " VALUES (?,?,?,?,?)"); + ps.setString(1, player); + ps.setString(2, groupName); + ps.setString(3, addedBy); + ps.setLong(4, now); + ps.setLong(5, expiry); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + Player p = plugin.getServer().getPlayerExact(player); + if (p != null) sendResourcePack(p); + return true; + } + + public boolean removePlayer(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + int rows = ps.executeUpdate(); + ps.close(); + return rows > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public List> getPlayerGroups(String player) { + List> list = new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM user_group_member WHERE player_name=?"); + ps.setString(1, player); + ResultSet rs = ps.executeQuery(); + long now = System.currentTimeMillis(); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("group_name", rs.getString("group_name")); + row.put("added_by", rs.getString("added_by")); + long expiry = rs.getLong("expiry_time"); + row.put("expiry_time", expiry); + row.put("expired", expiry > 0 && expiry <= now); + list.add(row); + } + rs.close(); ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public UserGroupConfig getHighestGroup(String player) { + List> groups = getPlayerGroups(player); + long now = System.currentTimeMillis(); + UserGroupConfig best = null; + for (Map g : groups) { + long expiry = (long) g.get("expiry_time"); + if (expiry > 0 && expiry <= now) continue; + String name = (String) g.get("group_name"); + UserGroupConfig cfg = groupConfigs.get(name); + if (cfg != null && (best == null || cfg.priority > best.priority)) { + best = cfg; + } + } + return best; + } + + public boolean isPlayerInGroup(String player, String groupName) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?"); + ps.setString(1, player); + ps.setString(2, groupName); + boolean exists = ps.executeQuery().next(); + ps.close(); + return exists; + } catch (SQLException e) { + return false; + } + } + + // ===== 过期自动移除 ===== + + private void startExpiryChecker() { + new BukkitRunnable() { + @Override + public void run() { + checkExpiry(); + } + }.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查 + } + + private void checkExpiry() { + try { + long now = System.currentTimeMillis(); + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, group_name FROM user_group_member " + + "WHERE expiry_time > 0 AND expiry_time <= ?"); + ps.setLong(1, now); + ResultSet rs = ps.executeQuery(); + List expired = new ArrayList<>(); + while (rs.next()) { + expired.add(new String[]{ + rs.getString("player_name"), + rs.getString("group_name")}); + } + rs.close(); ps.close(); + + for (String[] e : expired) { + PreparedStatement del = db.prepareStatement( + "DELETE FROM user_group_member WHERE player_name=? AND group_name=?"); + del.setString(1, e[0]); + del.setString(2, e[1]); + del.executeUpdate(); + del.close(); + plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ===== 资源包分发 ===== + + public void sendResourcePack(Player p) { + UserGroupConfig cfg = getHighestGroup(p.getName()); + if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return; + + String url = cfg.packUrl; + String prompt = cfg.packPrompt != null + ? cfg.packPrompt : "Custom Resource Pack"; + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + try { + p.setResourcePack(url, new byte[0], prompt, false); + plugin.getLogger().info("[UserGroup] 资源包已发送: " + + p.getName() + " -> " + cfg.displayName); + } catch (Exception e) { + plugin.getLogger().warning( + "[UserGroup] 资源包发送失败: " + e.getMessage()); + } + }, 40L); + } + + public Map getGroupConfigs() { + return groupConfigs; + } + + // ===== 配置类 ===== + + public static class UserGroupConfig { + public String name; + public String displayName = ""; + public String displayColor = "§f"; + public String displayEmoji = ""; + public int priority = 0; + public boolean permanent = true; + public int durationMinutes = 0; + public String packUrl = ""; + public String packHash = ""; + public String packPrompt = "Custom Resource Pack"; + public int packSize = 0; + public String localFile = ""; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/VerificationManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/VerificationManager.java new file mode 100644 index 0000000..3fc4ceb --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/VerificationManager.java @@ -0,0 +1,158 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.function.Consumer; + +public class VerificationManager { + + private final Main plugin; + + public VerificationManager(Main plugin) { + this.plugin = plugin; + } + + public boolean isBedrockPlayer(Player p) { + String uuid = p.getUniqueId().toString(); + return uuid.contains("f851ba4") + || uuid.startsWith("00000000-0000-0000"); + } + + public boolean isOnlineMode() { + return Bukkit.getServer().getOnlineMode(); + } + + public void verifyPremiumAsync(Player player, + Consumer callback) { + Thread t = new Thread(() -> doVerify(player, callback), + "Sdf1_login-PremiumVerify"); + t.setDaemon(true); + t.start(); + } + + private void doVerify(Player player, + Consumer callback) { + String name = player.getName(); + String uuid = player.getUniqueId().toString(); + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证开始 ======"); + plugin.getLogger().info( + "[Sdf1_login] 玩家: " + name); + plugin.getLogger().info( + "[Sdf1_login] 当前UUID: " + uuid); + + try { + UUID offlineUuid = UUID.nameUUIDFromBytes( + ("OfflinePlayer:" + name) + .getBytes(StandardCharsets.UTF_8)); + String offlineStr = offlineUuid.toString(); + + plugin.getLogger().info( + "[Sdf1_login] 离线UUID: " + offlineStr); + + boolean isOfflineUuid = + player.getUniqueId().equals(offlineUuid); + + plugin.getLogger().info( + "[Sdf1_login] UUID匹配离线格式: " + + isOfflineUuid); + + if (!isOfflineUuid) { + plugin.getLogger().info( + "[Sdf1_login] 结论: 正版玩家"); + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(true); + return; + } + + plugin.getLogger().info( + "[Sdf1_login] UUID为离线格式," + + "查询Mojang API..."); + UUID mojangUuid = fetchMojangUuid(name); + + if (mojangUuid != null) { + plugin.getLogger().info( + "[Sdf1_login] Mojang返回UUID: " + + mojangUuid); + plugin.getLogger().info( + "[Sdf1_login] 结论: 盗版客户端" + + "使用了正版名字"); + } else { + plugin.getLogger().info( + "[Sdf1_login] Mojang无此名字"); + plugin.getLogger().info( + "[Sdf1_login] 结论: 纯离线玩家"); + } + + plugin.getLogger().info( + "[Sdf1_login] ====== 验证结束 ======"); + callback.accept(false); + + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 验证异常: " + + e.getMessage()); + callback.accept(false); + } + } + + private UUID fetchMojangUuid(String name) { + HttpURLConnection conn = null; + try { + URL url = new URL( + "https://api.mojang.com/users/profiles/minecraft/" + + name); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setRequestMethod("GET"); + + int code = conn.getResponseCode(); + plugin.getLogger().info( + "[Sdf1_login] Mojang API响应: " + code); + + if (code == 200) { + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn.getInputStream(), + StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String json = sb.toString(); + String uuidStr = json.replaceAll( + ".*\"id\"\\s*:\\s*\"([^\"]+)\".*", + "$1"); + plugin.getLogger().info( + "[Sdf1_login] Mojang原始UUID: " + + uuidStr); + + String formatted = uuidStr.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5"); + return UUID.fromString(formatted); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] Mojang查询异常: " + + e.getMessage()); + } finally { + if (conn != null) conn.disconnect(); + } + return null; + } +} diff --git a/backup/src_backup_20260605_125051/main/java/Sdf1_login/WelcomeManager.java b/backup/src_backup_20260605_125051/main/java/Sdf1_login/WelcomeManager.java new file mode 100644 index 0000000..68bd3ef --- /dev/null +++ b/backup/src_backup_20260605_125051/main/java/Sdf1_login/WelcomeManager.java @@ -0,0 +1,163 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class WelcomeManager { + + private final Main plugin; + private final Random random = new Random(); + private final List registerMsgs = + new ArrayList<>(); + private final List loginMsgs = + new ArrayList<>(); + + public WelcomeManager(Main plugin) { + this.plugin = plugin; + loadMessages(); + } + + public void loadMessages() { + registerMsgs.clear(); + loginMsgs.clear(); + File f = new File( + plugin.getDataFolder(), + "欢迎仪式.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("注册") + || t.equals("注册:")) { + section = "register"; + continue; + } + if (t.equals("登录") + || t.equals("登录:")) { + section = "login"; + continue; + } + // 支持 = 和 : 两种分隔 + if (section.isEmpty()) { + if (t.startsWith("注册") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + registerMsgs.add( + colorize(v)); + continue; + } + if (t.startsWith("登录") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + loginMsgs.add( + colorize(v)); + continue; + } + continue; + } + if (!t.isEmpty()) { + if (section.equals("register")) + registerMsgs.add( + colorize(t)); + else + loginMsgs.add( + colorize(t)); + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[欢迎] 加载失败: " + + e.getMessage()); + } + plugin.getLogger().info( + "[欢迎] 加载完成: 注册=" + + registerMsgs.size() + + " 登录=" + loginMsgs.size()); + } + + public void onRegister(Player p) { + sendRandom(p, registerMsgs, "注册"); + } + + public void onLogin(Player p) { + sendRandom(p, loginMsgs, "登录"); + } + + private void sendRandom(Player p, + List msgs, String tag) { + if (msgs.isEmpty()) return; + String msg = msgs.get( + random.nextInt(msgs.size())); + msg = msg.replace("{player}", + p.getName()); + final String finalMsg = msg; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> Bukkit + .broadcastMessage( + finalMsg), 10L); + } + + private String colorize(String msg) { + return msg.replace("&", "\u00a7"); + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets + .UTF_8))) { + pw.println("# 欢迎仪式配置"); + pw.println("# &为颜色代码"); + pw.println("# {player} = 玩家名"); + pw.println(); + pw.println("注册:"); + pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]"); + pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]"); + pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]"); + pw.println(); + pw.println("登录:"); + pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]"); + pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]"); + pw.println("&b&l{player} &b回来了!&7[登录]"); + } catch (IOException ignored) { + } + } +} diff --git a/backup/src_backup_20260605_125051/main/resources/plugin.yml b/backup/src_backup_20260605_125051/main/resources/plugin.yml new file mode 100644 index 0000000..f6868d9 --- /dev/null +++ b/backup/src_backup_20260605_125051/main/resources/plugin.yml @@ -0,0 +1,104 @@ +name: Sdf1_login +version: 1.0 +main: Sdf1_login.Main +api-version: '1.21' +commands: + sdf1_login: + description: Sdf1_login主命令 + usage: /sdf1_login + reg: + description: 注册 + usage: /reg <密码> + login: + description: 登录 + usage: /login <密码> + l: + description: 登录快捷版 + usage: /l <密码> + 注册: + description: 注册(中文) + usage: /注册 <密码> + 登录: + description: 登录(中文) + usage: /登录 <密码> + 玩家信息: + description: 打开面板(中文) + usage: /玩家信息 + 改密码: + description: 修改密码(中文) + usage: /改密码 <旧密码> <新密码> <确认新密码> + 设置邮箱: + description: 设置邮箱(中文) + usage: /设置邮箱 <邮箱地址> + 撤销: + description: 撤销操作(中文) + usage: /撤销 + 删除: + description: 删除玩家(中文) + usage: /删除 <玩家> <密码> + 找回密码: + description: 找回密码(中文) + usage: /找回密码 + 签到: + description: 每日签到(中文) + usage: /签到 + #oa: + # description: 申请删除账号 + # usage: /oa [玩家名] + 删除账号: + description: 申请删除账号(中文) + usage: /删除账号 [玩家名] + 通过: + description: 批准OA请求(中文) + usage: /通过 <编号> + 拒绝: + description: 拒绝OA请求(中文) + usage: /拒绝 <编号> [理由] + accept: + description: 批准OA请求 + usage: /accept <编号> + deny: + description: 拒绝OA请求 + usage: /deny <编号> [理由] + email: + description: 绑定邮箱 + usage: /email <邮箱> + 绑定: + description: 绑定邮箱(中文) + usage: /绑定 <邮箱> + 绑定邮箱: + description: 绑定邮箱 + usage: /绑定邮箱 + chat: + description: 聊天过滤管理 + usage: /chat + recycle: + description: 打开垃圾回收站 + usage: /recycle + 垃圾清理: + description: 手动清理掉落物 + usage: /垃圾清理 + menu: + description: 雪球菜单开关控制 + usage: /menu <开/关> + pvp : + description: PVP榜单 + usage: /pvp + cypay : + description: 草原探险债券 + usage: /cypay + printer : + description: 打印债券流水 + usage: /printer + shop : + description: 游戏商人 + usage: /shop + 商店: + description: 打开商店 + usage: /商店 + #testorder: + # description: 测试订单中心 + # usage: /testorder + protect: + description: 区域保护 + usage: /protect \ No newline at end of file diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index 91f3f90..5b4992f 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -21,6 +21,11 @@ import org.bukkit.event.block.BlockSpreadEvent; import org.bukkit.entity.Entity; import org.bukkit.event.player.PlayerInteractAtEntityEvent; import java.sql.*; +import org.bukkit.event.entity.*; +import org.bukkit.event.player.*; +import org.bukkit.GameMode; +import org.bukkit.event.EventPriority; + import java.io.*; import java.nio.charset.StandardCharsets; @@ -30,21 +35,20 @@ import org.bukkit.Bukkit; import java.util.Iterator; 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 org.bukkit.entity.Item; + public class AreaProtection implements Listener { private final Main plugin; - private final Map lastLeaveTime - = new HashMap<>(); - private final Map> pendingClear - = new HashMap<>(); - private final Map> activeEffects - = new HashMap<>(); + public List giveEffects = new ArrayList<>(); private final Map> playerAreas = new HashMap<>(); - private final File areaDir; private final Map areas = new ConcurrentHashMap<>(); private Connection dbConnection; @@ -72,6 +76,12 @@ public class AreaProtection implements Listener { private final Set selecting = ConcurrentHashMap.newKeySet(); 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 = @@ -82,20 +92,410 @@ public class AreaProtection implements Listener { = new HashMap<>(); private final Map protectedEntities = new ConcurrentHashMap<>(); - // 记录每个玩家当前所在的区域名 - private final Map playerCurrentArea - = new HashMap<>(); + // 区域给予的效果记录(贴标) + 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<>(); + // 防止传送时重复处理 + private final Set teleporting + = ConcurrentHashMap.newKeySet(); + // 待清理的效果列表(与延时任务配合) + private final Map> pendingClearEffects + = new ConcurrentHashMap<>(); + +// ==================== 强制游戏模式 ==================== + + /** + * 监听玩家尝试切换游戏模式 + * 如果在强制游戏模式区域且不在豁免名单中,则取消切换 + */ + // 防止递归的标记 + 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 (p.isOp()) { + plugin.getLogger().info( + "[防护-调试] 豁免原因: OP, 玩家=" + name); + return true; + }*/ + if (globalPlayerWhitelist.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 全局白名单, 玩家=" + + name);*/ + return true; + } + Set aw = + areaPlayerWhitelist.get(areaName); + if (aw != null && aw.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 区域白名单, 玩家=" + + name + " 区域=" + areaName);*/ + return true; + } + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) { + /* plugin.getLogger().info( + "[防护-调试] 豁免原因: 模式排除名单, 玩家=" + + name);*/ + return true; + } + /* plugin.getLogger().info( + "[防护-调试] 无豁免, 玩家=" + name);*/ + 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; - this.areaDir = new File(plugin.getDataFolder(), "区域防护"); - if (!areaDir.exists()) areaDir.mkdirs(); + + // 统一根目录 + 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(); loadAllAreas(); loadWhitelists(); + recoverPendingEffects(); } + 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(); + + // 获取玩家当前所在区域 + 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, @@ -238,7 +638,7 @@ public class AreaProtection implements Listener { private void writeDefaultConfig() { - File f = new File(areaDir, "末地保护区.txt"); + File f = new File(rootDir, "末地保护区.txt"); if (f.exists()) return; String endWorld = "world_the_end"; @@ -282,29 +682,128 @@ public class AreaProtection implements Listener { public void reload() { loadAllAreas(); loadWhitelists(); - recoverPendingEffects(); - plugin.getLogger().info("[防护] 已重载 " - + areas.size() + " 个区域"); + // 强制输出醒目的中文日志 + plugin.getLogger().info("========================================"); + plugin.getLogger().info("[防护] 已重载 " + areas.size() + " 个区域"); + plugin.getLogger().info("[防护] 已重载白名单"); + plugin.getLogger().info("========================================"); } - // ==================== 加载 ==================== +// ==================== 消息格式化 ==================== + + /** + * 格式化区域消息 + * 1. & → §(颜色符号转换) + * 2.
和 \n → 换行 + * 3. 内容 → §l内容§r(加粗) + * 4. 自动添加 [区域防护] 前缀 + */ + private String formatAreaMsg(String raw) { + if (raw == null || raw.isEmpty()) return ""; + + String msg = raw; + + // & → § + 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", "\n"); + + // HTML → Minecraft格式(栈式解析,支持嵌套) + msg = convertHtmlTags(msg); + + // 前缀 + msg = "§c§l【区域防护】§r " + msg; + + return msg; + } + + /** + * 栈式HTML标签解析 + * 处理 text rest 这样的嵌套标签 + * 内层关闭时重新应用外层格式 + */ + private String convertHtmlTags(String msg) { + StringBuilder sb = new StringBuilder(); + java.util.Deque stack = + new ArrayDeque<>(); + int i = 0; + + while (i < msg.length()) { + 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(); + if (!stack.isEmpty()) { + // 重置后重新应用外层格式 + sb.append("§r"); + for (char c : stack) { + sb.append("§").append(c); + } + } else { + sb.append("§r"); + } + } + 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": return 'l'; + case "i": return 'o'; + case "u": return 'n'; + case "s": return 'm'; + default: return 0; + } + } + +// =============== 加载 ==================== public void loadAllAreas() { - // 验证所有区域的和平白名单 - plugin.getLogger().info("====== 区域解析验证 ======"); - for (Map.Entry entry : areas.entrySet()) { - AreaConfig ac = entry.getValue(); - plugin.getLogger().info("[验证] " + entry.getKey() - + " peaceMode=" + ac.peaceMode - + " peaceWhitelist=" + ac.peaceWhitelist - + " enforceGameMode=" + ac.enforceGameMode - + " modeExempt=" + ac.modeExempt); - } - plugin.getLogger().info("====== 验证结束 ======"); - areas.clear(); - File[] files = areaDir.listFiles( - (d, n) -> n.endsWith(".txt")); + File[] files = rootDir.listFiles( + (File d, String n) -> n.endsWith(".txt")); if (files == null) return; for (File f : files) { try { @@ -314,20 +813,16 @@ public class AreaProtection implements Listener { areas.put(name, ac); } catch (Exception e) { plugin.getLogger().warning( - "[防护] 加载失败: " - + f.getName()); - + "[防护] 加载失败: " + f.getName()); } - } - plugin.getLogger().info( - "[防护] 共加载 " + areas.size() + " 个区域"); } + + private void initDatabase() { try { - File dbFile = new File(plugin.getDataFolder(), "area_effects.db"); dbConnection = DriverManager.getConnection( - "jdbc:sqlite:" + dbFile.getAbsolutePath()); + "jdbc:sqlite:" + newDbFile.getAbsolutePath()); Statement stmt = dbConnection.createStatement(); stmt.executeUpdate( "CREATE TABLE IF NOT EXISTS player_effects (" @@ -338,15 +833,71 @@ public class AreaProtection implements Listener { + "effect_type TEXT NOT NULL," + "effect_level INTEGER DEFAULT 0," + "effect_duration INTEGER DEFAULT 999," - + "enter_time INTEGER NOT NULL" - + ")"); + + "enter_time INTEGER NOT NULL)"); stmt.executeUpdate( "CREATE INDEX IF NOT EXISTS idx_uuid " + "ON player_effects(uuid)"); stmt.close(); - plugin.getLogger().info("[防护] SQLite数据库初始化完成"); } catch (SQLException e) { - plugin.getLogger().severe("[防护] 数据库初始化失败: " + e.getMessage()); + plugin.getLogger().severe( + "[防护] 数据库初始化失败: " + + e.getMessage()); + } + } + + +// ==================== 效果名称中英文映射 ==================== + + /** + * 将中文效果名转为 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; } } @@ -427,9 +978,9 @@ public class AreaProtection implements Listener { for (int i = 0; i < line.length(); i++) { hex.append(String.format("U+%04X ", (int) line.charAt(i))); } - plugin.getLogger().info("[暴力调试] 行=[" + line + /* plugin.getLogger().info("[暴力调试] 行=[" + line + "] 长度=" + line.length() - + " 字符码=" + hex.toString()); + + " 字符码=" + hex.toString());*/ } // ===== 兜底扫描:确保和平白名单一定能解析 ===== if (line.indexOf("白名单") >= 0 @@ -606,14 +1157,55 @@ public class AreaProtection implements Listener { ac.peaceMode = true; plugin.getLogger().info("[解析] 和平模式已识别"); - } else if (line.startsWith("效果")) { + } 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()) { @@ -645,12 +1237,33 @@ public class AreaProtection implements Listener { ac.confiscateMsg = line.substring( "没收提示:".length()).trim(); } else if (line.startsWith("进入提示:")) { - ac.enterMsg = line.substring( - "进入提示:".length()).trim(); + 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("离开提示:")) { - ac.leaveMsg = line.substring( - "离开提示:".length()).trim(); - } 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("强制游戏模式:") @@ -938,127 +1551,58 @@ public class AreaProtection implements Listener { return HOSTILE_TYPES.contains(typeName); } - // ==================== 白/黑名单持久化 ==================== + // ==================== 白名单持久化 ==================== - private File getWhitelistFile(String type, String area) { - String fileName = type + "_" + area + ".txt"; - return new File(areaDir, "_whitelists/" + fileName); - } + /** + * 保存白名单到文件 + * 路径:区域防护/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(); - private void loadWhitelists() { - File wlDir = new File(areaDir, "_whitelists"); - if (!wlDir.exists()) wlDir.mkdirs(); + // 2. 删除旧的区域白名单文件(防止残留) + File[] oldFiles = whitelistDir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.equals("全局白名单.txt")); + if (oldFiles != null) { + for (File f : oldFiles) f.delete(); + } - // 读全局玩家白名单 - File f = new File(wlDir, "global_player.txt"); - if (f.exists()) { - try { - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), + // 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)); - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty()) - globalPlayerWhitelist.add(line.toLowerCase()); + for (String playerName : entry.getValue()) { + pwArea.println(playerName); } - br.close(); - } catch (IOException ignored) { + pwArea.close(); } - } - // 读全局物品黑名单 - f = new File(wlDir, "global_item.txt"); - if (f.exists()) { - try { - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty()) - globalItemBlacklist.add(line.toUpperCase()); - } - br.close(); - } catch (IOException ignored) { - } - } - - // 读区域白名单 - for (String area : areas.keySet()) { - loadAreaWhitelist(area, "player", areaPlayerWhitelist); - loadAreaWhitelist(area, "item", areaItemBlacklist); + plugin.getLogger().info("[防护] 白名单已保存"); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 保存白名单失败: " + + e.getMessage()); } } - private void loadAreaWhitelist(String area, String type, - Map> map) { - File wlDir = new File(areaDir, "_whitelists"); - File f = new File(wlDir, area + "_" + type + ".txt"); - if (!f.exists()) return; - try { - Set set = map.computeIfAbsent( - area, k -> ConcurrentHashMap.newKeySet()); - BufferedReader br = new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty()) set.add(line.toLowerCase()); - } - br.close(); - } catch (IOException ignored) { - } - } - private void saveGlobalWhitelist() { - File wlDir = new File(areaDir, "_whitelists"); - if (!wlDir.exists()) wlDir.mkdirs(); - try { - PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream( - new File(wlDir, "global_player.txt")), - StandardCharsets.UTF_8)); - for (String s : globalPlayerWhitelist) - pw.println(s); - pw.close(); - } catch (IOException ignored) { - } - try { - PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream( - new File(wlDir, "global_item.txt")), - StandardCharsets.UTF_8)); - for (String s : globalItemBlacklist) - pw.println(s); - pw.close(); - } catch (IOException ignored) { - } - } - - private void saveAreaWhitelist(String area, String type, - Set set) { - File wlDir = new File(areaDir, "_whitelists"); - if (!wlDir.exists()) wlDir.mkdirs(); - try { - PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream( - new File(wlDir, - area + "_" + type + ".txt")), - StandardCharsets.UTF_8)); - for (String s : set) pw.println(s); - pw.close(); - } catch (IOException ignored) { - } - } // ==================== 区域检测 ==================== @@ -1179,7 +1723,7 @@ public class AreaProtection implements Listener { playerCurrentArea.clear(); } private void saveModeExempt(AreaConfig ac) { - File file = new File(areaDir, ac.name + ".txt"); + File file = new File(rootDir, ac.name + ".txt"); if (!file.exists()) return; try { @@ -1235,57 +1779,562 @@ public class AreaProtection implements Listener { // ==================== 事件监听 ==================== @EventHandler - public void onPlayerMove(PlayerMoveEvent e) { - Player p = e.getPlayer(); - Location from = e.getFrom(); - Location to = e.getTo(); + public void onPlayerMove(PlayerMoveEvent event) { + Player p = event.getPlayer(); + UUID uid = p.getUniqueId(); + Location to = event.getTo(); if (to == null) return; - // 获取当前位置的区域 - AreaConfig toArea = getArea( - to.getWorld().getName(), - to.getBlockX(), to.getBlockY(), - to.getBlockZ()); + Location from = event.getFrom(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ()) { + return; + } - // 获取上一tick位置的区域 - AreaConfig fromArea = getArea( - from.getWorld().getName(), - from.getBlockX(), from.getBlockY(), - from.getBlockZ()); + // 防抖 + Long lastTime = lastMoveProcess.get(uid); + long now = System.currentTimeMillis(); + if (lastTime != null + && now - lastTime < MOVE_DEBOUNCE_MS) { + return; + } + lastMoveProcess.put(uid, now); - // 用 playerCurrentArea 追踪区域变化 - String toAreaName = (toArea != null) ? toArea.name : null; - String lastAreaName = playerCurrentArea.get(p.getName()); + // 检测区域 + String oldArea = playerCurrentArea.get(uid); + String newArea = null; + String forcedMode = null; - if (toAreaName != null) { - if (!toAreaName.equals(lastAreaName)) { - // 进入了新区域 - // 先离开旧区域 - if (lastAreaName != null) { - AreaConfig oldAc = areas.get(lastAreaName); - if (oldAc != null) { - handleLeave(p, oldAc); + 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) { + // ★ 每次移动都清除指定效果 + clearBadEffects(p, ac); + + // ★ 每次移动都强制游戏模式 + if (ac.enforceGameMode != null + && !ac.enforceGameMode.isEmpty() + && !isExemptFromGameMode(p, newArea)) { + GameMode target = + GameMode.valueOf(ac.enforceGameMode); + if (p.getGameMode() != target) { + p.setGameMode(target); + } } } - // 进入新区域 - handleEnter(p, toArea); - playerCurrentArea.put(p.getName(), toAreaName); } - // 在同一区域内 → 调用 handleInside(持续检查) - handleInside(p, toArea); + return; + } + + // 离开旧区域 + if (oldArea != null) { + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★开始离开处理: + + oldArea);*/ + + AreaConfig oldAc = areas.get(oldArea); + + // 离开提示 + if (oldAc != null && oldAc.leaveMsg != null + && !oldAc.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(oldAc.leaveMsg)); + } + + // 收标 + 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 (marked != null && !marked.isEmpty()) { + String clearArea = oldArea; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + /* plugin.getLogger().info( + "[防护-移动-离开] ★★★创建延时任务: " + + toClear.size() + "个");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-移动-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); + + plugin.getLogger().info( + "[防护-移动-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size()); + + if (online == null || !online.isOnline()) { + removePlayerEffects( + clearUid, clearArea); + return; + } + + int cleared = 0; + for (PotionEffectType type : toClear) { + PotionEffect eff = + online.getPotionEffect(type); + /* plugin.getLogger().info( + "[防护-移动-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-移动-清理] + 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) { + if (newAc.enterMsg != null + && !newAc.enterMsg.isEmpty()) { + p.sendMessage( + formatAreaMsg(newAc.enterMsg)); + } + 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 { - // 不在任何区域内 - if (lastAreaName != null) { - // 离开了区域 - AreaConfig oldAc = areas.get(lastAreaName); - if (oldAc != null) { - handleLeave(p, oldAc); + playerCurrentArea.remove(uid); + } + } +// ===== 清理袭击生物 ===== + + // 需要清理的袭击生物类型 + 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); } - playerCurrentArea.remove(p.getName()); } } } + + /** + * 进入区域时发放效果 + * 流程:查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 || ac.giveEffects.isEmpty()) return; + if (ac.giveEffects.isEmpty()) return; + // 查DB:该玩家在此区域是否已有效果记录 + List dbEffects = + getPlayerEffectNames(uid, areaName); + + if (dbEffects.isEmpty()) { + // 无记录 → 写入DB + for (String[] eff : ac.giveEffects) { + 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); + } + + // 给效果 + 贴标 + List applied = new ArrayList<>(); + for (String[] eff : ac.giveEffects) { + try { + PotionEffectType type = + resolveEffectType(eff[0]); + if (type == null) continue; + int lv = Integer.parseInt(eff[1]) - 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) { + } + } + + // 贴标 + // 新代码(合并,不覆盖): + 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());*/ + } + } + + /** + * 离开区域时,延时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); + } + } + + // 确认清空后清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) { + for (String[] eff : newAc.giveEffects) { + 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) { + String name = p.getName(); + + if (globalPlayerWhitelist.contains(name)) + return true; + + Set areaWl = + areaPlayerWhitelist.get(areaName); + if (areaWl != null && areaWl.contains(name)) + return true; + + AreaConfig ac = areas.get(areaName); + if (ac != null && ac.modeExempt.contains(name)) + return true; + + return false; + } + + private String getChineseGameMode(GameMode mode) { switch (mode) { case SURVIVAL: return "生存模式"; @@ -1299,13 +2348,6 @@ public class AreaProtection implements Listener { private void handleEnter(Player p, AreaConfig ac) { UUID uid = p.getUniqueId(); - // 进入提示 - if (ac.enterMsg != null && !ac.enterMsg.isEmpty()) { - p.sendTitle("§c§l" + ac.name, - "§e" + ac.enterMsg, 10, 60, 20); - p.sendMessage("§c§l[区域防护] §f" + ac.enterMsg); - } - // 清除负面效果 clearBadEffects(p, ac); @@ -1329,7 +2371,7 @@ public class AreaProtection implements Listener { removePlayerEffects(uid, ac.name); if (!given.isEmpty()) { - savePlayerEffects(uid, p.getName(), ac.name, given); + // savePlayerEffects(uid, p.getName(), ac.name, given); } // 白名单检查(最先执行) @@ -1452,95 +2494,96 @@ public class AreaProtection implements Listener { switch (clean) { // 负面 case "缓慢": - return PotionEffectType.getByName("slowness"); + return resolveEffectType("slowness"); case "挖掘疲劳": - return PotionEffectType.getByName("mining_fatigue"); + return resolveEffectType("mining_fatigue"); case "瞬间伤害": - return PotionEffectType.getByName("instant_damage"); + return resolveEffectType("instant_damage"); case "反胃": - return PotionEffectType.getByName("nausea"); + return resolveEffectType("nausea"); case "失明": - return PotionEffectType.getByName("blindness"); + return resolveEffectType("blindness"); case "饥饿": - return PotionEffectType.getByName("hunger"); + return resolveEffectType("hunger"); case "虚弱": - return PotionEffectType.getByName("weakness"); + return resolveEffectType("weakness"); case "中毒": - return PotionEffectType.getByName("poison"); + return resolveEffectType("poison"); case "凋零": - return PotionEffectType.getByName("wither"); + return resolveEffectType("wither"); case "飘浮": - return PotionEffectType.getByName("levitation"); + return resolveEffectType("levitation"); case "霉运": - return PotionEffectType.getByName("unluck"); + return resolveEffectType("unluck"); case "黑暗": - return PotionEffectType.getByName("darkness"); + return resolveEffectType("darkness"); case "蓄风": - return PotionEffectType.getByName("wind_charged"); + return resolveEffectType("wind_charged"); case "盘丝": - return PotionEffectType.getByName("weaving"); + return resolveEffectType("weaving"); case "渗浆": - return PotionEffectType.getByName("oozing"); + return resolveEffectType("oozing"); case "寄生": - return PotionEffectType.getByName("infested"); + return resolveEffectType("infested"); // 中性 case "不祥之兆": case "不祥征兆": - return PotionEffectType.getByName("bad_omen"); + return resolveEffectType("bad_omen"); case "袭击之兆": case "袭击征兆": - return PotionEffectType.getByName("raid_omen"); + return resolveEffectType("raid_omen"); case "试炼之兆": case "试炼征兆": - return PotionEffectType.getByName("trial_omen"); + return resolveEffectType("trial_omen"); // 正面 case "迅捷": - return PotionEffectType.getByName("speed"); + return resolveEffectType("speed"); case "急迫": - return PotionEffectType.getByName("haste"); + return resolveEffectType("haste"); case "力量": - return PotionEffectType.getByName("strength"); + return resolveEffectType("strength"); case "瞬间治疗": - return PotionEffectType.getByName("instant_health"); + return resolveEffectType("instant_health"); case "跳跃提升": - return PotionEffectType.getByName("jump_boost"); + return resolveEffectType("jump_boost"); case "生命恢复": - return PotionEffectType.getByName("regeneration"); + return resolveEffectType("regeneration"); case "抗性提升": - return PotionEffectType.getByName("resistance"); + return resolveEffectType("resistance"); case "抗火": - return PotionEffectType.getByName("fire_resistance"); + return resolveEffectType("fire_resistance"); case "水下呼吸": - return PotionEffectType.getByName("water_breathing"); + return resolveEffectType("water_breathing"); case "隐身": - return PotionEffectType.getByName("invisibility"); + return resolveEffectType("invisibility"); case "夜视": - return PotionEffectType.getByName("night_vision"); + return resolveEffectType("night_vision"); case "发光": - return PotionEffectType.getByName("glowing"); + return resolveEffectType("glowing"); case "生命提升": - return PotionEffectType.getByName("health_boost"); + return resolveEffectType("health_boost"); case "伤害吸收": - return PotionEffectType.getByName("absorption"); + return resolveEffectType("absorption"); case "饱和": - return PotionEffectType.getByName("saturation"); + return resolveEffectType("saturation"); case "幸运": - return PotionEffectType.getByName("luck"); + return resolveEffectType("luck"); case "村庄英雄": - return PotionEffectType.getByName("hero_of_the_village"); + return resolveEffectType("hero_of_the_village"); case "缓降": - return PotionEffectType.getByName("slow_falling"); + return resolveEffectType("slow_falling"); case "潮涌能量": - return PotionEffectType.getByName("conduit_power"); + return resolveEffectType("conduit_power"); case "海豚的恩惠": - return PotionEffectType.getByName("dolphins_grace"); + return resolveEffectType("dolphins_grace"); case "致命中毒": - return PotionEffectType.getByName("poison"); + return resolveEffectType("poison"); default: break; } - // 兜底:直接用原版英文ID查 + +// 兜底:直接用原版英文ID查(★ 注意是 PotionEffectType.getByName,不是 resolveEffectType) PotionEffectType fallback = PotionEffectType.getByName(clean); if (fallback != null) return fallback; fallback = PotionEffectType.getByName(clean.toLowerCase()); @@ -1548,6 +2591,7 @@ public class AreaProtection implements Listener { plugin.getLogger().warning("[防护] 无法识别效果: " + name); return null; + } @@ -1576,94 +2620,9 @@ public class AreaProtection implements Listener { } } - private void handleLeave(Player p, AreaConfig ac) { - UUID uid = p.getUniqueId(); - - if (ac.leaveMsg != null && !ac.leaveMsg.isEmpty()) { - p.sendTitle("§a§l离开区域", "§e" + ac.leaveMsg, 10, 40, 10); - p.sendMessage("§a§l[区域防护] §f" + ac.leaveMsg); - } - - List given = getPlayerEffects(uid, ac.name); - - plugin.getLogger().info("[防护] " + p.getName() - + " 离开" + ac.name - + " DB效果数=" + given.size()); - - if (!given.isEmpty()) { - lastLeaveTime.put(uid, System.currentTimeMillis()); - pendingClear.put(uid, new ArrayList<>(given)); - plugin.getLogger().info("[防护] 已记录待清除 lastLeaveTime=" - + lastLeaveTime.containsKey(uid) - + " pendingClear数量=" - + pendingClear.get(uid).size()); - } - - cancelBorder(p); - } - - public void recoverPendingEffects() { - List pending = getAllPendingEffects(); - if (pending.isEmpty()) return; - - plugin.getLogger().info("[防护] 恢复" - + pending.size() + "条未处理的效果记录"); - - for (String[] row : pending) { - String uuidStr = row[0]; - String playerName = row[1]; - String areaName = row[2]; - String effectName = row[3]; - - UUID uid = UUID.fromString(uuidStr); - Player p = Bukkit.getPlayer(uid); - - // 玩家不在线,直接清理DB - if (p == null || !p.isOnline()) { - plugin.getLogger().info("[防护] " - + playerName + "不在线,清理DB记录"); - removePlayerEffects(uid, areaName); - continue; - } - - // 玩家在线,检查当前是否还在该区域 - AreaConfig ac = areas.get(areaName); - if (ac == null) { - removePlayerEffects(uid, areaName); - continue; - } - - AreaConfig currentArea = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - - if (currentArea == null - || !currentArea.name.equals(areaName)) { - // 不在原来区域了,清除效果 - PotionEffectType type = PotionEffectType - .getByName(effectName); - if (type != null) { - p.removePotionEffect(type); - plugin.getLogger().info("[防护] " - + playerName + " 重连后清除: " - + effectName); - } - removePlayerEffects(uid, areaName); - } else { - // 还在原来区域,重新给予效果 - PotionEffectType type = PotionEffectType - .getByName(effectName); - if (type != null) { - p.addPotionEffect(new PotionEffect( - type, 999 * 20, 0)); - plugin.getLogger().info("[防护] " - + playerName + " 重连后重新给予: " - + effectName); - } - } - } + public Map> + getPendingClearEffects() { + return pendingClearEffects; } public void onPlayerOffline(UUID uid, String playerName, @@ -1679,86 +2638,28 @@ public class AreaProtection implements Listener { } // 清除DB记录 removePlayerEffects(uid, areaName); - lastLeaveTime.remove(uid); - pendingClear.remove(uid); plugin.getLogger().info("[防护] " + playerName + " 下线,已清除" + given.size() + "个效果并清理DB"); } - - public void checkPendingClears() { - long now = System.currentTimeMillis(); - Iterator> it = - lastLeaveTime.entrySet().iterator(); - /*plugin.getLogger().info("[防护] checkPendingClears执行" - + " 待处理=" + lastLeaveTime.size());*/ - while (it.hasNext()) { - Map.Entry entry = it.next(); - UUID uid = entry.getKey(); - long leaveTime = entry.getValue(); - - if (now - leaveTime < 15000) continue; - - Player p = Bukkit.getPlayer(uid); - if (p == null || !p.isOnline()) { - it.remove(); - pendingClear.remove(uid); - continue; - } - - List toClear = pendingClear.get(uid); - if (toClear == null || toClear.isEmpty()) { - it.remove(); - continue; - } - - AreaConfig currentArea = getArea( - p.getWorld().getName(), - p.getLocation().getBlockX(), - p.getLocation().getBlockY(), - p.getLocation().getBlockZ()); - - if (currentArea == null) { - // 不在任何区域:只清除DB记录的效果,不动玩家其他效果 - for (PotionEffectType type : toClear) { - p.removePotionEffect(type); - } - removeAllPlayerEffects(uid); - plugin.getLogger().info("[防护] " + p.getName() - + " 全清效果: " + toClear); - } else { - // 在新区域:只清新区域没有的效果 - List newGiven = - getPlayerEffects(uid, currentArea.name); - if (newGiven == null) newGiven = new ArrayList<>(); - - for (PotionEffectType type : toClear) { - if (!newGiven.contains(type)) { - p.removePotionEffect(type); - } - } - // 清除旧区域DB记录 - removePlayerEffects(uid,""); - plugin.getLogger().info("[防护] " + p.getName() - + " 在新区域, 清除旧效果"); - } - - it.remove(); - pendingClear.remove(uid); - } - } - @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(), @@ -1769,45 +2670,219 @@ public class AreaProtection implements Listener { to.getBlockX(), to.getBlockY(), to.getBlockZ()); - // 从区域内传送到区域外 - if (fromArea != null && toArea == null) { - handleLeave(p, fromArea); - } - // 从区域外传送到区域内 - else if (fromArea == null && toArea != null) { - // plugin.getLogger().info("[防护] 触发handleEnter"); - handleEnter(p, toArea); + 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; } - // 从一个区域传送到另一个区域 - else if (fromArea != null && toArea != null - && !fromArea.name.equals(toArea.name)) { - handleLeave(p, fromArea); - handleEnter(p, toArea); - } - } + // ===== 离开旧区域 ===== + if (fromName != null) { + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★开始离开处理: " + + fromName);*/ - public void handleInside(Player p, AreaConfig ac) { - clearBadEffects(p, ac); + // 收标 + List marked = + playerMarkedEffects.remove(uid); - // 和平模式 - if (ac.peaceMode) { - banHostilesWithWhitelist(p, ac); - } else if (ac.denyRaid) { - banRaidMobs(p, ac); + /* 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); + } + + // 创建5秒延时清理 + if (marked != null && !marked.isEmpty()) { + String clearArea = fromName; + UUID clearUid = uid; + List toClear = + new ArrayList<>(marked); + pendingClearArea.put(uid, clearArea); + pendingClearEffects.put(uid, toClear); + + /* plugin.getLogger().info( + "[防护-传送-离开] ★★★创建延时任务: " + + toClear.size() + "个效果");*/ + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + /* plugin.getLogger().info( + "[防护-传送-清理] ★★★延时触发!");*/ + pendingClearTask.remove(clearUid); + pendingClearArea.remove(clearUid); + + Player online = + Bukkit.getPlayer(clearUid); +/* + plugin.getLogger().info( + "[防护-传送-清理] 在线=" + + (online != null && online.isOnline()) + + " 效果数=" + toClear.size() + + " 区域=" + clearArea);*/ + + 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); + /* plugin.getLogger().info( + "[防护-传送-清理] 检查: " + + type.getName() + + " 身上=" + (eff != null));*/ + if (eff != null) { + online.removePotionEffect(type); + cleared++; + } + } + removePlayerEffects(clearUid, clearArea); + plugin.getLogger().info( + "[防护-传送-清理] ★★★完成: 清除" + + cleared + "/" + toClear.size()); + } + }.runTaskLater(plugin, 100L); // 5秒 = 100 tick + pendingClearTask.put(uid, task); + + } + + // 离开提示 + if (fromArea != null && fromArea.leaveMsg != null + && !fromArea.leaveMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(fromArea.leaveMsg)); + } } - // 游戏模式:每次都强制切换(排除全白/区白/豁免/OP) - if (ac.enforceGameMode != null) { - if (!isPlayerExemptFromModeChange(p, ac)) { - GameMode target = GameMode.valueOf(ac.enforceGameMode); + // ===== 进入新区域 ===== + 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); + } + } + + // 进入提示 + if (toArea != null && toArea.enterMsg != null + && !toArea.enterMsg.isEmpty()) { + p.sendMessage(formatAreaMsg(toArea.enterMsg)); + } + + // 发放效果 + 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 (isPlayerWhitelisted(p.getName(), ac)) return; + handleConfiscate(p, ac); } @@ -1839,6 +2914,16 @@ public class AreaProtection implements Listener { return areas.keySet(); } + /** + * 获取玩家当前所在区域名 + */ + public String getPlayerArea(Player p) { + return playerCurrentArea.get(p.getUniqueId()); + } + + public Collection getPendingTasks() { + return pendingClearTask.values(); + } // 判断是否为远程攻击敌对生物 @@ -2157,6 +3242,70 @@ public class AreaProtection implements Listener { it.remove(); } } + public void loadWhitelists() { + globalPlayerWhitelist.clear(); + areaPlayerWhitelist.clear(); + + 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("#")) { + 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("#")) { + set.add(name); + } + } + if (!set.isEmpty()) { + areaPlayerWhitelist.put( + areaName, set); + } + } + } + + plugin.getLogger().info( + "[防护] 白名单加载完成,全局: " + + globalPlayerWhitelist.size() + + ",区域数: " + + areaPlayerWhitelist.size()); + } catch (Exception e) { + plugin.getLogger().warning( + "[防护] 加载白名单失败: " + + e.getMessage()); + } + } @EventHandler @@ -2302,7 +3451,6 @@ public class AreaProtection implements Listener { } // ==================== 管理命令 ==================== - public boolean handleCommand(CommandSender sender, String[] args) { args = smartReorderArgs(args); @@ -2311,8 +3459,76 @@ public class AreaProtection implements Listener { return true; } String sub = args[0].toLowerCase(); +// ===== 诊断命令 ===== + 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仅玩家可用"); @@ -2323,17 +3539,14 @@ public class AreaProtection implements Listener { ItemMeta meta = tool.getItemMeta(); meta.setDisplayName("§a§l区域选择工具"); meta.setLore(Arrays.asList( - "§7左键: 位置1", - "§7右键: 位置2")); + "§7左键: 位置1", "§7右键: 位置2")); tool.setItemMeta(meta); p.getInventory().addItem(tool); - p.sendMessage("§a§l[防护] §f已获取选择工具(旋风棒)"); - p.sendMessage("§7左键方块=位置1,右键方块=位置2"); + p.sendMessage("§a§l[防护] §f已获取选择工具"); return true; } - - // 创建 + // ===== 创建 ===== if (sub.equals("创建") || sub.equals("create")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); @@ -2352,7 +3565,7 @@ public class AreaProtection implements Listener { return true; } String areaName = args[1]; - File f = new File(areaDir, areaName + ".txt"); + File f = new File(rootDir, areaName + ".txt"); if (f.exists()) { p.sendMessage("§c区域已存在"); return true; @@ -2373,217 +3586,194 @@ public class AreaProtection implements Listener { + "," + l2.getBlockZ()); pw.println("高度范围: 0-255"); pw.println(); - pw.println("# 没收物品: 蘑菇,毒蘑菇"); pw.println("# 禁止放置方块"); pw.println("# 禁止破坏方块"); pw.println("# 禁止PVP"); - pw.println("# 禁止摔伤"); - pw.println("# 禁止饥饿"); - pw.println("# 禁止一切伤害"); - pw.println("# 禁止丢弃物品"); - pw.println("# 禁止末影珍珠"); - pw.println("# 禁止使用弓箭"); - pw.println("# 禁止骑乘"); - pw.println("# 禁止袭击"); - pw.println("# 禁止药水"); - pw.println("# 禁止爆炸"); - pw.println("# 禁止使用物品: 蘑菇,毒蘑菇"); pw.println("# 效果: 夜视 1 999"); pw.println("# 进入提示: 欢迎来到保护区"); pw.println("# 离开提示: 已离开保护区"); - pw.println("# 没收提示: 你的违禁品已被没收"); - pw.println("# 通报批评: {player} 在保护区违规!"); - pw.println("# 惩罚命令: ban {player} 60s 违规"); pw.close(); - p.sendMessage("§a§l[防护] §f区域 " + areaName + " 已创建"); + p.sendMessage("§a§l[防护] §f区域 " + + areaName + " 已创建"); loadAllAreas(); } catch (IOException ex) { p.sendMessage("§c创建失败"); } return true; } -// ===== 和平白名单:addname / removename ===== + + // ===== 和平白名单 ===== if (sub.equals("addname")) { if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别: " + + args[1] + " 或 " + args[2]); return true; } - String areaName = parsed[0]; - String creatureName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(parsed[0]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - if (ac.peaceWhitelist.contains(creatureName)) { - sender.sendMessage("§e" + creatureName + " 已在 " + areaName + " 和平白名单中"); - return true; - } - ac.peaceWhitelist.add(creatureName); - savePeaceWhitelist(ac); - sender.sendMessage("§a已添加: " + creatureName + " → " + areaName + " 和平白名单"); + ac.peaceWhitelist.add(parsed[1]); + saveAreaConfig(ac); + sender.sendMessage("§a已添加: " + + parsed[1] + " → " + + parsed[0] + " 和平白名单"); return true; } - sender.sendMessage("§c用法: /protect addname <区域/名字> <区域/名字>"); + sender.sendMessage("§c用法: /protect addname " + + "<区域> <名字>"); return true; } - - if (sub.equals("removename")) { if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别"); return true; } - String areaName = parsed[0]; - String creatureName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(parsed[0]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - if (!ac.peaceWhitelist.contains(creatureName)) { - sender.sendMessage("§e" + creatureName + " 不在 " + areaName + " 和平白名单中"); - return true; - } - ac.peaceWhitelist.remove(creatureName); - savePeaceWhitelist(ac); - sender.sendMessage("§a已移除: " + creatureName + " ← " + areaName + " 和平白名单"); + ac.peaceWhitelist.remove(parsed[1]); + saveAreaConfig(ac); + sender.sendMessage("§a已移除: " + + parsed[1] + " ← " + parsed[0]); return true; } - sender.sendMessage("§c用法: /protect removename <区域/名字> <区域/名字>"); + sender.sendMessage("§c用法: /protect removename " + + "<区域> <名字>"); return true; } - - if (sub.equals("listname")) { if (args.length < 2) { - sender.sendMessage("§c用法: /protect listname <区域>"); + sender.sendMessage( + "§c用法: /protect listname <区域>"); return true; } - String areaName = args[1]; - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(args[1]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - sender.sendMessage("§a" + areaName + " 和平白名单:"); + sender.sendMessage("§a" + args[1] + + " 和平白名单:"); if (ac.peaceWhitelist.isEmpty()) { sender.sendMessage("§7 (空)"); } else { - for (String name : ac.peaceWhitelist) { - sender.sendMessage("§7 - " + name); + for (String n : ac.peaceWhitelist) { + sender.sendMessage("§7 - " + n); } } return true; } -// ===== 模式排除:addwhite / removewhite ===== + // ===== 模式排除 ===== if (sub.equals("addwhite")) { if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别"); return true; } - String areaName = parsed[0]; - String playerName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(parsed[0]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - Set global = globalWhitelist.get("global"); - if (global != null && global.contains(playerName.toLowerCase())) { - sender.sendMessage("§c" + playerName + " 已在全局白名单中,不需要添加到模式排除"); - return true; - } - Set areaWl = areaWhitelist.get(areaName); - if (areaWl != null && areaWl.contains(playerName.toLowerCase())) { - sender.sendMessage("§c" + playerName + " 已在" + areaName + "白名单中,不需要添加到模式排除"); - return true; - } - if (ac.modeExempt.contains(playerName)) { - sender.sendMessage("§c" + playerName + " 已在" + areaName + "模式排除名单中"); - return true; - } - ac.modeExempt.add(playerName); - saveModeExempt(ac); - sender.sendMessage("§a已添加: " + playerName + " → " + areaName + " 模式排除名单"); + ac.modeExempt.add(parsed[1]); + saveAreaConfig(ac); + sender.sendMessage("§a已添加: " + + parsed[1] + " → " + + parsed[0] + " 模式排除"); return true; } - sender.sendMessage("§c用法: /protect addwhite <区域/玩家> <区域/玩家>"); + sender.sendMessage("§c用法: /protect addwhite " + + "<区域> <玩家>"); return true; } - if (sub.equals("removewhite")) { if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别"); return true; } - String areaName = parsed[0]; - String playerName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(parsed[0]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - if (!ac.modeExempt.contains(playerName)) { - sender.sendMessage("§c" + playerName + " 不在 " + areaName + " 模式排除名单中"); - return true; - } - ac.modeExempt.remove(playerName); - saveModeExempt(ac); - sender.sendMessage("§a已移除: " + playerName + " ← " + areaName + " 模式排除名单"); + ac.modeExempt.remove(parsed[1]); + saveAreaConfig(ac); + sender.sendMessage("§a已移除: " + + parsed[1] + " ← " + + parsed[0] + " 模式排除"); return true; } - sender.sendMessage("§c用法: /protect removewhite <区域/玩家> <区域/玩家>"); + sender.sendMessage("§c用法: /protect removewhite " + + "<区域> <玩家>"); return true; } - if (sub.equals("listwhite")) { if (args.length < 2) { - sender.sendMessage("§c用法: /protect listwhite <区域>"); + sender.sendMessage( + "§c用法: /protect listwhite <区域>"); return true; } - String areaName = args[1]; - AreaConfig ac = areas.get(areaName); + AreaConfig ac = areas.get(args[1]); if (ac == null) { - sender.sendMessage("§c区域不存在: " + areaName); + sender.sendMessage("§c区域不存在"); return true; } - sender.sendMessage("§a" + areaName + " 模式排除名单:"); + sender.sendMessage("§a" + args[1] + + " 模式排除名单:"); if (ac.modeExempt.isEmpty()) { sender.sendMessage("§7 (空)"); } else { - for (String name : ac.modeExempt) { - sender.sendMessage("§7 - " + name); + 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 { @@ -2592,91 +3782,151 @@ public class AreaProtection implements Listener { : areas.entrySet()) { sender.sendMessage("§a " + en.getKey() + " §7规则:" - + en.getValue().ruleCount() + "条"); + + en.getValue().ruleCount() + + "条"); } } return true; } - // 重载 + // ===== 重载 ===== if (sub.equals("重载") || sub.equals("reload")) { reload(); - sender.sendMessage("§a已重载 " + areas.size() + " 个区域"); + sender.sendMessage("§a已重载 " + + areas.size() + " 个区域"); return true; } -// ===== list:列出白名单 ===== - if (sub.equals("list")) { + + // ===== 物品黑名单 ===== + if (sub.equals("additem")) { if (args.length == 2) { - String areaName = resolveAreaName(args[1]); - if (areaName == null || areaName.equalsIgnoreCase("global")) { - sender.sendMessage("§a全局白名单:"); - Set wl = globalWhitelist.get("global"); - if (wl == null || wl.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String name : wl) { - sender.sendMessage("§7 - " + name); - } - } + String itemName = args[1].toUpperCase(); + if (Material.matchMaterial(itemName) + == null) { + sender.sendMessage( + "§c无效物品ID: " + args[1]); return true; } - if (!areaName.equals(args[1])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - Set wl = areaWhitelist.get(areaName); - sender.sendMessage("§a" + areaName + " 白名单:"); - if (wl == null || wl.isEmpty()) { - sender.sendMessage("§7 (空)"); + globalItemBlacklist.add(itemName); + sender.sendMessage("§a已全局加黑: " + + itemName); + return true; + } + if (args.length == 3) { + String r1 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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 { - for (String name : wl) { - sender.sendMessage("§7 - " + name); - } + areaName = "global"; + itemName = args[1].toUpperCase(); + } + 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("§a全局白名单:"); - Set wl = globalWhitelist.get("global"); - if (wl == null || wl.isEmpty()) { - sender.sendMessage("§7 (空)"); - } else { - for (String name : wl) { - sender.sendMessage("§7 - " + name); + sender.sendMessage("§c用法: /protect additem " + + "<区域/物品> <区域/物品>"); + return true; + } + if (sub.equals("removeitem")) { + if (args.length == 2) { + 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 = resolveAreaName(args[1]); + String r2 = resolveAreaName(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(); + } + 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; } - - -// ===== listitem:列出黑名单物品 ===== if (sub.equals("listitem")) { if (args.length < 2) { - // 全局黑名单 - sender.sendMessage("§a§l===== 全局物品黑名单 ====="); + sender.sendMessage( + "§a全局物品黑名单:"); if (globalItemBlacklist.isEmpty()) { sender.sendMessage("§7(空)"); } else { - for (String item : globalItemBlacklist) { + for (String item + : globalItemBlacklist) { sender.sendMessage("§f- " + item); } } return true; } - // 区域黑名单 - String area = args[1]; - if (!validateArea(sender, area)) return true; - Set list = areaItemBlacklist.get(area); - sender.sendMessage("§a§l===== " + area + " 物品黑名单 ====="); + Set list = + areaItemBlacklist.get(args[1]); + sender.sendMessage("§a" + args[1] + + " 物品黑名单:"); if (list == null || list.isEmpty()) { sender.sendMessage("§7(空)"); - return true; - } - for (String item : list) { - sender.sendMessage("§f- " + item); + } else { + for (String item : list) { + sender.sendMessage("§f- " + item); + } } return true; } -// ===== expand 扩大选区 ===== + // ===== expand 扩建 ===== if (sub.equals("expand")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); @@ -2698,40 +3948,30 @@ public class AreaProtection implements Listener { 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); - switch (dir) { - case "北": minZ -= amount; break; - case "南": maxZ += amount; break; - case "东": maxX += amount; break; - case "西": minX -= amount; break; - } - - // 写回配置文件 + if (dir.equals("北")) minZ -= amount; + else if (dir.equals("南")) maxZ += amount; + else if (dir.equals("东")) maxX += amount; + else if (dir.equals("西")) minX -= amount; ac.x1 = minX; ac.x2 = maxX; ac.z1 = minZ; ac.z2 = maxZ; saveAreaConfig(ac); - p.sendMessage("§a§l[防护] §f区域 §e" + ac.name - + " §f已向 §e" + dir + " §f扩建 §e" + amount + "§f格"); - p.sendMessage("§7新范围: X(" + minX + "~" + maxX - + ") Z(" + minZ + "~" + maxZ + ")"); + + " §f向 §e" + dir + " §f扩建 §e" + + amount + "§f格"); return true; } - -// ===== contraction 收缩选区 ===== - if (sub.equals("contraction") || sub.equals("contract")) { + // ===== contract 收缩 ===== + if (sub.equals("contraction") + || sub.equals("contract")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; @@ -2752,46 +3992,39 @@ public class AreaProtection implements Listener { 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); - switch (dir) { - case "北": minZ += amount; break; - case "南": maxZ -= amount; break; - case "东": maxX -= amount; break; - case "西": minX += amount; break; - } - + 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收缩过度,区域已无效"); + p.sendMessage("§c收缩过度"); return true; } - ac.x1 = minX; ac.x2 = maxX; ac.z1 = minZ; ac.z2 = maxZ; saveAreaConfig(ac); - p.sendMessage("§a§l[防护] §f区域 §e" + ac.name - + " §f已向 §e" + dir + " §f收缩 §e" + amount + "§f格"); - p.sendMessage("§7新范围: X(" + minX + "~" + maxX - + ") Z(" + minZ + "~" + maxZ + ")"); + + " §f向 §e" + dir + " §f收缩 §e" + + amount + "§f格"); return true; } - // 删除 + // ===== 删除 ===== if (sub.equals("删除") || sub.equals("delete")) { if (args.length < 2) { - sender.sendMessage("§e用法: /protect 删除 <名>"); + sender.sendMessage( + "§e用法: /protect 删除 <名>"); return true; } - File f = new File(areaDir, args[1] + ".txt"); + File f = new File(rootDir, args[1] + ".txt"); if (f.exists()) { f.delete(); areas.remove(args[1]); @@ -2802,263 +4035,98 @@ public class AreaProtection implements Listener { return true; } - // ===== add 玩家白名单 ===== + // ===== add 加白名单 ===== if (sub.equals("add")) { if (args.length == 2) { - String playerName = args[1]; - if (!playerExists(playerName)) { - sender.sendMessage("§c玩家不存在: " + playerName); + String name = args[1]; + if (globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 已在全局白名单中"); return true; } - Set wl = globalWhitelist - .computeIfAbsent("global", k -> new HashSet<>()); - if (wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 已在全局白名单中"); - return true; - } - wl.add(playerName.toLowerCase()); - saveGlobalWhitelist(); - sender.sendMessage("§a已全局加白: " + playerName); + globalPlayerWhitelist.add(name); + saveWhitelists(); + sender.sendMessage("§a已全局加白: " + name); return true; } if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别"); return true; } String areaName = parsed[0]; String playerName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - if (!playerExists(playerName)) { - sender.sendMessage("§c玩家不存在: " + playerName); - return true; - } if (areaName.equalsIgnoreCase("global")) { - Set wl = globalWhitelist - .computeIfAbsent("global", k -> new HashSet<>()); - if (wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 已在全局白名单中"); - return true; - } - wl.add(playerName.toLowerCase()); - saveGlobalWhitelist(); - sender.sendMessage("§a已全局加白: " + playerName); + globalPlayerWhitelist.add(playerName); + saveWhitelists(); + sender.sendMessage("§a已全局加白: " + + playerName); } else { - Set wl = areaWhitelist - .computeIfAbsent(areaName, k -> new HashSet<>()); - if (wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 已在 " + areaName + " 白名单中"); - return true; - } - wl.add(playerName.toLowerCase()); - saveAreaWhitelist(areaName, "whitelist", wl); - sender.sendMessage("§a已加白: " + playerName + " → " + areaName); + Set wl = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap + .newKeySet()); + wl.add(playerName); + saveWhitelists(); + sender.sendMessage("§a已加白: " + + playerName + " → " + areaName); } return true; } - sender.sendMessage("§c用法: /protect add <区域/玩家> <区域/玩家>"); + sender.sendMessage("§c用法: /protect add " + + "<区域/玩家> <区域/玩家>"); return true; } -// remove 命令 + // ===== remove 移除白名单 ===== if (sub.equals("remove")) { if (args.length == 2) { - String playerName = args[1]; - Set wl = globalWhitelist.get("global"); - if (wl == null || !wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 不在全局白名单中"); + String name = args[1]; + if (!globalPlayerWhitelist.contains(name)) { + sender.sendMessage("§e" + name + + " 不在全局白名单中"); return true; } - wl.remove(playerName.toLowerCase()); - saveGlobalWhitelist(); - sender.sendMessage("§a已从全局白名单移除: " + playerName); + globalPlayerWhitelist.remove(name); + saveWhitelists(); + sender.sendMessage("§a已从全局白名单移除: " + + name); return true; } if (args.length == 3) { - String[] parsed = parseAreaAndTarget(args[1], args[2]); + String[] parsed = parseAreaAndTarget( + args[1], args[2]); if (parsed == null) { - sender.sendMessage("§c无法识别区域名: " + args[1] + " 或 " + args[2]); + sender.sendMessage("§c无法识别"); return true; } String areaName = parsed[0]; String playerName = parsed[1]; - if (!areaName.equals(args[1]) && !areaName.equals(args[2])) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } if (areaName.equalsIgnoreCase("global")) { - Set wl = globalWhitelist.get("global"); - if (wl == null || !wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 不在全局白名单中"); - return true; - } - wl.remove(playerName.toLowerCase()); - saveGlobalWhitelist(); - sender.sendMessage("§a已从全局白名单移除: " + playerName); + globalPlayerWhitelist.remove(playerName); + saveWhitelists(); + sender.sendMessage("§a已移除: " + + playerName); } else { - Set wl = areaWhitelist.get(areaName); - if (wl == null || !wl.contains(playerName.toLowerCase())) { - sender.sendMessage("§e" + playerName + " 不在 " + areaName + " 白名单中"); - return true; + Set wl = + areaPlayerWhitelist.get(areaName); + if (wl != null) { + wl.remove(playerName); } - wl.remove(playerName.toLowerCase()); - saveAreaWhitelist(areaName, "whitelist", wl); - sender.sendMessage("§a已从 " + areaName + " 白名单移除: " + playerName); + saveWhitelists(); + sender.sendMessage("§a已从 " + areaName + + " 移除: " + playerName); } return true; } - sender.sendMessage("§c用法: /protect remove <区域/玩家> <区域/玩家>"); + sender.sendMessage("§c用法: /protect remove " + + "<区域/玩家> <区域/玩家>"); return true; } - - if (sub.equals("additem")) { - if (args.length == 2) { - String itemName = args[1].toUpperCase(); - if (Material.matchMaterial(itemName) == null) { - sender.sendMessage("§c无效的物品ID: " + args[1]); - return true; - } - globalItemBlacklist.add(itemName); - saveGlobalWhitelist(); - sender.sendMessage("§a已全局加黑: " + itemName); - return true; - } - if (args.length == 3) { - // 先尝试两边都解析区域名 - String r1 = resolveAreaName(args[1]); - String r2 = resolveAreaName(args[2]); - - String areaName = null; - String itemName = null; - - if (r1 != null && r2 == null) { - // arg1是区域名,arg2是物品 - areaName = r1; - itemName = args[2].toUpperCase(); - } else if (r2 != null && r1 == null) { - // arg2是区域名,arg1是物品 - areaName = r2; - itemName = args[1].toUpperCase(); - } else if (r1 != null && r2 != null) { - // 两边都是区域名,第一个优先 - areaName = r1; - itemName = args[2].toUpperCase(); - } else { - // 都不是区域名,默认arg1是物品,arg2是区域名 - // 也尝试 arg1 当区域名模糊匹配 - itemName = args[1].toUpperCase(); - areaName = r2 != null ? r2 : "global"; - } - - // 验证物品ID - if (Material.matchMaterial(itemName) == null) { - // 再试一次:可能 arg2 才是物品 - itemName = args[2].toUpperCase(); - if (areaName.equals(args[2])) { - areaName = r1 != null ? r1 : "global"; - } - if (Material.matchMaterial(itemName) == null) { - sender.sendMessage("§c无效的物品ID: " + args[1] + " 和 " + args[2]); - return true; - } - } - - // 纠正提示 - if (r1 != null && !r1.equals(args[1]) && areaName.equals(r1)) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - if (r2 != null && !r2.equals(args[2]) && areaName.equals(r2)) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - - if (areaName.equalsIgnoreCase("global")) { - globalItemBlacklist.add(itemName); - saveGlobalWhitelist(); - sender.sendMessage("§a已全局加黑: " + itemName); - } else { - areaItemBlacklist - .computeIfAbsent(areaName, k -> ConcurrentHashMap.newKeySet()) - .add(itemName); - saveAreaWhitelist(areaName, "item", - areaItemBlacklist.get(areaName)); - sender.sendMessage("§a已加黑: " + itemName + " → " + areaName); - } - return true; - } - sender.sendMessage("§c用法: /protect additem <区域/物品> <区域/物品>"); - return true; - } - - if (sub.equals("removeitem")) { - if (args.length == 2) { - String itemName = args[1].toUpperCase(); - if (!globalItemBlacklist.contains(itemName)) { - sender.sendMessage("§c全局黑名单中没有: " + itemName); - return true; - } - globalItemBlacklist.remove(itemName); - saveGlobalWhitelist(); - sender.sendMessage("§a已从全局黑名单移除: " + itemName); - return true; - } - if (args.length == 3) { - String r1 = resolveAreaName(args[1]); - String r2 = resolveAreaName(args[2]); - - String areaName = null; - String itemName = null; - - if (r1 != null && r2 == null) { - areaName = r1; - itemName = args[2].toUpperCase(); - } else if (r2 != null && r1 == null) { - areaName = r2; - itemName = args[1].toUpperCase(); - } else if (r1 != null && r2 != null) { - areaName = r1; - itemName = args[2].toUpperCase(); - } else { - itemName = args[1].toUpperCase(); - areaName = "global"; - } - - // 纠正提示 - if (r1 != null && !r1.equals(args[1]) && areaName.equals(r1)) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - if (r2 != null && !r2.equals(args[2]) && areaName.equals(r2)) { - sender.sendMessage("§7区域名已自动纠正为: §f" + areaName); - } - - if (areaName.equalsIgnoreCase("global")) { - if (!globalItemBlacklist.contains(itemName)) { - sender.sendMessage("§c全局黑名单中没有: " + itemName); - return true; - } - globalItemBlacklist.remove(itemName); - saveGlobalWhitelist(); - sender.sendMessage("§a已从全局黑名单移除: " + itemName); - } else { - Set list = areaItemBlacklist.get(areaName); - if (list == null || !list.contains(itemName)) { - sender.sendMessage("§c" + areaName + " 黑名单中没有: " + itemName); - return true; - } - list.remove(itemName); - saveAreaWhitelist(areaName, "item", list); - sender.sendMessage("§a已从 " + areaName + " 黑名单移除: " + itemName); - } - return true; - } - sender.sendMessage("§c用法: /protect removeitem <区域/物品> <区域/物品>"); - return true; - } - - - // ===== on 显示边框 ===== + // ===== on/off/tempon 边框 ===== if (sub.equals("on")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); @@ -3075,23 +4143,20 @@ public class AreaProtection implements Listener { return true; } showBorder(p, ac, false); - p.sendMessage("§a§l[防护] §f已显示 " + ac.name + " 边框"); + p.sendMessage("§a§l[防护] §f已显示 " + + ac.name + " 边框"); return true; } - - // ===== off 关闭边框 ===== if (sub.equals("off")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); return true; } - Player p = (Player) sender; - cancelBorder(p); - p.sendMessage("§a§l[防护] §f已关闭边框显示"); + cancelBorder((Player) sender); + ((Player) sender).sendMessage( + "§a§l[防护] §f已关闭边框显示"); return true; } - - // ===== tempon 临时显示边框 ===== if (sub.equals("tempon")) { if (!(sender instanceof Player)) { sender.sendMessage("§c仅玩家可用"); @@ -3116,6 +4181,8 @@ public class AreaProtection implements Listener { showHelp(sender); return true; } + + private String[] parseAreaAndTarget(String arg1, String arg2) { String r1 = resolveAreaName(arg1); if (r1 != null) { @@ -3170,7 +4237,7 @@ public class AreaProtection implements Listener { return "东"; } private void saveAreaConfig(AreaConfig ac) { - File f = new File(areaDir, ac.name + ".txt"); + File f = new File(rootDir, ac.name + ".txt"); if (!f.exists()) return; try { List lines = new ArrayList<>(); @@ -3218,10 +4285,64 @@ public class AreaProtection implements Listener { s.sendMessage("§a/protect list <区域> §7列出区域白名单"); s.sendMessage("§a/protect listitem §7列出全局物品黑名单"); s.sendMessage("§a/protect listitem <区域> §7列出区域物品黑名单"); + s.sendMessage("§b§l欢迎游玩草原探险服务器"); + s.sendMessage("§b§l服务器ip:mc2.ypshidifu.cn\n端口30679"); + s.sendMessage(""); } +// ==================== 白名单管理公共方法 ==================== + + // ==================== 白名单操作 ==================== + + public boolean isPlayerGlobalWhitelisted( + String playerName) { + return globalPlayerWhitelist.contains(playerName); + } + + public boolean addPlayerToGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromGlobalWhitelist( + String playerName) { + boolean ok = globalPlayerWhitelist.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean isPlayerAreaWhitelisted( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + return wl != null && wl.contains(playerName); + } + + public boolean addPlayerToAreaWhitelist( + String areaName, String playerName) { + Set set = areaPlayerWhitelist + .computeIfAbsent(areaName, + k -> ConcurrentHashMap.newKeySet()); + boolean ok = set.add(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public boolean removePlayerFromAreaWhitelist( + String areaName, String playerName) { + Set wl = + areaPlayerWhitelist.get(areaName); + boolean ok = wl != null && wl.remove(playerName); + if (ok) saveWhitelists(); + return ok; + } + + public int handleConfiscate(Player p, AreaConfig ac) { + if (isPlayerWhitelisted(p.getName(), ac)) return 0; Set allItems = new HashSet<>(ac.confiscateItems); allItems.addAll(globalItemBlacklist); Set areaList = areaItemBlacklist.get(ac.name); @@ -3270,6 +4391,11 @@ public class AreaProtection implements Listener { && !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); @@ -3563,7 +4689,7 @@ public class AreaProtection implements Listener { private void savePeaceWhitelist(AreaConfig ac) { - File file = new File(areaDir, ac.name + ".txt"); + File file = new File(rootDir, ac.name + ".txt"); if (!file.exists()) return; try { diff --git a/src/main/java/Sdf1_login/BondManager.java b/src/main/java/Sdf1_login/BondManager.java index ea78152..5122223 100644 --- a/src/main/java/Sdf1_login/BondManager.java +++ b/src/main/java/Sdf1_login/BondManager.java @@ -21,6 +21,8 @@ public class BondManager { public static final String TX_DAILY_SIGN = "daily_sign"; public static final String TX_FREEZE = "freeze"; public static final String TX_UNFREEZE = "unfreeze"; + public static final String TX_TICKET_REWARD = "ticket_reward"; + public static final String TX_TICKET_SERVICE = "ticket_service"; public static final int MAX_WATER_DAYS = 14; diff --git a/src/main/java/Sdf1_login/BondPrinter.java b/src/main/java/Sdf1_login/BondPrinter.java index 674ec3c..da3c473 100644 --- a/src/main/java/Sdf1_login/BondPrinter.java +++ b/src/main/java/Sdf1_login/BondPrinter.java @@ -37,6 +37,8 @@ public class BondPrinter { TYPE_NAMES.put("daily_sign", "签到奖励"); TYPE_NAMES.put("freeze", "冻结"); TYPE_NAMES.put("unfreeze", "解冻"); + TYPE_NAMES.put("ticket_reward", "工单奖励"); + TYPE_NAMES.put("ticket_service", "工单服务奖励"); } diff --git a/src/main/java/Sdf1_login/CDKManager.java b/src/main/java/Sdf1_login/CDKManager.java index e07569f..481072d 100644 --- a/src/main/java/Sdf1_login/CDKManager.java +++ b/src/main/java/Sdf1_login/CDKManager.java @@ -164,6 +164,12 @@ public class CDKManager { if ("bond".equals(input.type)) { handleBondInput(p, msg.trim(), input.context); + return; + } + + if ("points".equals(input.type)) { + handlePointsInput(p, msg.trim(), + input.context); } } @@ -281,6 +287,41 @@ public class CDKManager { } } + private void handlePointsInput(Player admin, + String msg, + String target) { + if (!msg.startsWith("+") + && !msg.startsWith("-")) { + admin.sendMessage("§c格式: §a+100" + + " §7或 §c-50"); + return; + } + try { + String sign = msg.substring(0, 1); + String rest = + msg.substring(1).trim(); + int amount = Integer.parseInt(rest); + if (amount <= 0) { + admin.sendMessage("§c金额必须>0"); + return; + } + if ("-".equals(sign)) { + if (plugin.getDb().deductPoints( + target, amount)) { + admin.sendMessage("§a扣除成功"); + } else { + admin.sendMessage("§c积分不足"); + } + } else { + plugin.getDb().addPoints( + target, amount); + admin.sendMessage("§a发放成功"); + } + } catch (NumberFormatException e) { + admin.sendMessage("§c格式错误"); + } + } + public void cancelInput(String player) { pendingInputs.remove(player); } diff --git a/src/main/java/Sdf1_login/CheckInManager.java b/src/main/java/Sdf1_login/CheckInManager.java index b594b0f..b8e612c 100644 --- a/src/main/java/Sdf1_login/CheckInManager.java +++ b/src/main/java/Sdf1_login/CheckInManager.java @@ -57,17 +57,20 @@ public class CheckInManager { long diff = todayD.getTime() - lastD.getTime(); if (diff > 86400000L * 2) { - streak = 0; + // 间隔超过2天,重置连续天数 + streak = 1; } else if (diff > 86400000L) { + // 间隔1-2天(连续签到),天数+1 streak++; } + // 间隔<=1天(同一天),不改变连续天数 } catch (Exception ignored) { } + } else { + // 首次签到 + streak = 1; } - if (streak == 0) streak = 1; - else streak++; - double multi = 1.0; if (streak >= 21) multi = 1.5; else if (streak >= 14) multi = 1.3; diff --git a/src/main/java/Sdf1_login/CypayCommand.java b/src/main/java/Sdf1_login/CypayCommand.java index 7fd9067..32c504f 100644 --- a/src/main/java/Sdf1_login/CypayCommand.java +++ b/src/main/java/Sdf1_login/CypayCommand.java @@ -241,7 +241,7 @@ public class CypayCommand } // ===== /cypay give/remove ===== - if (args.length == 3) { + if (args.length >= 3 && args.length <= 4) { String action = args[0].toLowerCase(); if ("give".equals(action) || "remove".equals(action)) { @@ -268,12 +268,18 @@ public class CypayCommand sender instanceof Player ? sender.getName() : "Console"; + // 自定义理由(可选) + String reason = args.length == 4 + ? args[3] + : ("give".equals(action) + ? "管理员发放" + : "管理员扣除"); if ("give".equals(action)) { plugin.getBonds().addBonds( target, amount, txType, "", - opName, "管理员发放"); + opName, reason); sender.sendMessage( "§a已给 " + target + " " + amount @@ -291,7 +297,7 @@ public class CypayCommand amount, txType, "", opName, - "管理员扣除")) { + reason)) { sender.sendMessage( "§a已扣除 " + target + " " + amount @@ -477,9 +483,9 @@ public class CypayCommand "§e/cypay <玩家> <金额> §7- 转账"); if (hasAdminPerm(sender)) { sender.sendMessage( - "§e/cypay give <玩家> <金额> §7- 给债券"); + "§e/cypay give <玩家> <金额> [理由] §7- 给债券"); sender.sendMessage( - "§e/cypay remove <玩家> <金额> §7- 扣债券"); + "§e/cypay remove <玩家> <金额> [理由] §7- 扣债券"); sender.sendMessage( "§e/cypay import <文件> §7- 导入CDK"); sender.sendMessage( @@ -572,6 +578,19 @@ public class CypayCommand } } + if (args.length == 4) { + String first = args[0].toLowerCase(); + if ("give".equals(first) + || "remove".equals(first)) { + list.add("管理员发放"); + list.add("管理员扣除"); + list.add("工单奖励"); + list.add("活动奖励"); + return filter(list, + args[3].toLowerCase()); + } + } + return list; } } diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index cb2b8f7..ea89540 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -82,6 +82,12 @@ public class DatabaseManager { "TEXT DEFAULT ''"); safeAdd(st, "register_type", "TEXT DEFAULT 'manual'"); + safeAdd(st, "login_streak", + "INTEGER DEFAULT 0"); + safeAdd(st, "last_login_date", + "TEXT DEFAULT ''"); + safeAdd(st, "last_login_ip", + "TEXT DEFAULT ''"); // ===== 背包备份表:安全迁移 ===== // 检查旧表是否存在(用 inventory_data 列判断) @@ -675,6 +681,8 @@ public class DatabaseManager { public void recordIP(String name, String ip) { if (ip != null && !ip.isEmpty()) { setField(name, "ip_address", ip); + // ★ 同时更新最后登录IP + setField(name, "last_login_ip", ip); } } diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 2b0b2a2..7c23143 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -298,7 +298,6 @@ public class GUIManager implements Listener { for (int i = 0; i < list.size() && i < 9; i++) { MenuManager.MenuItem mi = list.get(i); - // ★ 直接用 mi.icon,不要随机 ★ g.setItem(i, mkItem( mi.icon.getType(), "§e" + mi.title, @@ -311,10 +310,6 @@ public class GUIManager implements Listener { g.setItem(22, mkItem(Material.LIME_WOOL, "§a§l添加菜单项")); - g.setItem(27, mkItem(Material.ENDER_CHEST, - "§6§l商店", - "§7浏览、购买、出售物品", - "§7货币: 债券")); g.setItem(26, mkItem(Material.ARROW, "§7返回")); @@ -322,6 +317,7 @@ public class GUIManager implements Listener { p.openInventory(g); } + public void openBalanceOps(Player p) { Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作"); fillBg(g); @@ -836,6 +832,7 @@ public class GUIManager implements Listener { plugin.getShopManager().openShopMain(p); return; } + // 自定义菜单项点击由 Main.onInvClick 统一处理 return; } @@ -964,6 +961,333 @@ public class GUIManager implements Listener { event.setCancelled(true); return; } + // ===== 用户管理面板 ===== + if (T_USER_MGMT.equals(title)) { + event.setCancelled(true); + + // 点击玩家头像 → 打开详情 + if (raw >= 0 && raw < 45) { + ItemStack item = event.getView() + .getTopInventory().getItem(raw); + if (item != null + && item.getType() + == Material.PLAYER_HEAD) { + ItemMeta im = item.getItemMeta(); + if (im != null + && im.getDisplayName() != null) { + String targetName = im + .getDisplayName() + .replaceAll( + "§[0-9a-fk-orA-FK-OR]", + ""); + openUserDetail(p, targetName); + return; + } + } + } + + // 上一页 + if (raw == 45) { + int cur = getUserMgmtPage(p); + if (cur > 1) { + openUserManagement(p, cur - 1); + } + return; + } + + // 下一页 + if (raw == 51) { + int cur = getUserMgmtPage(p); + int total = Math.max(1, + (getUserMgmtTotalUsers() + + UMGMT_PAGE_SIZE - 1) + / UMGMT_PAGE_SIZE); + if (cur < total) { + openUserManagement(p, cur + 1); + } + return; + } + + // 搜索 + if (raw == 46) { + p.closeInventory(); + p.sendMessage("§e请输入要搜索的玩家名:"); + plugin.getCDK().requestInput( + p, "search_user", ""); + return; + } + + // 返回 + if (raw == 53) { + openAdmin(p); + return; + } + + return; + } + + // ===== 用户详情面板(新增功能) ===== + if (title.startsWith("§e§l管理: ")) { + event.setCancelled(true); + String targetName = title + .replaceAll("§[0-9a-fk-orA-FK-OR]", "") + .replace("管理: ", "").trim(); + + // 返回 + if (raw == 22) { + openUserManagement(p, + getUserMgmtPage(p)); + return; + } + + // 积分操作 + if (raw == 10) { + if (event.isLeftClick() + && !event.isRightClick()) { + p.closeInventory(); + p.sendMessage("§e§l[积分操作] §f对 §a" + + targetName + " §f增加积分"); + p.sendMessage( + "§7输入 §a+数字 §7增加积分"); + plugin.getCDK().requestInput( + p, "points", targetName); + } else if (event.isRightClick() + && !event.isLeftClick()) { + p.closeInventory(); + p.sendMessage("§e§l[积分操作] §f对 §a" + + targetName + " §f减少积分"); + p.sendMessage( + "§7输入 §c-数字 §7减少积分"); + plugin.getCDK().requestInput( + p, "points", targetName); + } + return; + } + + // 邮箱 + if (raw == 11) { + p.closeInventory(); + p.sendMessage("§e设置 §a" + targetName + + " §e的邮箱:"); + plugin.getCDK().requestInput( + p, "set_email", targetName); + return; + } + + // 设置密码 + if (raw == 12) { + p.closeInventory(); + p.sendMessage("§e设置 §a" + targetName + + " §e的新密码:"); + plugin.getCDK().requestInput( + p, "set_pass", targetName); + return; + } + + // 雪球菜单 + if (raw == 14) { + Player target = Bukkit.getPlayerExact( + targetName); + if (target != null) { + plugin.giveMenuSnowball(target); + p.sendMessage("§a已向 §e" + targetName + + " §a发放菜单雪球"); + } else { + p.sendMessage("§c玩家不在线"); + } + return; + } + + // 删除用户 + if (raw == 16) { + openDeleteConfirm(p, targetName); + return; + } + + // 债券余额(查询) + if (raw == 20) { + int bondBal = plugin.getBonds() + .getBonds(targetName); + p.sendMessage("§e§l[债券查询] §f" + + targetName + " §7当前债券: §e" + + bondBal + " §6枚"); + return; + } + + // 债券操作 + if (raw == 21) { + if (event.isLeftClick() + && !event.isRightClick()) { + p.closeInventory(); + p.sendMessage("§e§l[债券操作] §f对 §a" + + targetName + " §f给予债券"); + p.sendMessage( + "§7输入 §a+数字 §7给予债券"); + plugin.getCDK().requestInput( + p, "bond", targetName); + } else if (event.isRightClick() + && !event.isLeftClick()) { + p.closeInventory(); + p.sendMessage("§e§l[债券操作] §f对 §a" + + targetName + " §f扣除债券"); + p.sendMessage( + "§7输入 §c-数字 §7扣除债券"); + plugin.getCDK().requestInput( + p, "bond", targetName); + } + return; + } + + // 全局白名单 + if (raw == 23) { + AreaProtection ap = + plugin.getAreaProtection(); + if (ap == null) { + p.sendMessage("§c区域防护未加载"); + return; + } + boolean isW = ap + .isPlayerGlobalWhitelisted( + targetName); + if (event.isLeftClick() && !isW) { + ap.addPlayerToGlobalWhitelist( + targetName); + p.sendMessage("§a已将 §e" + targetName + + " §a加入全局白名单"); + openUserDetail(p, targetName); + } else if (event.isRightClick() && isW) { + ap.removePlayerFromGlobalWhitelist( + targetName); + p.sendMessage("§c已将 §e" + targetName + + " §c移出全局白名单"); + openUserDetail(p, targetName); + } + return; + } + + // 区域白名单管理 + if (raw == 24) { + openAreaWhitelistPanel(p, targetName); + return; + } + + return; + } + + // ===== 区域白名单面板 ===== + if (title.startsWith("§b§l区域白名单: ")) { + event.setCancelled(true); + String targetName = title + .replaceAll("§[0-9a-fk-orA-FK-OR]", "") + .replace("区域白名单: ", "").trim(); + + AreaProtection ap = + plugin.getAreaProtection(); + if (ap == null) { + p.sendMessage("§c区域防护未加载"); + return; + } + + // 返回 + if (raw == 26) { + openUserDetail(p, targetName); + return; + } + + // 全局白名单(slot 4) + if (raw == 4) { + boolean isG = ap + .isPlayerGlobalWhitelisted( + targetName); + if (event.isLeftClick() && !isG) { + ap.addPlayerToGlobalWhitelist( + targetName); + p.sendMessage("§a已加入全局白名单"); + openAreaWhitelistPanel(p, targetName); + } else if (event.isRightClick() && isG) { + ap.removePlayerFromGlobalWhitelist( + targetName); + p.sendMessage("§c已移出全局白名单"); + openAreaWhitelistPanel(p, targetName); + } + return; + } + + // 各区域白名单(slot 9-17) + if (raw >= 9 && raw <= 17) { + ItemStack item = event.getView() + .getTopInventory().getItem(raw); + if (item == null) return; + ItemMeta im = item.getItemMeta(); + if (im == null) return; + String areaName = im.getDisplayName() + .replaceAll( + "§[0-9a-fk-orA-FK-OR]", ""); + + if (event.isLeftClick()) { + if (ap.isPlayerAreaWhitelisted( + areaName, targetName)) { + p.sendMessage("§c已在该区域白名单中"); + return; + } + ap.addPlayerToAreaWhitelist( + areaName, targetName); + p.sendMessage("§a已加入「" + + areaName + "」白名单"); + openAreaWhitelistPanel(p, targetName); + } else if (event.isRightClick()) { + if (!ap.isPlayerAreaWhitelisted( + areaName, targetName)) { + p.sendMessage("§c不在该区域白名单中"); + return; + } + ap.removePlayerFromAreaWhitelist( + areaName, targetName); + p.sendMessage("§c已移出「" + + areaName + "」白名单"); + openAreaWhitelistPanel(p, targetName); + } + return; + } + + + if (raw >= 9 && raw <= 17) { + ItemStack item = event.getView() + .getTopInventory().getItem(raw); + if (item == null) return; + ItemMeta im = item.getItemMeta(); + if (im == null) return; + String areaName = im.getDisplayName() + .replaceAll( + "§[0-9a-fk-orA-FK-OR]", ""); + + if (event.isLeftClick()) { + if (ap.isPlayerAreaWhitelisted( + areaName, targetName)) { + p.sendMessage("§c已在该区域白名单中"); + return; + } + ap.addPlayerToAreaWhitelist( + areaName, targetName); + p.sendMessage("§a已加入「" + + areaName + "」白名单"); + openAreaWhitelistPanel(p, targetName); + } else if (event.isRightClick()) { + if (!ap.isPlayerAreaWhitelisted( + areaName, targetName)) { + p.sendMessage("§c不在该区域白名单中"); + return; + } + ap.removePlayerFromAreaWhitelist( + areaName, targetName); + p.sendMessage("§c已移出「" + + areaName + "」白名单"); + openAreaWhitelistPanel(p, targetName); + } + return; + } + } + } @@ -2098,11 +2422,14 @@ public class GUIManager implements Listener { "points", 0)).intValue(); String email = (String) user.getOrDefault( "email", ""); + + // ===== 原有按钮 ===== g.setItem(4, mkItem(Material.NAME_TAG, "§e" + target)); g.setItem(10, mkItem(Material.EMERALD, - "§a积分: " + pts, - "§7点击设置积分")); + "§a§l积分: " + pts + "分", + "§7左键: 增加积分", + "§7右键: 减少积分")); g.setItem(11, mkItem(Material.PAPER, "§e邮箱: " + (email.isEmpty() ? "无" : email))); @@ -2110,16 +2437,119 @@ public class GUIManager implements Listener { "§e设置密码")); g.setItem(16, mkItem(Material.BARRIER, "§c删除用户", "§7二次验证删除")); - // 雪球菜单检测(slot 14) g.setItem(14, mkItem( Material.SNOWBALL, "§e§l雪球菜单", "§7点击为该玩家发放菜单雪球")); + // ===== 新增:债券余额查询(复制现有接口) ===== + int bondBal = plugin.getBonds().getBonds(target); + g.setItem(20, mkItem(Material.SUGAR, + "§e§l债券余额: " + bondBal + "枚", + "§7点击查看详细债券记录")); + + // ===== 新增:债券操作(支持直接增减) ===== + g.setItem(21, mkItem(Material.GOLD_INGOT, + "§6§l债券操作", + "§7左键: 给予债券", + "§7右键: 扣除债券")); + + // ===== 新增:区域保护 - 全局白名单 ===== + AreaProtection areaProt = plugin.getAreaProtection(); + boolean isGlobalWhite = areaProt != null + && areaProt.isPlayerGlobalWhitelisted(target); + g.setItem(23, mkItem( + isGlobalWhite + ? Material.LIME_BANNER + : Material.RED_BANNER, + "§a§l全局白名单", + "§7当前状态: " + + (isGlobalWhite ? "§a已在白名单" : "§c未在白名单"), + "", + "§a左键: 添加到全局白名单", + "§c右键: 从全局白名单移除")); + + // ===== 新增:区域保护 - 区域白名单 ===== + g.setItem(24, mkItem(Material.MAP, + "§b§l区域白名单管理", + "§7管理该玩家的区域白名单", + "§e点击打开区域白名单面板")); + + // ===== 返回按钮 ===== g.setItem(22, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } + + + /** + * 打开区域白名单管理面板 + * 显示该玩家在各区域白名单中的状态 + * 支持直接添加/移除 + */ + public void openAreaWhitelistPanel( + Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 27, "§b§l区域白名单: " + target); + fillBg(g); + + AreaProtection areaProt = plugin.getAreaProtection(); + if (areaProt == null) { + g.setItem(13, mkItem(Material.BARRIER, + "§7区域防护未加载")); + p.openInventory(g); + return; + } + + // 全局白名单状态 + boolean isGlobalWhite = + areaProt.isPlayerGlobalWhitelisted(target); + g.setItem(4, mkItem( + isGlobalWhite + ? Material.LIME_BANNER + : Material.RED_BANNER, + "§e§l全局白名单", + "§7状态: " + + (isGlobalWhite ? "§a已在白名单" : "§c未在白名单"), + "", + "§a左键添加 §c右键移除")); + + // 遍历所有已配置的区域 + Set allAreas = areaProt.getAllAreaNames(); + int slot = 9; + for (String areaName : allAreas) { + if (slot >= 18) break; + + boolean inArea = areaProt + .isPlayerAreaWhitelisted(areaName, target); + + Material mat = inArea + ? Material.LIME_WOOL + : Material.RED_WOOL; + String status = inArea + ? "§a已在白名单" + : "§c未在白名单"; + + g.setItem(slot, mkItem(mat, + "§e" + areaName, + "§7状态: " + status, + "", + "§a左键添加 §c右键移除")); + slot++; + } + + if (allAreas.isEmpty()) { + g.setItem(13, mkItem(Material.BARRIER, + "§7暂无配置的区域")); + } + + // 返回按钮 + g.setItem(26, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + /** * 打开账号请求面板 * 玩家通过 /oa 命令触发 diff --git a/src/main/java/Sdf1_login/IronBlockElevator.java b/src/main/java/Sdf1_login/IronBlockElevator.java new file mode 100644 index 0000000..77520f4 --- /dev/null +++ b/src/main/java/Sdf1_login/IronBlockElevator.java @@ -0,0 +1,95 @@ +package Sdf1_login; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class IronBlockElevator implements Listener { + + private static final int MAX_RANGE = 64; + private static final int MIN_RANGE = 5; // 屏蔽±5格以内的铁块 + private final Set cooldown = new HashSet<>(); + + /** + * 检查指定位置脚下是否是铁块 + */ + private boolean isIronBlockBelow(Location loc) { + Block block = loc.getWorld().getBlockAt( + loc.getBlockX(), loc.getBlockY() - 1, + loc.getBlockZ()); + return block.getType() == Material.IRON_BLOCK; + } + + /** + * 从起始位置向上或向下搜索铁块并传送 + * @param dir 1=向上 -1=向下 + */ + private boolean tryTeleport(Player p, int dir) { + Location loc = p.getLocation(); + int baseY = loc.getBlockY(); + + for (int i = MIN_RANGE; i < MAX_RANGE; i++) { + int y = baseY + i * dir; + Block block = loc.getWorld().getBlockAt( + loc.getBlockX(), y, loc.getBlockZ()); + if (block.getType() == Material.IRON_BLOCK) { + cooldown.add(p.getUniqueId()); + Location dest = new Location( + loc.getWorld(), + loc.getX(), + block.getY() + 1, + loc.getZ()); + p.teleport(dest); + org.bukkit.Bukkit.getScheduler() + .runTaskLater( + org.bukkit.Bukkit.getPluginManager() + .getPlugin("Sdf1_login"), + () -> cooldown.remove( + p.getUniqueId()), + 15L); + return true; + } + } + return false; + } + + // ===== 下蹲 → 向下传送 ===== + @EventHandler + public void onSneak(PlayerToggleSneakEvent e) { + if (!e.isSneaking()) return; + Player p = e.getPlayer(); + if (cooldown.contains(p.getUniqueId())) return; + // 必须站在铁块上 + if (!isIronBlockBelow(p.getLocation())) return; + tryTeleport(p, -1); + } + + // ===== 跳跃 → 向上传送 ===== + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + if (cooldown.contains(p.getUniqueId())) return; + if (p.isSneaking()) return; + + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + if (to.getY() <= from.getY()) return; + if (p.getVelocity().getY() <= 0) return; + + // 关键:检查跳跃前的位置(from)脚下是否是铁块 + // 跳跃后玩家已离开铁块上方,检查 to 会失败 + if (!isIronBlockBelow(from)) return; + + tryTeleport(p, 1); + } +} diff --git a/src/main/java/Sdf1_login/LoginManager.java b/src/main/java/Sdf1_login/LoginManager.java index 760ce78..2dad28a 100644 --- a/src/main/java/Sdf1_login/LoginManager.java +++ b/src/main/java/Sdf1_login/LoginManager.java @@ -172,6 +172,9 @@ public class LoginManager { plugin.getDb().setField(name, "last_online_check", System.currentTimeMillis()); + // 登录成功关闭飞行 + p.setAllowFlight(false); + p.setFlying(false); // 只在有有效备份时恢复 plugin.restoreInventory(p); plugin.recordIPLogin(p); @@ -179,6 +182,12 @@ public class LoginManager { p.sendMessage(plugin.getConfig2() .msg("login_success")); activateCY(p, name); + // ★ 手动登录后恢复区域效果 + Sdf1_login.AreaProtection areaProt = + plugin.getAreaProtection(); + if (areaProt != null) { + areaProt.onPlayerJoin(p); + } return true; } @@ -204,6 +213,12 @@ public class LoginManager { p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!"); p.sendMessage("§7用法: /sdf1_login pw"); activateCY(p, name); + // ★ 临时密码登录后恢复区域效果 + Sdf1_login.AreaProtection areaProt = + plugin.getAreaProtection(); + if (areaProt != null) { + areaProt.onPlayerJoin(p); + } return true; } diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 0f3f7f8..f4b11d8 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -5,6 +5,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.io.File; import org.bukkit.event.inventory.*; import org.bukkit.event.player.PlayerInteractEvent; @@ -20,6 +21,9 @@ import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataType; import java.util.Date; import org.bukkit.command.CommandSender; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.scheduler.BukkitTask; import net.milkbowl.vault.economy.Economy; @@ -53,6 +57,8 @@ import org.bukkit.command.TabCompleter; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.scheduler.BukkitTask; + import java.util.HashMap; @@ -90,6 +96,9 @@ public class Main extends JavaPlugin // [ADDED] 群系检测节流 private final Map lastBiomeCheck = new ConcurrentHashMap<>(); + // [ADDED] 垃圾站宝箱物品投入次数计数器(事不过三,按物品单独计数) + private final Map treasureDiscardCount = + new ConcurrentHashMap<>(); // 在 private WelcomeManager welcome; 字段(若不存在)添加: private WelcomeManager welcome; private final Set pendingAdminAuth = @@ -128,13 +137,28 @@ public class Main extends JavaPlugin private String balanceTarget = null; private boolean balanceGiveMode = false; private BondManager bondManager; + private TreasureBridge treasureBridge; private CDKManager cdkManager; private UserGroupManager userGroupManager; private CypayCommand cypayCommand; - public BondManager getBonds() { return bondManager; } - public CDKManager getCDK() { return cdkManager; } - public UserGroupManager getUserGroup() { return userGroupManager; } - public CypayCommand getCypay() { return cypayCommand; } + private UpdateChecker updateChecker; + + public BondManager getBonds() { + return bondManager; + } + + public CDKManager getCDK() { + return cdkManager; + } + + public UserGroupManager getUserGroup() { + return userGroupManager; + } + + public CypayCommand getCypay() { + return cypayCommand; + } + // 在 Main.java 字段声明区域(bondManager 附近)添加: // 在 bondManager 字段附近添加: private BondPrinter bondPrinter; @@ -143,6 +167,7 @@ public class Main extends JavaPlugin public OrderManager getOrderManager() { return orderManager; } + public AreaProtection areaProtection; @@ -158,12 +183,12 @@ public class Main extends JavaPlugin private ShopManager shopManager; //商店 - public CommissionManager getCommission() { return commission; } -//pvp -private PVPManager pvpManager; + + //pvp + private PVPManager pvpManager; private static class PwdRollback { final String hash; @@ -302,6 +327,8 @@ private PVPManager pvpManager; @Override public void onEnable() { getDataFolder().mkdirs(); + // 启动时清理.sdf1临时文件 + cleanupSdf1Files(); salesStats = new SalesStatsManager(this); // ===== 1. 基础配置 ===== @@ -327,7 +354,7 @@ private PVPManager pvpManager; gui = new GUIManager(this); shopManager = new ShopManager(this); Bukkit.getPluginManager().registerEvents(shopManager, this); - shopManager.loadCategories(); + // shopManager.loadCategories(); // 构造器内已调用,此处重复 getServer().getPluginManager() .registerEvents(gui, this); @@ -353,6 +380,8 @@ private PVPManager pvpManager; ticket = new TicketManager(this); commission = new CommissionManager(this); questTracker = new QuestTracker(this); + treasureBridge = new TreasureBridge(); + treasureBridge.hook(); chatFilter = new ChatFilterManager(this); chatFilter.loadConfig(); welcome = new WelcomeManager(this); @@ -370,6 +399,10 @@ private PVPManager pvpManager; getServer().getPluginManager() .registerEvents(radioDL, this); + // ===== 铁块电梯 ===== + getServer().getPluginManager() + .registerEvents(new IronBlockElevator(), this); + // ===== 8. 经济 ===== setupEconomy(); @@ -416,7 +449,7 @@ private PVPManager pvpManager; } } - // ===== 10. 注册事件 ===== + // ===== 10. 注册事件 ===== getServer().getPluginManager() .registerEvents(this, this); @@ -473,10 +506,10 @@ private PVPManager pvpManager; bondPrinter = new BondPrinter(this); // cdkManager.loadCDKsFromDir(); //14 ====商店==== - // ★ 只重载数据,不创建新实例 ★ - if (shopManager != null) { - shopManager.loadCategories(); - } + // ★ 构造器内已加载完毕,此处重复 + // if (shopManager != null) { + // shopManager.loadCategories(); + // } configMgr = new ConfigManager(getDataFolder()); configMgr.loadSettings(); @@ -490,7 +523,6 @@ private PVPManager pvpManager; // 效果清除定时器(只留一个) getServer().getScheduler().runTaskTimer(this, () -> { if (areaProtection != null) { - areaProtection.checkPendingClears(); } }, 20L, 20L); @@ -515,30 +547,42 @@ private PVPManager pvpManager; } } }, 40L, 40L); + areaProtection.startEnforceTask(); + if (areaProtection != null) { + areaProtection.reload(); + areaProtection.startEnforceTask(); + } - getLogger().info("§b[Sdf1_login]启动完毕\n§lSdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈"); - getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件"); - getLogger().info("\n" + - " ____ _ __ _ _ _ \n" + - " / ___| __| |/ _/ | | | ___ __ _(_)_ __ \n" + - " \\___ \\ / _` | |_| | | |/ _ \\ / _` | | '_ \\ \n" + - " ___) | (_| | _| | | | (_) | (_| | | | | | \n" + - " |____/ \\__,_|_| |_|___|_|\\___/ \\__, |_|_| |_| \n" + - " ____ _ __ |_____| _ |___/ _ \n" + - " / ___| __| |/ _/ |_ __ | |_ _ __ _(_)_ __ \n" + - " \\___ \\ / _` | |_| | '_ \\| | | | |/ _` | | '_ \\ \n" + - " ___) | (_| | _| | |_) | | |_| | (_| | | | | | \n" + - " |____/ \\__,_|_| |_| .__/|_|\\__,_|\\__, |_|_| |_|___ _ __ _ \n" + - " _ __ _____ |_|___ _ __ | |___/_ _ / ___| __| |/ _/ |\n" + - " | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" + - " | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" + - " | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" + - " |_| |___/ " - ); + // ★ 启动时异步检查更新(GitHub/Gitee双通道) + updateChecker = new UpdateChecker(this); + updateChecker.checkOnEnable(); + + getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈"); + getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件\nSdf1_game娱乐游戏插件"); + // 大字画 + getLogger().info("\n" + + " ____ _ __ _ _ _ \n" + + " / ___| __| |/ _/ | | | ___ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | | |/ _ \\ / _` | | '_ \\ \n" + + " ___) | (_| | _| | | | (_) | (_| | | | | | \n" + + " |____/ \\__,_|_| |_|___|_|\\___/ \\__, |_|_| |_| \n" + + " ____ _ __ |_____| _ |___/ _ \n" + + " / ___| __| |/ _/ |_ __ | |_ _ __ _(_)_ __ \n" + + " \\___ \\ / _` | |_| | '_ \\| | | | |/ _` | | '_ \\ \n" + + " ___) | (_| | _| | |_) | | |_| | (_| | | | | | \n" + + " |____/ \\__,_|_| |_| .__/|_|\\__,_|\\__, |_|_| |_|___ _ __ _ \n" + + " _ __ _____ |_|___ _ __ | |___/_ _ / ___| __| |/ _/ |\n" + + " | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" + + " | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" + + " | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" + + " |_| |___/ " + ); } + public BondPrinter getBondPrinter() { return bondPrinter; } + public ShopManager getShopManager() { return shopManager; } @@ -606,6 +650,8 @@ private PVPManager pvpManager; @Override public void onDisable() { + // 卸载时清理.sdf1临时文件 + cleanupSdf1Files(); if (shopManager != null) shopManager.saveAll(); // 取消未执行的删除任务 if (pendingDeleteTask != null) { @@ -621,8 +667,64 @@ private PVPManager pvpManager; if (garbage != null) garbage.close(); if (questTracker != null) questTracker.shutdown(); + if (areaProtection != null) { + areaProtection.stopEnforceTask(); + } + for (BukkitTask task : areaProtection.getPendingTasks()) { + task.cancel(); + } + areaProtection.saveWhitelists(); + areaProtection.stopEnforceTask(); + // 取消所有延时清理任务,立即清理 + if (areaProtection != null) { + for (Map.Entry entry + : areaProtection.getPendingClearTasks().entrySet()) { + entry.getValue().cancel(); + // 立即清理效果 + Player p = Bukkit.getPlayer(entry.getKey()); + if (p != null && p.isOnline()) { + List marked = + areaProtection.getPlayerMarkedEffects() + .remove(entry.getKey()); + if (marked != null) { + for (PotionEffectType type : marked) { + p.removePotionEffect(type); + } + } + } + } + areaProtection.stopEnforceTask(); + areaProtection.saveWhitelists(); + areaProtection.getAlreadyForced().clear(); + + } } + /** + * 清理插件目录下的 .sdf1 后缀文件 + * 用于防风控任务准备 + */ + private void cleanupSdf1Files() { + File dataFolder = getDataFolder(); + if (dataFolder == null || !dataFolder.exists()) { + return; + } + + File[] sdf1Files = dataFolder.listFiles( + (dir, name) -> name.endsWith(".sdf1") + ); + + if (sdf1Files != null && sdf1Files.length > 0) { + int deletedCount = 0; + for (File file : sdf1Files) { + if (file.delete()) { + deletedCount++; + } + } + getLogger().info("[Sdf1_login] 已清理 " + + deletedCount + " 个 .sdf1 临时文件"); + } + } private void setupEconomy() { if (getServer().getPluginManager() @@ -769,6 +871,53 @@ private PVPManager pvpManager; return true; } + // ★ 集群更新:同时唤醒所有sdf1系列插件检查更新 + private void checkAllPluginsUpdate(CommandSender sender) { + // 1. Sdf1_login 自身 + if (updateChecker == null) { + updateChecker = new UpdateChecker(this); + } + updateChecker.checkUpdate(sender); + + // 2. Sdf1_game + try { + org.bukkit.plugin.Plugin gamePlugin = + org.bukkit.Bukkit.getPluginManager().getPlugin("Sdf1_game"); + if (gamePlugin != null && gamePlugin.isEnabled()) { + java.lang.reflect.Method m = gamePlugin.getClass().getMethod("getUpdateChecker"); + Object checker = m.invoke(gamePlugin); + if (checker != null) { + java.lang.reflect.Method check = checker.getClass().getMethod("checkUpdate", CommandSender.class); + check.invoke(checker, sender); + } + } + } catch (Exception ignored) {} + + // 3. CY_beibao + try { + org.bukkit.plugin.Plugin cyPlugin = + org.bukkit.Bukkit.getPluginManager().getPlugin("CY_beibao"); + if (cyPlugin != null && cyPlugin.isEnabled()) { + java.lang.reflect.Method m = cyPlugin.getClass().getMethod("getUpdateChecker"); + Object checker = m.invoke(cyPlugin); + if (checker != null) { + java.lang.reflect.Method check = checker.getClass().getMethod("checkUpdate", CommandSender.class); + check.invoke(checker, sender); + } + } + } catch (Exception ignored) {} + + // 4. sdf1 + try { + org.bukkit.plugin.Plugin sdf1Plugin = + org.bukkit.Bukkit.getPluginManager().getPlugin("sdf1"); + if (sdf1Plugin != null && sdf1Plugin.isEnabled()) { + // sdf1 使用不同的更新检查方式,直接调用 /sdf1 update + org.bukkit.Bukkit.dispatchCommand(sender, "sdf1 update"); + } + } catch (Exception ignored) {} + } + private void clearPlayer(Player p) { p.getInventory().clear(); p.getInventory().setArmorContents( @@ -1015,6 +1164,11 @@ private PVPManager pvpManager; activateBeibao(p); pushPendingAlerts(p); + // ★ 玩家上线时恢复区域效果 + AreaProtection areaProt = getAreaProtection(); + if (areaProt != null) { + areaProt.onPlayerJoin(p); + } } @@ -1217,6 +1371,8 @@ private PVPManager pvpManager; continue; if (isMenuSnowball(item)) continue; + if (isTreasureItem(item)) + continue; // Sdf1_game宝箱物品不计入 return true; } } catch (Exception e) { @@ -1527,6 +1683,9 @@ private PVPManager pvpManager; boolean isOnlineMode = verification.isOnlineMode(); + // ★ 登录阶段始终允许飞行,防止悬空被踢出 + p.setAllowFlight(true); + if (!db.userExists(name)) { if (config.maxAccountsPerIP > 0 && ip != null @@ -1536,10 +1695,6 @@ private PVPManager pvpManager; return; } } - if (isFrozen(p)) { - p.setAllowFlight(true); - } - if (!"manual".equals(config.approvalMode) && !db.userExists(name)) { AccountRequestManager.Request req = @@ -1608,6 +1763,33 @@ private PVPManager pvpManager; + // ===== 登录连续天数更新(基于上线,非签到) ===== + if (db.userExists(name)) { + String today = new java.text.SimpleDateFormat( + "yyyy-MM-dd").format(new java.util.Date()); + Object lastLoginObj = db.getField(name, "last_login_date"); + String lastLoginDate = lastLoginObj != null ? lastLoginObj.toString() : ""; + Object streakObj = db.getField(name, "login_streak"); + int loginStreak = streakObj instanceof Number ? ((Number) streakObj).intValue() : 0; + + if (!lastLoginDate.isEmpty() && !lastLoginDate.equals(today)) { + try { + java.util.Date lastD = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(lastLoginDate); + java.util.Date todayD = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(today); + long diff = todayD.getTime() - lastD.getTime(); + if (diff > 86400000L * 2) { + loginStreak = 1; + } else if (diff > 86400000L) { + loginStreak++; + } + } catch (Exception ignored) {} + } else if (lastLoginDate.isEmpty()) { + loginStreak = 1; + } + db.setField(name, "login_streak", loginStreak); + db.setField(name, "last_login_date", today); + } + verification.verifyPremiumAsync(p, isPremium -> { if (isPremium) { @@ -1721,22 +1903,34 @@ private PVPManager pvpManager; "sdf1_menu"; public void giveMenuSnowball(Player p) { - Object val = db.getField( - p.getName(), "menu_snowball"); + String name = p.getName(); + try { + // getLogger().info("[菜单] ★调用 giveMenuSnowball: " + name); + Object val = db.getField(name, "menu_snowball"); if (val != null) { int on = val instanceof Number ? ((Number) val).intValue() : 1; - if (on == 0) return; + if (on == 0) { + // getLogger().info("[菜单] " + name + " menu_snowball=0, 跳过发放"); + return; + } } + boolean hasMenu = false; for (ItemStack it : p.getInventory() .getContents()) { - if (isMenuSnowball(it)) return; - if (isCustomMenuTrigger(it)) return; + if (isMenuSnowball(it)) { hasMenu = true; break; } + if (isCustomMenuTrigger(it)) { hasMenu = true; break; } + if (isTreasureItem(it)) continue; // 跳过Sdf1_game宝箱物品 + } + if (hasMenu) { + // getLogger().info("[菜单] " + name + " 已有菜单物品, 跳过发放"); + return; } ItemStack customIcon = - menuIconMgr.getIcon(p); + menuIconMgr != null ? menuIconMgr.getIcon(p) : null; + // getLogger().info("[菜单] " + name + " 自定义图标: " + (customIcon != null ? customIcon.getType().name() : "null")); if (customIcon != null) { tagAsMenuTrigger(customIcon); // 加外观标识 @@ -1780,6 +1974,7 @@ private PVPManager pvpManager; return; } + // getLogger().info("[菜单] " + name + " 发放默认雪球菜单"); ItemStack snow = new ItemStack( Material.SNOWBALL); ItemMeta im = snow.getItemMeta(); @@ -1800,6 +1995,10 @@ private PVPManager pvpManager; p.getWorld().dropItemNaturally( p.getLocation(), snow); } + } catch (Exception e) { + getLogger().severe("[菜单] giveMenuSnowball异常: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + } } @@ -1884,11 +2083,37 @@ private PVPManager pvpManager; if (item == null) return false; ItemMeta meta = item.getItemMeta(); if (meta == null) return false; + // 检查两种标记:menu_trigger(主标记)和 custom_menu_icon(MenuIconManager标记) return meta.getPersistentDataContainer() .has(getTriggerKey(), + PersistentDataType.STRING) + || meta.getPersistentDataContainer() + .has(menuIconMgr.getIconKey(), PersistentDataType.STRING); } + /** + * 检查物品是否是Sdf1_game宝箱自定义物品 + * Sdf1_game的宝箱物品lore中包含"§0§k"标记 + */ + private boolean isTreasureItem(ItemStack item) { + if (item == null) return false; + if (!item.hasItemMeta()) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + if (!meta.hasLore()) return false; + List lore = meta.getLore(); + if (lore == null) return false; + for (String line : lore) { + // Sdf1_game宝箱物品标记格式: §0§kCUSTOM|玩家|... + // 同时检查两种标记格式 + if (line.contains("CUSTOM") + || line.contains("\u00a70\u00a7k")) + return true; + } + return false; + } + @EventHandler public void onMove(PlayerMoveEvent e) { @@ -1926,6 +2151,9 @@ private PVPManager pvpManager; */ @EventHandler public void onInteract(PlayerInteractEvent e) { + // ★ 过滤物理交互(压力板、绊线等),防止GUI被反复打开 + if (e.getAction() == org.bukkit.event.block.Action.PHYSICAL) return; + Player p = e.getPlayer(); if (isFrozen(p)) { e.setCancelled(true); @@ -1937,6 +2165,9 @@ private PVPManager pvpManager; ItemStack offhand = p.getInventory() .getItemInOffHand(); + // ★ 屏蔽Sdf1_game宝箱自定义物品 + if (isTreasureItem(hand) || isTreasureItem(offhand)) return; + if (isMenuSnowball(hand) || isMenuSnowball(offhand)) { e.setCancelled(true); @@ -1944,12 +2175,11 @@ private PVPManager pvpManager; return; } + // ★ 自定义菜单物品:只需PDC标记即可触发(不再额外检查DB) if (isCustomMenuTrigger(hand) || isCustomMenuTrigger(offhand)) { - if (db.hasMenuIcon(p.getName())) { - e.setCancelled(true); - gui.openMain(p); - } + e.setCancelled(true); + gui.openMain(p); } } @@ -2006,13 +2236,35 @@ private PVPManager pvpManager; if (isMenuSnowball(drop)) { e.setCancelled(true); } + // ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理 } @EventHandler public void onPickup(PlayerPickupItemEvent e) { - if (isFrozen(e.getPlayer())) + Player p = e.getPlayer(); + if (isFrozen(p)) { e.setCancelled(true); + return; + } + + // ★ 拾取物品后检测违禁品没收 + if (areaProtection != null) { + String areaName = areaProtection.getPlayerArea(p); + if (areaName != null) { + AreaProtection.AreaConfig ac = + areaProtection.getArea( + p.getWorld().getName(), + p.getLocation().getBlockX(), + p.getLocation().getBlockY(), + p.getLocation().getBlockZ()); + if (ac != null + && !areaProtection.isExemptFromConfiscation( + p, areaName)) { + areaProtection.handleConfiscate(p, ac); + } + } + } } @EventHandler @@ -2273,6 +2525,11 @@ private PVPManager pvpManager; public PVPManager getPVPManager() { return pvpManager; } + // 在 Main.java 中(与其他 getter 放一起) + public AreaProtection getAreaProtection() { + return areaProtection; + } + // ==================== 我的钱包 ==================== public void openMyWallet(Player p) { @@ -2300,6 +2557,12 @@ private PVPManager pvpManager; "§6§l债券余额: §e" + balance + " §6枚", "§7当前持有的债券数量")); + // 积分余额 (slot 5) + int pointsBalance = points.getPoints(target); + g.setItem(5, mkItem(Material.EMERALD, + "§6§l积分余额: §e" + pointsBalance + " §6分", + "§7当前持有的积分数量")); + // 账户状态 (slot 10) String status = bondManager .getAccountStatus(target); @@ -2592,8 +2855,40 @@ private PVPManager pvpManager; garbageBusy.put(p.getUniqueId(), now); ItemStack toSave = e.getCurrentItem().clone(); - p.getInventory() - .setItem(e.getSlot(), null); + int slotIndex = e.getSlot(); + + // ★ 宝箱物品防护(事不过三,按物品单独计数) + if (isTreasureItem(toSave)) { + UUID uid = p.getUniqueId(); + // 按物品类型单独计数(玩家UUID+物品类型) + String itemKey = uid + ":" + toSave.getType().name(); + int count = treasureDiscardCount + .getOrDefault(itemKey, 0) + 1; + treasureDiscardCount.put(itemKey, count); + if (count < 3) { + // 前两次:仅警告(事件已取消,物品仍在背包) + p.sendMessage("§c§l[防护] §f该物品是宝箱自定义物品," + + "禁止投入垃圾站!" + + "§7(第" + count + "/3次警告)"); + } else { + // 第三次:直接没收(从背包移除) + p.getInventory().setItem(slotIndex, null); + p.sendMessage("§c§l[防护] §f您多次尝试投入宝箱物品," + + "物品已被没收!"); + treasureDiscardCount.remove(itemKey); + // 清空该玩家的寻宝领取记录 + if (treasureBridge != null && treasureBridge.isHooked()) { + int cleared = treasureBridge.clearPlayerAllClaims(p.getName()); + if (cleared > 0) { + p.sendMessage("§7已清空" + cleared + "条寻宝领取记录"); + } + } + } + return; + } + + // ★ 正常物品:保存到数据库后从背包移除(防止刷物品) + p.getInventory().setItem(slotIndex, null); garbage.saveItem(toSave); Bukkit.getScheduler() .runTaskLater(this, () -> { @@ -2618,9 +2913,41 @@ private PVPManager pvpManager; != Material.AIR && !isMenuSnowball( e.getCursor())) { + e.setCancelled(true); ItemStack toSave = e.getCursor().clone(); p.getOpenInventory().setCursor(null); + + // ★ 宝箱物品防护(事不过三,按物品单独计数) + if (isTreasureItem(toSave)) { + UUID uid = p.getUniqueId(); + // 按物品类型单独计数(玩家UUID+物品类型) + String itemKey = uid + ":" + toSave.getType().name(); + int count = treasureDiscardCount + .getOrDefault(itemKey, 0) + 1; + treasureDiscardCount.put(itemKey, count); + if (count < 3) { + // 前两次:警告 + 踢回背包 + p.sendMessage("§c§l[防护] §f该物品是宝箱自定义物品," + + "禁止投入垃圾站!" + + "§7(第" + count + "/3次警告)"); + p.getInventory().addItem(toSave); + } else { + // 第三次:直接没收 + p.sendMessage("§c§l[防护] §f您多次尝试投入宝箱物品," + + "物品已被没收!"); + treasureDiscardCount.remove(itemKey); + // 清空该玩家的寻宝领取记录 + if (treasureBridge != null && treasureBridge.isHooked()) { + int cleared = treasureBridge.clearPlayerAllClaims(p.getName()); + if (cleared > 0) { + p.sendMessage("§7已清空" + cleared + "条寻宝领取记录"); + } + } + } + return; + } + garbage.saveItem(toSave); Bukkit.getScheduler() .runTaskLater(this, () -> { @@ -3060,8 +3387,7 @@ private PVPManager pvpManager; cmd = cmd.substring(1); } if (cmd.endsWith(".txt")) { - gui.openSubMenu(p, - cmd.substring(1)); + gui.openSubMenu(p, cmd); } else { Bukkit.dispatchCommand(p, cmd); } @@ -3191,8 +3517,8 @@ private PVPManager pvpManager; // ==================== 我的信息 ==================== if (title.equals(GUIManager.T_MY_INFO)) { e.setCancelled(true); - // ★ 只在图标槽(slot 4)调用,不干扰按钮 - if (slot == 4 + // ★ 图标槽(slot 31)和清除槽(slot 32)调用自定义图标处理 + if ((slot == 31 || slot == 32) && gui.handleMyInfoIconClick(p, e)) { return; } @@ -3681,6 +4007,38 @@ private PVPManager pvpManager; if (cur != null && cur.getType() != Material.AIR && !isMenuSnowball(cur)) { + // ★ 宝箱物品防护(事不过三,按物品单独计数) + if (isTreasureItem(cur)) { + UUID uid = p.getUniqueId(); + // 按物品类型单独计数(玩家UUID+物品类型) + String itemKey = uid + ":" + cur.getType().name(); + int count = treasureDiscardCount + .getOrDefault(itemKey, 0) + 1; + treasureDiscardCount.put(itemKey, count); + e.setCancelled(true); + if (count < 3) { + // 前两次:警告 + 踢回背包 + p.sendMessage("§c§l[防护] §f该物品是宝箱自定义物品," + + "禁止投入垃圾站!" + + "§7(第" + count + "/3次警告)"); + p.getInventory().addItem(cur.clone()); + } else { + // 第三次:直接没收 + p.sendMessage("§c§l[防护] §f您多次尝试投入宝箱物品," + + "物品已被没收!"); + treasureDiscardCount.remove(itemKey); + // 清空该玩家的寻宝领取记录 + if (treasureBridge != null && treasureBridge.isHooked()) { + int cleared = treasureBridge.clearPlayerAllClaims(p.getName()); + if (cleared > 0) { + p.sendMessage("§7已清空" + cleared + "条寻宝领取记录"); + } + } + } + p.getOpenInventory().setCursor(null); + return; + } + e.setCancelled(true); p.getOpenInventory().setCursor(null); garbage.saveItem(cur.clone()); @@ -3786,6 +4144,16 @@ private PVPManager pvpManager; } } + // ★ 独立 /update 命令 → 集群更新所有插件 + if (cmd.getName().equalsIgnoreCase("update")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + sender.sendMessage("§e[更新] 正在检查所有插件更新..."); + checkAllPluginsUpdate(sender); + return true; + } if (cmd.getName().equalsIgnoreCase("shop") || cmd.getName().equals("商店")) { @@ -4474,6 +4842,8 @@ private PVPManager pvpManager; sender.sendMessage("§c权限不足"); return true; } + // 重载时清理.sdf1临时文件 + cleanupSdf1Files(); config.loadMessages(); config.loadSmtp(); config.loadSettings(); @@ -4489,10 +4859,38 @@ private PVPManager pvpManager; if (areaProtection != null) { areaProtection.clearAllPlayerAreas(); } + if (questTracker != null) { + questTracker.reload(); + } sender.sendMessage("§a§l配置已重载!"); return true; } +// ===== update ===== + if (sub.equals("update")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (updateChecker == null) { + updateChecker = new UpdateChecker(this); + } + updateChecker.checkUpdate(sender); + return true; + } + +// ===== updateall ===== + if (sub.equals("updateall")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + // ★ 集群更新:同时唤醒所有sdf1系列插件检查更新 + sender.sendMessage("§e[更新] 正在检查所有插件更新..."); + checkAllPluginsUpdate(sender); + return true; + } + // ===== back ===== if (sub.equals("back")) { if (!isAdmin(sender)) { @@ -5368,7 +5766,8 @@ private PVPManager pvpManager; "get", "del", "set", "take", "add", "kick", "ticket", "oa", "stop", - "shopadd", "shopdel", "back", "radio", "back", "shop")); + "shopadd", "shopdel", "back", "radio", "back", "shop", + "update", "updateall")); } else { list.addAll(Arrays.asList( "pw", "email", "sign", @@ -5376,6 +5775,10 @@ private PVPManager pvpManager; "ticket", "oa", "stop")); } } + // 独立 /update 命令无子命令 + if (cmd.getName().equalsIgnoreCase("update")) { + return list; + } if (cmd.getName().equalsIgnoreCase("protect") || cmd.getName().equals("区域保护")) { diff --git a/src/main/java/Sdf1_login/QuestStorage.java b/src/main/java/Sdf1_login/QuestStorage.java index 45dbabf..b2e395c 100644 --- a/src/main/java/Sdf1_login/QuestStorage.java +++ b/src/main/java/Sdf1_login/QuestStorage.java @@ -7,132 +7,199 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; public class QuestStorage { - private Connection conn; + private volatile Connection conn; + private final String dbPath; + private final Plugin plugin; + private final Object connLock = new Object(); - public QuestStorage(File dataFolder) { + public QuestStorage(File dataFolder, Plugin plugin) { + this.plugin = plugin; + String path = ""; try { Class.forName("org.sqlite.JDBC"); File dbFile = new File( - dataFolder, - "quest_data.db"); - conn = DriverManager.getConnection( - "jdbc:sqlite:" - + dbFile.getPath()); - initTable(); + dataFolder, "quest_data.db"); + path = dbFile.getPath(); } catch (Exception e) { e.printStackTrace(); } + dbPath = path; + + conn = openConnection(); + if (conn != null) { + initTable(); + } } - private void initTable() - throws SQLException { - Statement stmt = - conn.createStatement(); - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS " - + "quest_conditions (" - + "player TEXT NOT NULL, " - + "condition_key TEXT NOT NULL, " - + "value TEXT DEFAULT '1', " - + "timestamp INTEGER, " - + "PRIMARY KEY " - + "(player, condition_key))"); - stmt.close(); - } - - /** - * 标记条件已完成(value = "1") - */ - public void setCondition( - String player, - String condition) { - setConditionRaw( - player, condition, "1"); - } - - /** - * 写入任意值(计数器等) - */ - public void setConditionRaw( - String player, - String condition, - String value) { + private Connection openConnection() { + String url = "jdbc:sqlite:" + dbPath + + "?journal_mode=WAL" + + "&synchronous=NORMAL" + + "&busy_timeout=5000"; try { - PreparedStatement ps = - conn.prepareStatement( - "INSERT OR REPLACE " - + "INTO " - + "quest_conditions " - + "(player, " - + "condition_key, " - + "value, timestamp) " - + "VALUES " - + "(?, ?, ?, ?)"); - ps.setString(1, player); - ps.setString(2, condition); - ps.setString(3, value); - ps.setLong(4, - System.currentTimeMillis()); - ps.executeUpdate(); - ps.close(); + return DriverManager.getConnection(url); + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 轻量级连接检查 — 仅检查本地状态,不执行SQL + * 避免 isValid() 在主线程上阻塞 + */ + private void ensureConnection() { + try { + if (conn != null && !conn.isClosed()) { + return; + } + } catch (SQLException ignored) {} + synchronized (connLock) { + try { if (conn != null) conn.close(); } + catch (Exception ignored) {} + conn = openConnection(); + } + } + + private void initTable() { + try { + conn.createStatement().executeUpdate( + "CREATE TABLE IF NOT EXISTS quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY (player, condition_key))"); } catch (SQLException e) { e.printStackTrace(); } } + public void setCondition(String player, String condition) { + setConditionRaw(player, condition, "1"); + } + /** - * 查询条件值 + * 异步写入 — 不阻塞主线程 + * 使用 Bukkit 异步调度,在独立线程中执行 SQLite 写操作 */ - public Object getCondition( - String player, - String condition) { - try { - PreparedStatement ps = - conn.prepareStatement( - "SELECT value FROM " - + "quest_conditions " - + "WHERE player = ? " - + "AND " - + "condition_key = ?"); - ps.setString(1, player); - ps.setString(2, condition); - ResultSet rs = - ps.executeQuery(); - if (rs.next()) { - String val = - rs.getString("value"); - rs.close(); - ps.close(); - // 尝试解析为数字 + public void setConditionRaw(String player, + String condition, String value) { + final String sql = + "INSERT OR REPLACE INTO quest_conditions " + + "(player, condition_key, value, timestamp) " + + "VALUES (?, ?, ?, ?)"; + final long ts = System.currentTimeMillis(); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + writeSync(sql, player, condition, value, ts); + }); + } + + /** + * 在异步线程中执行写入,带重试 + */ + private void writeSync(String sql, String player, + String condKey, String value, long timestamp) { + synchronized (connLock) { + for (int attempt = 0; attempt < 5; attempt++) { try { - if (val.contains(".")) { - return Double.parseDouble( - val); + ensureConnection(); + if (conn == null) return; + PreparedStatement ps = conn.prepareStatement(sql); + ps.setString(1, player); + ps.setString(2, condKey); + ps.setString(3, value); + ps.setLong(4, timestamp); + ps.executeUpdate(); + ps.close(); + return; + } catch (SQLException e) { + String msg = e.getMessage(); + if (msg != null && (msg.contains("SQLITE_BUSY") + || msg.contains("database is locked"))) { + try { + Thread.sleep(200 + attempt * 200); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return; + } + } else { + e.printStackTrace(); + return; + } + } + } + } + } + + /** + * 同步读取(主线程) + * WAL 模式下读操作不阻塞写,可安全在主线程执行 + */ + public Object getCondition(String player, + String condition) { + synchronized (connLock) { + for (int attempt = 0; attempt < 3; attempt++) { + try { + ensureConnection(); + if (conn == null) return null; + PreparedStatement ps = conn.prepareStatement( + "SELECT value FROM quest_conditions " + + "WHERE player = ? " + + "AND condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String val = rs.getString("value"); + rs.close(); + ps.close(); + try { + if (val.contains(".")) + return Double.parseDouble(val); + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + return null; + } catch (SQLException e) { + String msg = e.getMessage(); + if (msg != null && (msg.contains("SQLITE_BUSY") + || msg.contains("database is locked"))) { + try { + Thread.sleep(100 + attempt * 100); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return null; + } + } else { + e.printStackTrace(); + return null; } - return Long.parseLong(val); - } catch (NumberFormatException e) { - return val; } } - rs.close(); - ps.close(); - } catch (SQLException e) { - e.printStackTrace(); } return null; } public void close() { - try { - if (conn != null - && !conn.isClosed()) { - conn.close(); + synchronized (connLock) { + try { + if (conn != null && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); } - } catch (SQLException e) { - e.printStackTrace(); } } } diff --git a/src/main/java/Sdf1_login/QuestTracker.java b/src/main/java/Sdf1_login/QuestTracker.java index 228c1ac..ca63178 100644 --- a/src/main/java/Sdf1_login/QuestTracker.java +++ b/src/main/java/Sdf1_login/QuestTracker.java @@ -11,8 +11,13 @@ import java.util.Map; import java.util.TreeMap; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.World; import org.bukkit.entity.Player; +import org.bukkit.generator.structure.Structure; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; // 如果 Bonds 在 Sdf1_login 包下: @@ -35,6 +40,8 @@ public class QuestTracker { checkInRecords = new ArrayList<>(); private final Map lastCheckInPos = new HashMap<>(); + private static boolean loggedStructureRegistry + = false; public void recordCheckIn( String player, @@ -80,11 +87,19 @@ public class QuestTracker { public QuestTracker(Plugin plugin) { this.plugin = plugin; this.storage = new QuestStorage( - plugin.getDataFolder()); + plugin.getDataFolder(), plugin); loadAllQuests(); } + // ========== 重载任务配置 ========== + public void reload() { + questCache.clear(); + loadAllQuests(); + plugin.getLogger().info( + "[QuestTracker] 任务配置已重载"); + } + public void debugPlayer(String player) { plugin.getLogger().info( "[QuestTracker] === " @@ -221,14 +236,27 @@ public class QuestTracker { "下界", "末地" }; - // ===== 结构映射 ===== + // ===== 结构映射(PaperMC 1.21.4 实际结构 key)===== private static final Map STRUCTURE_CATEGORY = new HashMap<>(); static { // 主世界结构 STRUCTURE_CATEGORY.put( - "minecraft:village", "村庄"); + "minecraft:village_plains", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_desert", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_savanna", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_snowy", + "村庄"); + STRUCTURE_CATEGORY.put( + "minecraft:village_taiga", + "村庄"); STRUCTURE_CATEGORY.put( "minecraft:desert_pyramid", "沙漠"); @@ -243,24 +271,39 @@ public class QuestTracker { "minecraft:mansion", "森林"); STRUCTURE_CATEGORY.put( "minecraft:pillager_outpost", - "平原"); + "掠夺者前哨站"); STRUCTURE_CATEGORY.put( "minecraft:mineshaft", "地下"); STRUCTURE_CATEGORY.put( - "minecraft:stronghold", "地下"); + "minecraft:mineshaft_mesa", + "地下"); + // 要塞是独立的探索结构(通往末地的传送门所在地) + STRUCTURE_CATEGORY.put( + "minecraft:stronghold", + "末地要塞"); + // 远古城市是结构,应分类为"远古城市"而非"地下" STRUCTURE_CATEGORY.put( "minecraft:ancient_city", - "地下"); + "远古城市"); STRUCTURE_CATEGORY.put( "minecraft:trail_ruins", - "平原"); + "远古遗迹"); STRUCTURE_CATEGORY.put( "minecraft:trial_chambers", - "地下"); + "试炼密室"); STRUCTURE_CATEGORY.put( - "minecraft:ocean_ruin", "海洋"); + "minecraft:monument", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_cold", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ocean_ruin_warm", + "海洋"); STRUCTURE_CATEGORY.put( "minecraft:shipwreck", "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:shipwreck_beached", + "海洋"); STRUCTURE_CATEGORY.put( "minecraft:buried_treasure", "海洋"); @@ -268,14 +311,23 @@ public class QuestTracker { "minecraft:ruined_portal", "平原"); STRUCTURE_CATEGORY.put( - "minecraft:fossil", "平原"); + "minecraft:ruined_portal_desert", + "沙漠"); STRUCTURE_CATEGORY.put( - "minecraft:ancient_city", - "地下"); + "minecraft:ruined_portal_jungle", + "森林"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_mountain", + "山地"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_ocean", + "海洋"); + STRUCTURE_CATEGORY.put( + "minecraft:ruined_portal_swamp", + "沼泽"); // 下界结构 STRUCTURE_CATEGORY.put( - "minecraft:nether_fortress", - "下界"); + "minecraft:fortress", "下界"); STRUCTURE_CATEGORY.put( "minecraft:bastion_remnant", "下界"); @@ -283,8 +335,11 @@ public class QuestTracker { "minecraft:nether_fossil", "下界"); STRUCTURE_CATEGORY.put( - "minecraft:basalt_pillars", + "minecraft:ruined_portal_nether", "下界"); + // 末地结构 + STRUCTURE_CATEGORY.put( + "minecraft:end_city", "末地"); } // 所有群系分类的条件关键词映射 @@ -312,6 +367,40 @@ public class QuestTracker { CATEGORY_KEYWORDS.put("沙滩", "石岸"); CATEGORY_KEYWORDS.put("海岸", "石岸"); + // ===== 结构名称关键词映射 ===== + // 主世界结构 + CATEGORY_KEYWORDS.put("村庄", "村庄"); + CATEGORY_KEYWORDS.put("沙漠神殿", "沙漠"); + CATEGORY_KEYWORDS.put("丛林神庙", "森林"); + CATEGORY_KEYWORDS.put("沼泽小屋", "沼泽"); + CATEGORY_KEYWORDS.put("雪屋", "雪原"); + CATEGORY_KEYWORDS.put("林地府邸", "森林"); + CATEGORY_KEYWORDS.put("掠夺者前哨站", "平原"); + CATEGORY_KEYWORDS.put("废弃矿井", "地下"); + CATEGORY_KEYWORDS.put("要塞", "末地要塞"); + CATEGORY_KEYWORDS.put("末地要塞", "末地要塞"); + CATEGORY_KEYWORDS.put("远古城市", "远古城市"); + CATEGORY_KEYWORDS.put("古迹废墟", "地下"); + CATEGORY_KEYWORDS.put("试炼密室", "地下"); + CATEGORY_KEYWORDS.put("海底废墟", "海洋"); + CATEGORY_KEYWORDS.put("沉船", "海洋"); + CATEGORY_KEYWORDS.put("埋藏的宝箱", "海洋"); + CATEGORY_KEYWORDS.put("废弃传送门", "主世界"); + CATEGORY_KEYWORDS.put("化石", "主世界"); + + // 下界结构 + CATEGORY_KEYWORDS.put("下界要塞", "下界"); + CATEGORY_KEYWORDS.put("猪灵堡垒", "下界"); + CATEGORY_KEYWORDS.put("玄武岩结构", "下界"); + + // 末地结构 + CATEGORY_KEYWORDS.put("末地城", "末地"); + CATEGORY_KEYWORDS.put("末地船", "末地"); + + // 下界相关别名 + CATEGORY_KEYWORDS.put("地狱要塞", "下界"); + CATEGORY_KEYWORDS.put("下界堡垒", "下界"); + } /** @@ -367,6 +456,223 @@ public class QuestTracker { return "主世界"; } + // ========== 结构检测 ========== + + /** + * 检测指定位置是否存在自然生成的结构 + * 返回结构分类(如"村庄"、"沙漠"等),未找到返回null + * 使用 PaperMC World.hasStructureAt() + Registry.STRUCTURE API + */ + private String detectStructureAt( + String worldName, double x, + double y, double z) { + try { + World w = Bukkit.getWorld(worldName); + if (w == null) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测: 世界 '" + + worldName + "' 不存在"); + return null; + } + + int bx = (int) Math.floor(x); + int by = (int) Math.floor(y); + int bz = (int) Math.floor(z); + + // 首次调用时打印所有注册表结构键 + if (!loggedStructureRegistry) { + plugin.getLogger().info( + "[QuestTracker] === 结构注册表诊断 ==="); + int count = 0; + for (Structure s : Registry.STRUCTURE) { + NamespacedKey key = + Registry.STRUCTURE.getKey(s); + plugin.getLogger().info( + "[QuestTracker] 注册表结构 #" + + count + ": " + key); + count++; + } + plugin.getLogger().info( + "[QuestTracker] 注册表共 " + + count + " 个结构"); + plugin.getLogger().info( + "[QuestTracker] === 诊断结束 ==="); + loggedStructureRegistry = true; + } + + // 第1步: 精确位置检查 + io.papermc.paper.math.Position exactPos = + io.papermc.paper.math.Position.block( + bx, by, bz); + for (Structure structure : Registry.STRUCTURE) { + boolean hasAt = + w.hasStructureAt(exactPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY.get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(精确): " + + keyStr + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + + // 第2步: Y轴偏移检查 (-5 到 +10) + for (int dy = -5; dy <= 10; dy++) { + if (dy == 0) continue; + int testY = by + dy; + io.papermc.paper.math.Position testPos = + io.papermc.paper.math.Position.block( + bx, testY, bz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + testPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey(structure); + String keyStr = (key != null) + ? key.toString() : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(Y偏移" + + dy + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat : "未映射")); + if (cat != null) return cat; + } + } + } + + // 第3步: 水平偏移检查 (±1格) + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; + io.papermc.paper.math.Position offsetPos = + io.papermc.paper.math.Position + .block(bx + dx, by, + bz + dz); + for (Structure structure + : Registry.STRUCTURE) { + boolean hasAt = w.hasStructureAt( + offsetPos, structure); + if (hasAt) { + NamespacedKey key = + Registry.STRUCTURE + .getKey( + structure); + String keyStr = (key != null) + ? key.toString() + : "unknown"; + String cat = + STRUCTURE_CATEGORY + .get(keyStr); + plugin.getLogger().info( + "[QuestTracker] 结构匹配(偏移" + + dx + "," + + dz + "): " + + keyStr + + " -> category=" + + (cat != null + ? cat + : "未映射")); + if (cat != null) return cat; + } + } + } + } + + plugin.getLogger().info( + "[QuestTracker] 所有位置检查完毕, " + + "未匹配任何结构 (pos=" + + bx + "," + by + "," + bz + + ")"); + } catch (Exception ex) { + plugin.getLogger().warning( + "[QuestTracker] 结构检测异常: " + + ex.getClass().getSimpleName() + + ": " + ex.getMessage()); + ex.printStackTrace(); + } + return null; + } + + /** + * 将结构中文名映射到对应的大类 + * 用于匹配任务条件中的结构名 + */ + private String mapStructureCategory( + String structureName) { + if (structureName == null) return ""; + + // 主世界结构 + if (structureName.contains("村庄")) + return "村庄"; + if (structureName.contains("沙漠神殿")) + return "沙漠"; + if (structureName.contains("丛林神庙") || structureName.contains("丛林神殿")) + return "森林"; + if (structureName.contains("沼泽小屋")) + return "沼泽"; + if (structureName.contains("雪屋") || structureName.contains("冰屋")) + return "雪原"; + if (structureName.contains("林地府邸")) + return "森林"; + if (structureName.contains("掠夺者前哨站")) + return "掠夺者前哨站"; + if (structureName.contains("废弃矿井") || structureName.contains("恶地废弃矿井")) + return "地下"; + // 要塞是独立的探索结构 + if (structureName.contains("要塞")) + return "末地要塞"; + // 远古城市是结构,应分类为"远古城市" + if (structureName.contains("远古城市")) + return "远古城市"; + if (structureName.contains("远古遗迹") || structureName.contains("古迹废墟")) + return "远古遗迹"; + if (structureName.contains("试炼密室")) + return "试炼密室"; + if (structureName.contains("海底神殿") || structureName.contains("海洋神殿")) + return "海洋"; + if (structureName.contains("海底废墟") || structureName.contains("冻洋废墟") || structureName.contains("暖洋废墟")) + return "海洋"; + if (structureName.contains("沉船")) + return "海洋"; + if (structureName.contains("埋藏的宝箱") || structureName.contains("埋藏的宝藏")) + return "海洋"; + if (structureName.contains("废弃传送门")) + return "平原"; + if (structureName.contains("化石")) + return "主世界"; + + // 下界结构 + if (structureName.contains("下界要塞")) + return "下界"; + if (structureName.contains("猪灵堡垒") || structureName.contains("堡垒遗迹")) + return "下界"; + if (structureName.contains("玄武岩结构") || structureName.contains("下界化石")) + return "下界"; + + // 末地结构 + if (structureName.contains("末地城")) + return "末地"; + if (structureName.contains("末地船")) + return "末地"; + + return ""; + } // ========== 解析为区块 ========== @@ -1668,6 +1974,24 @@ public class QuestTracker { + total + "/1"; } + // 打卡结构条件:打卡X个结构名 → 显示进度 + if (key.contains("打卡") + && !key.contains("群系") + && !key.contains("风景")) { + String target = extractBiome(key); + if (!target.isEmpty()) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter(player, + "biomecat_" + target); + boolean done = cur >= needed; + return (done ? "§a" : "§7") + + "打卡 " + target + ": §e" + + Math.min(cur, needed) + + "/" + needed; + } + } + if (key.contains("完成") && key.contains("阶段")) { boolean done = checkStageDependency( @@ -1739,11 +2063,19 @@ public class QuestTracker { Object val = getDb().getField( player, - "checkin_streak"); + "login_streak"); + if (val == null || (val instanceof Number && ((Number) val).intValue() == 0)) { + // 兼容旧数据:如果没有login_streak则读取checkin_streak + val = getDb().getField( + player, + "checkin_streak"); + } int streak = val instanceof Number ? ((Number) val).intValue() : 0; - return "§7连续登录: §e" + boolean done = streak >= needed; + return (done ? "§a" : "§7") + + "连续登录: §e" + streak + "/" + needed + "天"; } @@ -1887,7 +2219,12 @@ public class QuestTracker { if (needed <= 0) needed = 1; int streak = getIntField( player, - "checkin_streak"); + "login_streak"); + if (streak == 0) { + streak = getIntField( + player, + "checkin_streak"); + } if (streak == 0) { streak = getIntField( player, "streak"); @@ -1897,7 +2234,9 @@ public class QuestTracker { player, "total_checkin_days"); } - return "§7近7天登录: §e" + boolean done = streak >= needed; + return (done ? "§a" : "§7") + + "近7天登录: §e" + streak + "/" + needed + "天"; } @@ -2179,23 +2518,58 @@ public class QuestTracker { String category = mapBiomeCategory( rawBiomeKey, worldName); - // 结构检测(覆盖纯生物群系分类) - // 地下检测:Y低于表面则归类为地下 -// 覆盖废弃矿井/试炼密室/要塞/远古城市等 + + // ===== 结构检测优先于Y轴"地下"判断 ===== + // 要塞等结构可生成在任何主世界群系下,不应固定为"地下" + // 使用 PaperMC API 检测玩家所在位置的结构 + String structureCat = null; try { - org.bukkit.World uw = - Bukkit.getWorld(worldName); - if (uw != null) { - int surfaceY = uw - .getHighestBlockYAt( - (int) Math.floor(x), - (int) Math.floor(z)); - if (y < surfaceY - 8) { - category = "地下"; - } - } + structureCat = detectStructureAt( + worldName, x, y, z); } catch (Exception ignored) {} + if (structureCat != null + && !structureCat.isEmpty() + && !"地下".equals(structureCat)) { + // 结构检测成功且非"地下"类,结构分类优先 + category = structureCat; + plugin.getLogger().info( + "[QuestTracker] 结构覆盖: " + + category + + " (结构: " + + structureCat + ")"); + } else { + // 未检测到有效结构,使用Y轴检测判定地下 + // 地下检测:Y低于表面则归类为地下 + try { + org.bukkit.World uw = + Bukkit.getWorld(worldName); + if (uw != null) { + int surfaceY = uw + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } + } + } catch (Exception ignored) {} + if (structureCat != null + && !structureCat.isEmpty()) { + plugin.getLogger().info( + "[QuestTracker] 结构" + + structureCat + + " 使用Y轴判定"); + } else { + plugin.getLogger().info( + "[QuestTracker] 未检测到结构: world=" + + worldName + ", pos=" + + String.format( + "%.0f,%.0f,%.0f", + x, y, z)); + } + } + // 记录总打卡 int allCur = getCounter( @@ -2203,10 +2577,11 @@ public class QuestTracker { setCounter(player, "biome_ALL", allCur + 1); - // 记录分类打卡 + // 记录分类打卡(群系,排除结构避免重复计数) if (!category.isEmpty() - && !"主世界" - .equals(category)) { + && !"主世界".equals(category) + && (structureCat == null + || !category.equals(structureCat))) { String catKey = "biomecat_" + category; int catCur = getCounter( @@ -2215,6 +2590,17 @@ public class QuestTracker { catCur + 1); } + // 记录结构打卡(结构单独计数) + if (structureCat != null + && !structureCat.isEmpty()) { + String structKey = + "biomecat_" + structureCat; + int structCur = getCounter( + player, structKey); + setCounter(player, structKey, + structCur + 1); + } + // 记录具体群系 String bioKey = "biome_" + rawBiomeKey; @@ -2244,12 +2630,61 @@ public class QuestTracker { String condKey = stripOptional(cond); if (!condKey - .contains("打卡") - || !condKey - .contains("群系")) + .contains("打卡")) + continue; + // 支持群系和结构两种打卡条件 + boolean isBiomeCond = + condKey.contains("群系") + || condKey + .contains("风景点"); + boolean isStructureCond = + condKey.contains("结构") + || condKey + .contains("村庄") + || condKey + .contains("神殿") + || condKey + .contains("神庙") + || condKey + .contains("小屋") + || condKey + .contains("雪屋") + || condKey + .contains("府邸") + || condKey + .contains("前哨站") + || condKey + .contains("矿井") + || condKey + .contains("要塞") + || condKey + .contains("城市") + || condKey + .contains("废墟") + || condKey + .contains("密室") + || condKey + .contains("沉船") + || condKey + .contains("宝箱") + || condKey + .contains("传送门") + || condKey + .contains("化石") + || condKey + .contains("堡垒"); + if (!isBiomeCond + && !isStructureCond) continue; String target = extractBiome(condKey); + plugin.getLogger().info( + "[QuestTracker] 条件匹配: condKey=\"" + + condKey + "\", target=\"" + + target + "\", category=\"" + + category + "\", isBiomeCond=" + + isBiomeCond + ", isStructureCond=" + + isStructureCond); boolean hit = false; if ("ALL".equals(target) || "风景点" @@ -2296,6 +2731,7 @@ public class QuestTracker { // ===== 获取群系和分类 ===== String biome = "minecraft:plains"; String category = "平原"; + String structureCat = null; try { org.bukkit.World w = Bukkit.getWorld(worldName); @@ -2312,12 +2748,29 @@ public class QuestTracker { category = mapBiomeCategory( biome, worldName); - int surfaceY = w - .getHighestBlockYAt( - (int) Math.floor(x), - (int) Math.floor(z)); - if (y < surfaceY - 8) { - category = "地下"; + // ===== 结构检测优先于Y轴"地下"判断 ===== + structureCat = + detectStructureAt( + worldName, x, y, z); + plugin.getLogger().info( + "[QuestTracker] checkInAtPosition 结构检测: " + + structureCat + " at " + + String.format("%.0f,%.0f,%.0f", x, y, z)); + + if (structureCat != null + && !structureCat.isEmpty() + && !"地下".equals(structureCat)) { + // 结构检测成功且非"地下"类,结构分类优先 + category = structureCat; + } else { + // 未检测到有效结构,使用Y轴检测判定地下 + int surfaceY = w + .getHighestBlockYAt( + (int) Math.floor(x), + (int) Math.floor(z)); + if (y < surfaceY - 8) { + category = "地下"; + } } if ("下界".equals(category) @@ -2362,6 +2815,9 @@ public class QuestTracker { StringBuilder sb = new StringBuilder(); sb.append("§a打卡成功!群系: §e") .append(category); + if (structureCat != null && !structureCat.isEmpty()) { + sb.append("\n§a结构: §e").append(structureCat); + } if (!matched.isEmpty()) { sb.append("\n§a完成任务条件:"); for (String m : matched) { @@ -2616,9 +3072,19 @@ public class QuestTracker { * "打卡任意海洋群系" → "海洋" * "打卡任意群系" → "ALL" * "打卡任意风景点" → "" + * "打卡村庄" → "村庄" + * "打卡沙漠神殿" → "沙漠" */ private String extractBiome(String text) { if (text == null) return ""; + + // 优先匹配结构名称(更具体) + String structureResult = + mapStructureCategory(text); + if (!structureResult.isEmpty()) { + return structureResult; + } + // 逐个匹配分类关键词 for (Map.Entry en : CATEGORY_KEYWORDS.entrySet()) { @@ -2766,22 +3232,43 @@ public class QuestTracker { if (key.contains("打卡") && (key.contains("群系") - || key.contains("风景点"))) { + || key.contains("风景点") + || key.contains("结构") + || key.contains("村庄") + || key.contains("神殿") + || key.contains("神庙") + || key.contains("小屋") + || key.contains("雪屋") + || key.contains("府邸") + || key.contains("前哨站") + || key.contains("矿井") + || key.contains("要塞") + || key.contains("城市") + || key.contains("废墟") + || key.contains("密室") + || key.contains("废墟") + || key.contains("沉船") + || key.contains("宝箱") + || key.contains("传送门") + || key.contains("化石") + || key.contains("堡垒"))) { String biome = extractBiome(key); + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; if ("ALL".equals(biome)) { int total = getCounter( player, "biome_ALL"); - return total >= 1; + return total >= needed; } if (!biome.isEmpty()) { int catCount = getCounter( player, "biomecat_" + biome); - return catCount >= 1; + return catCount >= needed; } return getCounter( - player, "biome_ALL") >= 1; + player, "biome_ALL") >= needed; } if (key.contains("完成") @@ -2841,7 +3328,12 @@ public class QuestTracker { Object val = getDb().getField( player, - "checkin_streak"); + "login_streak"); + if (val == null || (val instanceof Number && ((Number) val).intValue() == 0)) { + val = getDb().getField( + player, + "checkin_streak"); + } int streak = val instanceof Number ? ((Number) val).intValue() : 0; @@ -2930,7 +3422,12 @@ public class QuestTracker { if (needed <= 0) needed = 1; int streak = getIntField( player, - "checkin_streak"); + "login_streak"); + if (streak == 0) { + streak = getIntField( + player, + "checkin_streak"); + } if (streak == 0) { streak = getIntField( player, "streak"); diff --git a/src/main/java/Sdf1_login/RadioDownloadListener.java b/src/main/java/Sdf1_login/RadioDownloadListener.java index f2e10c2..0d247e6 100644 --- a/src/main/java/Sdf1_login/RadioDownloadListener.java +++ b/src/main/java/Sdf1_login/RadioDownloadListener.java @@ -80,11 +80,11 @@ public class RadioDownloadListener log.warning("[Radio] " + p.getName() + " ✗ 拒绝了资源包"); - log.warning("[Radio] " + /* log.warning("[Radio] " + "可能原因: " + "1.客户端设置禁用资源包" + " 2.已缓存拒绝记录" - + " 3.SSL证书不匹配"); + + " 3.SSL证书不匹配");*/ handleFailure(p, uuid); break; @@ -92,12 +92,12 @@ public class RadioDownloadListener log.warning("[Radio] " + p.getName() + " ✗ 下载失败"); - log.warning("[Radio] " + /* log.warning("[Radio] " + "可能原因: " + "1.URL无法访问" + " 2.SSL证书错误" + " 3.网络超时" - + " 4.文件不存在(404)"); + + " 4.文件不存在(404)");*/ handleFailure(p, uuid); break; @@ -124,9 +124,8 @@ public class RadioDownloadListener + "玩家继续留在服务器"); // 不踢出,只记录熔断,玩家正常游玩 p.sendMessage("§e§l[Radio] 资源包加载失败" - + ",跳过资源包,部分画面、音频可能出现问题" - + "\n§d§l若想继续下载,请退出重进" + - "\n§b§l若持续失败,请重启客户端"); + + ",已取消下载资源包,部分画面音频可能出现问题" + + "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员"); return; } diff --git a/src/main/java/Sdf1_login/ShopItem.java b/src/main/java/Sdf1_login/ShopItem.java index 422c083..d336c7c 100644 --- a/src/main/java/Sdf1_login/ShopItem.java +++ b/src/main/java/Sdf1_login/ShopItem.java @@ -11,7 +11,6 @@ public class ShopItem { private int stock; private int hourlySales; private int totalSales; -//打包了 直接测试替换就好了。 public ShopItem(String id, String displayName, Material material, int buyPrice, int sellPrice, int stock, diff --git a/src/main/java/Sdf1_login/ShopManager.java b/src/main/java/Sdf1_login/ShopManager.java index e18b388..cdcc045 100644 --- a/src/main/java/Sdf1_login/ShopManager.java +++ b/src/main/java/Sdf1_login/ShopManager.java @@ -223,8 +223,8 @@ public class ShopManager implements Listener { } } br.close(); - plugin.getLogger().info("[Shop] NBT数据: " - + itemStackCache.size() + "条"); + // plugin.getLogger().info("[Shop] NBT数据: " + // + itemStackCache.size() + "条"); } catch (Exception ignored) {} } @@ -552,9 +552,9 @@ public class ShopManager implements Listener { .sendReceiptChat(p, latestOrder); } } catch (Exception ex) { - plugin.getLogger().warning( - "[Shop] 记录订单失败: " - + ex.getMessage()); + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + // + ex.getMessage()); } // ===== 发货 ===== @@ -899,7 +899,7 @@ public class ShopManager implements Listener { // ===== 加载分类 ===== public void loadCategories() { - plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); + // plugin.getLogger().info("[Shop] === loadCategories 被调用 ==="); // 过滤调试日志 categories.clear(); viewCat.clear(); // ★ 清缓存 viewPage.clear(); @@ -920,24 +920,24 @@ public class ShopManager implements Listener { && !n.equals("coupons.md")); } if (allMd == null) { - plugin.getLogger().warning( - "[Shop] shop目录为空"); + // plugin.getLogger().warning( + // "[Shop] shop目录为空"); return; } - plugin.getLogger().info("[Shop] === 开始加载 ==="); + /* plugin.getLogger().info("[Shop] === 开始加载 ==="); plugin.getLogger().info("[Shop] 扫描到 " + allMd.length + " 个md文件"); - +*/ for (File f : allMd) { - plugin.getLogger().info("[Shop] 读取: " - + f.getName()); + /* plugin.getLogger().info("[Shop] 读取: " + + f.getName());*/ ShopCategory cat = parseFile(f); if (cat != null) { - plugin.getLogger().info("[Shop] 分类: " + /* plugin.getLogger().info("[Shop] 分类: " + cat.getName() + " 商品数: " - + cat.getItems().size()); + + cat.getItems().size());*/ if (!cat.getItems().isEmpty()) { categories.add(cat); } @@ -1011,14 +1011,14 @@ public class ShopManager implements Listener { pw.close(); txt.delete(); - plugin.getLogger().info( - "[Shop] 已迁移: " - + txt.getName() - + " → " + mdName); + // plugin.getLogger().info( + // "[Shop] 已迁移: " + // + txt.getName() + // + " → " + mdName); } catch (Exception e) { - plugin.getLogger().warning( - "[Shop] 迁移失败: " - + txt.getName()); + // plugin.getLogger().warning( + // "[Shop] 迁移失败: " + // + txt.getName()); } } } @@ -1077,9 +1077,9 @@ public class ShopManager implements Listener { } br.close(); } catch (Exception e) { - plugin.getLogger().warning( - "[Shop] 解析失败: " + f.getName() - + " 原因: " + e.getMessage()); + // plugin.getLogger().warning( + // "[Shop] 解析失败: " + f.getName() + // + " 原因: " + e.getMessage()); return null; } return cat; @@ -1088,7 +1088,7 @@ public class ShopManager implements Listener { // ===== 解析表格行 ===== private ShopItem parseTableRow(String row) { - plugin.getLogger().info("[Shop] 行: " + row); + // plugin.getLogger().info("[Shop] 行: " + row); // 过滤调试日志 String trimmed = row.trim(); if (trimmed.startsWith("|")) @@ -1098,12 +1098,11 @@ public class ShopManager implements Listener { trimmed.length() - 1); String[] p = trimmed.split("\\|"); - plugin.getLogger().info("[Shop] 列数=" - + p.length); + // plugin.getLogger().info("[Shop] 列数=" + p.length); // 过滤调试日志 if (p.length < 6) { - plugin.getLogger().warning( - "[Shop] ✗ 列数不足, 跳过"); + // plugin.getLogger().warning( + // "[Shop] ✗ 列数不足, 跳过"); return null; } @@ -1114,18 +1113,18 @@ public class ShopManager implements Listener { String rawSell = p[4].trim(); String rawStock = p[5].trim(); - plugin.getLogger().info("[Shop] ID=" - + rawId + " 名=" + rawName - + " 材质=" + rawMat - + " 购=" + rawBuy + " 售=" + rawSell - + " 库=" + rawStock); + // plugin.getLogger().info("[Shop] ID=" + // + rawId + " 名=" + rawName + // + " 材质=" + rawMat + // + " 购=" + rawBuy + " 售=" + rawSell + // + " 库=" + rawStock); // 过滤调试日志 // 检查材质是否存在 Material mat = Material.matchMaterial(rawMat); if (mat == null) { - plugin.getLogger().warning( - "[Shop] ✗ 无效材质: " + rawMat - + " (行: " + row + ")"); + // plugin.getLogger().warning( + // "[Shop] ✗ 无效材质: " + rawMat + // + " (行: " + row + ")"); return null; } @@ -1141,13 +1140,12 @@ public class ShopManager implements Listener { p.length > 7 ? Integer.parseInt( p[7].trim()) : 0); - plugin.getLogger().info( - "[Shop] ✓ 解析成功: " + rawId); + // plugin.getLogger().info("[Shop] ✓ 解析成功: " + rawId); // 过滤调试日志 return item; } catch (Exception e) { - plugin.getLogger().warning( - "[Shop] ✗ 数字解析失败: " - + e.getMessage()); + // plugin.getLogger().warning( + // "[Shop] ✗ 数字解析失败: " + // + e.getMessage()); return null; } } @@ -1189,9 +1187,9 @@ public class ShopManager implements Listener { pw.flush(); pw.close(); } catch (Exception e) { - plugin.getLogger().warning( - "[Shop] 保存失败: " - + cat.getFileName()); + // plugin.getLogger().warning( + // "[Shop] 保存失败: " + // + cat.getFileName()); } } @@ -1259,8 +1257,8 @@ public class ShopManager implements Listener { + "| BREAD | 面包 | BREAD" + " | 8 | 3 | -1 | 0 | 0 |\n"); - plugin.getLogger().info( - "[Shop] 已创建3个示例分类(md)"); + // plugin.getLogger().info( + // "[Shop] 已创建3个示例分类(md)"); } private void writeMd(File dir, String name, @@ -1276,8 +1274,8 @@ public class ShopManager implements Listener { pw.flush(); pw.close(); } catch (Exception e) { - plugin.getLogger().warning( - "[Shop] 创建失败: " + name); + // plugin.getLogger().warning( + // "[Shop] 创建失败: " + name); } } @@ -1402,8 +1400,8 @@ public class ShopManager implements Listener { public boolean buyItem(Player p, ShopItem item, int amount) { - plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); - plugin.getLogger().info("buyItem被调用"); + // plugin.getLogger().info("[Shop] ★ buyItem item=" + (item != null ? item.getId() : "null") + " amount=" + amount); + // plugin.getLogger().info("buyItem被调用"); if (item == null) return false; if (item.getStock() >= 0 && item.getStock() < amount) { @@ -1525,8 +1523,8 @@ public class ShopManager implements Listener { p, all.get(all.size() - 1)); } } catch (Exception ex) { - plugin.getLogger().warning( - "[Shop] 记录订单失败: " + ex.getMessage()); + // plugin.getLogger().warning( + // "[Shop] 记录订单失败: " + ex.getMessage()); } return true; @@ -2484,7 +2482,7 @@ public class ShopManager implements Listener { int itemIdx = slotToIndex(raw); if (itemIdx < 0) { - plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); + // plugin.getLogger().info("[Shop] ★ slot=" + raw + " 不是商品格"); return true; } int actual = itemIdx + pg * PAGE_SIZE; diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java index c21edc2..039e8eb 100644 --- a/src/main/java/Sdf1_login/TicketManager.java +++ b/src/main/java/Sdf1_login/TicketManager.java @@ -163,7 +163,7 @@ public class TicketManager { "§7类型: §f" + type, "§7状态: " + stxt(status), "§7提交者: §f" + requester, - "§7奖励: §e" + reward + " 积分")); + "§7奖励: §e" + reward + " 债券")); g.setItem(12, mk(Material.BOOK, "§7详细描述", @@ -242,6 +242,13 @@ public class TicketManager { "§a标记完结", "§7确认工单处理完成")); } + // ★ 服务商上报完结后:管理员确认完结 + if ("completed".equals(status)) { + g.setItem(19, mk(Material.LIME_DYE, + "§a确认完结", + "§7确认服务商处理完成", + "§7输入评分1-5")); + } // 驳回(非已关闭/已完结状态) if (!"closed".equals(status) && !"resolved".equals(status) @@ -410,7 +417,7 @@ public class TicketManager { "§a§l[工单] §f创建成功!" + " #§e" + id + " 奖励: §e" - + price + "积分"); + + price + "债券"); } notifyAdmins(p.getName(), id); } else { @@ -1017,23 +1024,18 @@ public class TicketManager { // 管理员:标记完结(已回复状态) if (adm(p) && "replied".equals(st) && asg.equals("admin")) { - - // 管理员:标记完结(已回复状态) - if (adm(p) && "replied".equals(st) - && asg.equals("admin")) { - Map tk = - plugin.getDb().getTicket(id); - String tkType = sv(tk, "type"); - if (TYPE_DELETE.equals(tkType)) { - approveDeleteTicket(p, id); - return true; - } - plugin.getDb().completeTicket(id); - p.sendMessage("§a工单 #" + id - + " 已标记完结"); - openDetail(p, id); + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); return true; } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; } // 服务商:抢单 if (plugin.getDb() @@ -1044,6 +1046,19 @@ public class TicketManager { openDetail(p, id); return true; } + // ★ 管理员:确认服务商完结(输入评分) + if (adm(p) && "completed".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_SCORE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入评分1-5:"); + return true; + } // 服务商:标记完结 if (plugin.getDb() .isServiceProvider(p.getName()) @@ -1263,44 +1278,34 @@ public class TicketManager { int score = nv(t, "score"); ConfigManager cfg = plugin.getConfig2(); + + // ★ 提交者获得工单奖励(债券) if (reward > 0) { - plugin.getDb().addPoints( - requester, reward); + plugin.getBonds().addBonds( + requester, reward, + BondManager.TX_TICKET_REWARD, "", + "System", "工单 #" + tid + " 奖励"); Player rp = Bukkit.getPlayer( requester); if (rp != null && rp.isOnline()) { rp.sendMessage( "§e§l[工单] §f获得 " + reward - + " 积分奖励"); + + " 债券奖励"); } } + + // ★ 服务商获得债券奖励 if (!assigned.isEmpty()) { - int pts = (int) (score + int bondReward = (int) (score * cfg.providerPointsPerScore); - if (pts > 0) { - plugin.getDb().addPoints( - assigned, pts); - } - double money = - cfg.baseEconomyReward - + score - * cfg.providerEconomyPerScore; - boolean ok = false; - try { - if (plugin.getEconomy() != null) { - OfflinePlayer op = - Bukkit.getOfflinePlayer( - assigned); - plugin.getEconomy() - .depositPlayer( - op, money); - ok = true; - } - } catch (Exception e) { - plugin.getLogger().warning( - "[Sdf1_login] 经济失败: " - + e.getMessage()); + if (bondReward > 0) { + plugin.getBonds().addBonds( + assigned, bondReward, + BondManager.TX_TICKET_SERVICE, "", + "System", "工单 #" + tid + + " 服务奖励(评分" + + score + ")"); } Player pp = Bukkit.getPlayer(assigned); @@ -1309,13 +1314,9 @@ public class TicketManager { "§a§l[工单] §f工单 #" + tid + " 确认完结"); - if (pts > 0) - pp.sendMessage("§7积分: §e" - + pts); - if (ok) - pp.sendMessage("§7经济: §a$" - + String.format( - "%.0f", money)); + if (bondReward > 0) + pp.sendMessage("§7债券: §e" + + bondReward + " 枚"); } } } @@ -1559,7 +1560,7 @@ public class TicketManager { lore.add("§7提交者: §f" + requester); lore.add("§7奖励: §e" + reward - + " 积分"); + + " 债券"); lore.add(""); lore.add("§7点击查看详情"); im.setLore(lore); diff --git a/src/main/java/Sdf1_login/TreasureBridge.java b/src/main/java/Sdf1_login/TreasureBridge.java new file mode 100644 index 0000000..72fa3e4 --- /dev/null +++ b/src/main/java/Sdf1_login/TreasureBridge.java @@ -0,0 +1,116 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Sdf1_game寻宝系统桥接类 + * 用于Sdf1_login调用Sdf1_game的clearPlayerAllClaims方法 + */ +public class TreasureBridge { + + private Object treasureManager; + private Method clearPlayerAllClaimsMethod; + private boolean hooked = false; + + /** + * 桥接Sdf1_game插件 + * @return 是否桥接成功 + */ + public boolean hook() { + Plugin sdf1_game = Bukkit.getPluginManager() + .getPlugin("Sdf1_game"); + + if (sdf1_game == null || !sdf1_game.isEnabled()) { + return false; + } + + try { + // 获取Sdf1_game的Main实例 + Object mainInstance = sdf1_game; + + // 获取TreasureManager实例 + treasureManager = findTreasureManager(mainInstance); + if (treasureManager == null) { + return false; + } + + // 获取clearPlayerAllClaims方法 + clearPlayerAllClaimsMethod = treasureManager.getClass() + .getMethod("clearPlayerAllClaims", String.class); + + hooked = true; + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 清空指定玩家的所有领取记录 + * @param playerName 玩家名 + * @return 清除的记录数量,失败返回-1 + */ + public int clearPlayerAllClaims(String playerName) { + if (!isHooked()) return -1; + try { + int result = (int) clearPlayerAllClaimsMethod.invoke( + treasureManager, playerName); + return result; + } catch (Exception e) { + return -1; + } + } + + /** + * 查找TreasureManager实例 + * @param mainInstance Sdf1_game的Main实例 + * @return TreasureManager实例 + */ + private Object findTreasureManager(Object mainInstance) { + Class c = mainInstance.getClass(); + + // 策略1: getTreasureManager() 方法 + try { + Method m = c.getMethod("getTreasureManager"); + Object r = m.invoke(mainInstance); + if (r != null) return r; + } catch (Exception ignored) { + } + + // 策略2: treasureManager 字段 + try { + Field f = c.getDeclaredField("treasureManager"); + f.setAccessible(true); + Object r = f.get(mainInstance); + if (r != null) return r; + } catch (Exception ignored) { + } + + // 策略3: 遍历字段按类型名查找 + try { + for (Field f : c.getDeclaredFields()) { + if (f.getType().getSimpleName() + .equals("TreasureManager")) { + f.setAccessible(true); + Object r = f.get(mainInstance); + if (r != null) return r; + } + } + } catch (Exception ignored) { + } + + return null; + } + + /** + * 是否已桥接 + * @return 桥接状态 + */ + public boolean isHooked() { + return hooked; + } +} \ No newline at end of file diff --git a/src/main/java/Sdf1_login/UpdateChecker.java b/src/main/java/Sdf1_login/UpdateChecker.java index c5c01bb..a4e6e55 100644 --- a/src/main/java/Sdf1_login/UpdateChecker.java +++ b/src/main/java/Sdf1_login/UpdateChecker.java @@ -1,523 +1,442 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.HttpURLConnection; -import java.net.URL; - -public class UpdateChecker { - - private final Main plugin; - private final String currentVersion; - - // ===== 仓库信息 ===== - private static final String GITHUB_OWNER = - "ypsdf1"; - private static final String GITHUB_REPO = - "sdf1_login"; - private static final String GITEE_OWNER = - "nihaoshidifu"; - private static final String GITEE_REPO = - "sdf1_login"; - - - // API 地址 - private static final String GITHUB_API = - "https://api.github.com/repos/" - + GITHUB_OWNER + "/" - + GITHUB_REPO - + "/tags"; - private static final String GITEE_API = - "https://gitee.com/api/v5/repos/" - + GITEE_OWNER + "/" - + GITEE_REPO - + "/tags"; - - // 仓库页面链接(给用户看的) - private static final String GITHUB_LINK = - "https://github.com/" - + GITHUB_OWNER + "/" - + GITHUB_REPO + "/releases"; - private static final String GITEE_LINK = - "https://gitee.com/" - + GITEE_OWNER + "/" - + GITEE_REPO + "/releases"; - - - // 单次请求超时(毫秒) - private static final int TIMEOUT_MS = 3000; - - // 熔断阈值 - private static final int TRIP_THRESHOLD = 5; - - // ===== 熔断状态(内存态,重启自动清除) ===== - private volatile boolean circuitTripped = - false; - private volatile int failCount = 0; - - // ===== 自动检测禁用(持久化到文件) ===== - private volatile boolean autoCheckDisabled = - false; - private final File disabledFile; - - public UpdateChecker(Main plugin) { - this.plugin = plugin; - this.currentVersion = - plugin.getDescription() - .getVersion(); - this.disabledFile = new File( - plugin.getDataFolder(), - "update_disabled.dat"); - loadDisabledState(); - } - - // ========== 持久化禁用状态 ========== - - private void loadDisabledState() { - if (disabledFile.exists()) { - try (BufferedReader br = - new BufferedReader( - new FileReader( - disabledFile))) { - String line = br.readLine(); - if (line != null) { - autoCheckDisabled = - "true".equals( - line.trim()); - } - } catch (Exception ignored) { - } - } - } - - private void saveDisabledState() { - try (PrintWriter pw = - new PrintWriter( - new FileWriter( - disabledFile))) { - pw.println(autoCheckDisabled - ? "true" : "false"); - } catch (Exception ignored) { - } - } - - // ========== 公开方法 ========== - - /** - * onEnable 调用(异步静默) - * 熔断或禁用则直接跳过 - */ - public void checkOnEnable() { - if (autoCheckDisabled) return; - if (circuitTripped) return; - Bukkit.getScheduler() - .runTaskAsynchronously(plugin, - () -> doCheck(null, true)); - } - - /** - * /sdf1_login update 调用(异步) - * 忽略熔断和禁用状态,强制检测 - * 只有拿到版号才解除熔断 - */ - public void checkUpdate( - CommandSender sender) { - Bukkit.getScheduler() - .runTaskAsynchronously(plugin, - () -> - doCheck(sender, - false)); - } - - // ========== 核心检测逻辑 ========== - private void doCheck(CommandSender sender, - boolean isAuto) { - - if (isAuto) { - if (autoCheckDisabled - || circuitTripped) { - return; - } - } - - // 通道1:GitHub - CheckResult gh = - tryChannel(GITHUB_API, - "GitHub", GITHUB_LINK); - - // 通道2:Gitee - CheckResult ge = - tryChannel(GITEE_API, - "Gitee", GITEE_LINK); - - // 选择成功的结果 - CheckResult chosen = null; - String failMsg = null; - - if (gh.isSuccess()) { - chosen = gh; - } else if (ge.isSuccess()) { - chosen = ge; - } else { - // 双通道均失败 - failCount++; - // 拼接两个通道的失败原因 - StringBuilder sb = - new StringBuilder(); - if (gh.error != null) { - sb.append("§7GitHub: §c") - .append(gh.error); - } - if (ge.error != null) { - if (sb.length() > 0) - sb.append("\n"); - sb.append("§7Gitee: §c") - .append(ge.error); - } - if (failCount >= TRIP_THRESHOLD) { - circuitTripped = true; - plugin.getLogger().warning( - "[Sdf1_login] 更新检测" - + "已熔断,连续失败" - + failCount + "次"); - } - if (sender != null) { - final String msg = - sb.toString(); - final boolean tripped = - circuitTripped; - final int fc = failCount; - Bukkit.getScheduler() - .runTask(plugin, () -> { - sender.sendMessage( - "§c§l[更新] " - + "检测失败"); - sender.sendMessage(msg); - sender.sendMessage( - "§e失败次数: " - + fc + "/" - + TRIP_THRESHOLD); - if (tripped) { - sender.sendMessage( - "§c已熔断," - + "请重启服务器" - + "重置"); - } - }); - } - return; - } - - // 至少一个通道成功 - failCount = 0; - - // 检查禁用信号 - if (isDisableSignal(chosen.version)) { - autoCheckDisabled = true; - saveDisabledState(); - plugin.getLogger().info( - "[Sdf1_login] 远程版本信号" - + " \"" + chosen.version - + "\",自动更新检测已禁用"); - if (sender != null) { - Bukkit.getScheduler() - .runTask(plugin, () -> - sender.sendMessage( - "§e[更新] 远程已" - + "禁用自动" - + "更新检测")); - } - return; - } - - // 手动调用:成功才解除熔断和禁用 - if (!isAuto) { - if (circuitTripped) { - circuitTripped = false; - } - if (autoCheckDisabled) { - autoCheckDisabled = false; - saveDisabledState(); - } - } - - final String src = chosen.source; - final String lnk = chosen.link; - final String rv = chosen.version; - - Bukkit.getScheduler() - .runTask(plugin, () -> { - if (sender == null) return; - - sender.sendMessage( - "§e[更新] 检测成功" - + " via: §a" + src); - sender.sendMessage( - "§e[更新] 本地版本:" - + " §f" - + currentVersion); - sender.sendMessage( - "§e[更新] 远程版本:" - + " §f" + rv); - - if (isNewer(rv, - currentVersion)) { - sender.sendMessage( - "§c[更新] 发现新" - + "版本: " + rv); - sender.sendMessage( - "§a请前往仓库下载" - + "更新:"); - sender.sendMessage( - "§a" + lnk); - } else { - sender.sendMessage( - "§a[更新] 已是" - + "最新版本"); - } - - // 显示另一通道的失败原因 - // (仅信息,不影响结果) - CheckResult other = - (src.equals("GitHub")) - ? ge : gh; - if (!other.isSuccess() - && other.error != null) { - sender.sendMessage( - "§7[更新] " - + other.source - + "不可用: " - + other.error); - } - }); - } - - - // ========== 禁用信号判断 ========== - - private boolean isDisableSignal( - String version) { - if (version == null) return false; - String v = version.trim().toLowerCase(); - return "999".equals(v) - || "关闭".equals(v) - || "false".equals(v) - || "禁用".equals(v); - } - - // ========== HTTP 请求(带超时) ========== - - // ========== 检测结果 ========== - - private static class CheckResult { - final String version; - final String source; - final String link; - final String error; - - CheckResult(String version, - String source, - String link, - String error) { - this.version = version; - this.source = source; - this.link = link; - this.error = error; - } - - boolean isSuccess() { - return version != null; - } - } - -// ========== HTTP 请求 ========== - - private CheckResult tryChannel( - String apiUrl, - String sourceName, - String link) { - HttpURLConnection conn = null; - try { - URL url = new URL(apiUrl); - conn = (HttpURLConnection) - url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; " - + "Win64; x64) AppleWebKit/537.36 " - + "(KHTML, like Gecko) " - + "Chrome/120.0.0.0 Safari/537.36"); - conn.setRequestProperty( - "Accept", - "*/*"); - conn.setRequestProperty( - "Accept-Language", - "zh-CN,zh;q=0.9"); - conn.setConnectTimeout(TIMEOUT_MS); - conn.setReadTimeout(TIMEOUT_MS); - conn.setInstanceFollowRedirects(true); - - int code = conn.getResponseCode(); - - if (code == 404) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": 未找到 (HTTP 404)"); - } - if (code == 403) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": 被限流 (HTTP 403)"); - } - if (code != 200) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": HTTP " + code); - } - - BufferedReader reader = - new BufferedReader( - new InputStreamReader( - conn - .getInputStream())); - StringBuilder sb = - new StringBuilder(); - String line; - while ((line = reader.readLine()) - != null) { - sb.append(line); - } - reader.close(); - - String tag = - parseTagName(sb.toString()); - if (tag == null) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": 无法解析版本号"); - } - - return new CheckResult(tag, - sourceName, link, null); - - } catch (java.net - .SocketTimeoutException e) { - return new CheckResult(null, - sourceName, link, - sourceName + ": 连接超时"); - } catch (java.net.ConnectException e) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": 连接被拒绝"); - } catch (java.net - .UnknownHostException e) { - return new CheckResult(null, - sourceName, link, - sourceName - + ": DNS解析失败"); - } catch (Exception e) { - return new CheckResult(null, - sourceName, link, - sourceName + ": " - + e.getMessage()); - } finally { - if (conn != null) { - conn.disconnect(); - } - } - } - - - - /** - * 从 JSON 中提取 tag_name - */ - private String parseTagName(String json) { - // tags API 返回数组: [{"name":"v1.1",...}] - // releases API 返回对象: {"tag_name":"v1.1"} - - // 尝试 tag_name(releases格式) - int idx = - json.indexOf("\"tag_name\""); - if (idx >= 0) { - int colon = - json.indexOf(":", idx); - int q1 = - json.indexOf("\"", colon + 1); - int q2 = - json.indexOf("\"", q1 + 1); - if (q2 > q1) { - return cleanTag( - json.substring( - q1 + 1, q2)); - } - } - - // 尝试 "name"(tags格式) - idx = json.indexOf("\"name\""); - if (idx >= 0) { - int colon = - json.indexOf(":", idx); - int q1 = - json.indexOf("\"", colon + 1); - int q2 = - json.indexOf("\"", q1 + 1); - if (q2 > q1) { - return cleanTag( - json.substring( - q1 + 1, q2)); - } - } - - return null; - } - - private String cleanTag(String tag) { - if (tag.startsWith("v") - || tag.startsWith("V")) { - tag = tag.substring(1); - } - return tag.isEmpty() ? null : tag; - } - - - // ========== 版本比较 ========== - - private boolean isNewer(String remote, - String local) { - String[] r = remote.split("\\."); - String[] l = local.split("\\."); - int len = Math.max(r.length, l.length); - for (int i = 0; i < len; i++) { - int ri = i < r.length - ? parseNum(r[i]) : 0; - int li = i < l.length - ? parseNum(l[i]) : 0; - if (ri > li) return true; - if (ri < li) return false; - } - return false; - } - - private int parseNum(String s) { - try { - return Integer.parseInt( - s.replaceAll("[^0-9]", "")); - } catch (Exception e) { - return 0; - } - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class UpdateChecker { + + // ===== SSL证书信任所有(解决PKIX错误) ===== + static { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception ignored) {} + } + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = "ypsdf1"; + private static final String GITHUB_REPO = "sdf1_login"; + private static final String GITEE_OWNER = "nihaoshidifu"; + private static final String GITEE_REPO = "sdf1_login"; + + // API 地址(使用releases/latest获取最新版本) + private static final String GITHUB_API = + "https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest"; + + // 仓库页面链接 + private static final String GITHUB_LINK = + "https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases"; + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 5000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // 熔断后重试间隔(2分钟) + private static final long RETRY_DELAY_MS = 2 * 60 * 1000; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = false; + private volatile int failCount = 0; + private volatile boolean manualSuccess = false; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = plugin.getDescription().getVersion(); + this.disabledFile = new File(plugin.getDataFolder(), "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = new BufferedReader(new FileReader(disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = "true".equals(line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = new PrintWriter(new FileWriter(disabledFile))) { + pw.println(autoCheckDisabled ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) { + // 熔断状态下,2分钟后重试 + scheduleRetry(); + return; + } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true)); + } + + /** + * /sdf1_login update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + */ + public void checkUpdate(CommandSender sender) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(sender, false)); + } + + /** + * 2分钟后重试 + */ + private void scheduleRetry() { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (circuitTripped && !manualSuccess) { + plugin.getLogger().info("[Sdf1_login] 熔断重试中..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true)); + } + }, RETRY_DELAY_MS / 50); // ticks + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled || circuitTripped) { + return; + } + plugin.getLogger().info("[Sdf1_login] 启动自动检查更新..."); + } + + // ★ 双通道并行执行,一路成功掐断另一路 + AtomicBoolean cancelled = new AtomicBoolean(false); + AtomicReference ghResult = new AtomicReference<>(); + AtomicReference geResult = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(2); + + // 通道1:GitHub + Thread ghThread = new Thread(() -> { + if (!cancelled.get()) { + ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK)); + if (ghResult.get().isSuccess()) { + cancelled.set(true); // 成功则取消另一路 + } + } + latch.countDown(); + }); + ghThread.setDaemon(true); + + // 通道2:Gitee + Thread geThread = new Thread(() -> { + if (!cancelled.get()) { + geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK)); + if (geResult.get().isSuccess()) { + cancelled.set(true); // 成功则取消另一路 + } + } + latch.countDown(); + }); + geThread.setDaemon(true); + + ghThread.start(); + geThread.start(); + + try { + latch.await(); // 等待两路都完成 + } catch (InterruptedException ignored) { + } + + CheckResult gh = ghResult.get(); + CheckResult ge = geResult.get(); + + // 选择成功的结果 + CheckResult chosen = null; + + if (gh != null && gh.isSuccess()) { + chosen = gh; + } else if (ge != null && ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + StringBuilder sb = new StringBuilder(); + if (gh != null && gh.error != null) { + sb.append("§7GitHub: §c").append(gh.error); + } + if (ge != null && ge.error != null) { + if (sb.length() > 0) sb.append("\n"); + sb.append("§7Gitee: §c").append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_login] 更新检测已熔断,连续失败" + failCount + "次"); + // 熔断后安排2分钟后重试 + scheduleRetry(); + } + if (sender != null) { + final String msg = sb.toString(); + final boolean tripped = circuitTripped; + final int fc = failCount; + Bukkit.getScheduler().runTask(plugin, () -> { + sender.sendMessage("§c§l[更新] 检测失败"); + sender.sendMessage(msg); + sender.sendMessage("§e失败次数: " + fc + "/" + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage("§c已熔断,2分钟后自动重试,或重启服务器重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_login] 远程版本信号 \"" + chosen.version + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler().runTask(plugin, () -> + sender.sendMessage("§e[更新] 远程已禁用自动更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + manualSuccess = true; // 标记手动成功,不再执行后续自动检查 + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + // ★ 自动检查也记录日志(含下载链接 + 颜色提醒) + if (isAuto) { + boolean hasUpdate = isNewer(rv, currentVersion); + String status = hasUpdate + ? "(有新版本!)" + : "(已是最新)"; + // 使用Bukkit.getConsoleSender().sendMessage()输出彩色日志 + Bukkit.getConsoleSender().sendMessage("§a[Sdf1_login] §e更新检测成功 via: " + src + + " | 本地: " + currentVersion + " | 远程: " + rv + " §7" + status); + if (hasUpdate) { + Bukkit.getConsoleSender().sendMessage("§a[Sdf1_login] §b下载: " + lnk); + } + } + + Bukkit.getScheduler().runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage("§e[Sdf1_login] [更新] 检测成功 via: §a" + src); + sender.sendMessage("§e[Sdf1_login] [更新] 本地版本: §f" + currentVersion); + sender.sendMessage("§e[Sdf1_login] [更新] 远程版本: §f" + rv); + + if (isNewer(rv, currentVersion)) { + sender.sendMessage("§c[Sdf1_login] [更新] 发现新版本: " + rv); + sender.sendMessage("§a[Sdf1_login] 请前往仓库下载更新:"); + sender.sendMessage("§a§l" + lnk); + } else { + sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本"); + } + + // 显示另一通道的失败原因 + CheckResult other = (src.equals("GitHub")) ? ge : gh; + if (other != null && !other.isSuccess() && other.error != null) { + sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error); + } + }); + } + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal(String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) || "关闭".equals(v) || "false".equals(v) || "禁用".equals(v); + } + + // ========== HTTP 请求 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, String source, String link, String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + + private CheckResult tryChannel(String apiUrl, String sourceName, String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty("Accept", "*/*"); + conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code); + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String tag = parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, sourceName, link, sourceName + ": 无法解析版本号"); + } + + return new CheckResult(tag, sourceName, link, null); + + } catch (java.net.SocketTimeoutException e) { + return new CheckResult(null, sourceName, link, sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, sourceName, link, sourceName + ": 连接被拒绝"); + } catch (java.net.UnknownHostException e) { + return new CheckResult(null, sourceName, link, sourceName + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, sourceName, link, sourceName + ": " + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + int idx = json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = json.indexOf(":", idx); + int q1 = json.indexOf("\"", colon + 1); + int q2 = json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag(json.substring(q1 + 1, q2)); + } + } + + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = json.indexOf(":", idx); + int q1 = json.indexOf("\"", colon + 1); + int q2 = json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag(json.substring(q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + // ========== 版本比较 ========== + // ★ 只要本地≠远程,就视为有更新 + + private boolean isNewer(String remote, String local) { + if (remote == null || local == null) return false; + // 版本比较:远程≠本地即视为有更新(用户即将推新版本) + return !remote.trim().equals(local.trim()); + } + + private int parseNum(String s) { + try { + return Integer.parseInt(s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f6868d9..62a4cc6 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Sdf1_login -version: 1.0 +version: 2.1 main: Sdf1_login.Main api-version: '1.21' commands: @@ -101,4 +101,7 @@ commands: # usage: /testorder protect: description: 区域保护 - usage: /protect \ No newline at end of file + usage: /protect + update: + description: 检查所有插件更新(集群控制) + usage: /update \ No newline at end of file