From cdce0c0e1910884c3e34ad4ad16aa81655dfbff2 Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Wed, 22 Jul 2026 15:45:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20PVP=E6=88=98=E7=BB=A9=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96+=E6=B8=85=E7=A9=BA=E9=98=9F=E5=88=97=E5=91=BD?= =?UTF-8?q?=E4=BB=A4+=E9=98=9F=E5=88=97=E7=A7=AF=E5=8E=8B=E4=B8=A2?= =?UTF-8?q?=E5=BC=83+PVP=E6=9F=A5=E8=AF=A23=E5=A4=A9=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. PVPManager新增showArenaTotalStats/showArenaSessionStats方法 2. DatabaseManager.getArenaSessionByNumber添加3天有效期检查 3. WebManager新增clearQueue()/discardOldestIfOverflow()方法 4. Main.java新增清空队列/cleartake命令处理 5. plugin.yml注册清空队列/cleartake命令 --- src/main/java/Sdf1_login/DatabaseManager.java | 190 ++++++++++++++++++ src/main/java/Sdf1_login/Main.java | 14 ++ src/main/java/Sdf1_login/PVPArenaManager.java | 73 +++++-- src/main/java/Sdf1_login/PVPManager.java | 50 ++++- src/main/java/Sdf1_login/WebManager.java | 45 +++++ src/main/resources/plugin.yml | 6 + 6 files changed, 362 insertions(+), 16 deletions(-) diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 57766d1..8c7bc17 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -228,6 +228,25 @@ public class DatabaseManager { + "PRIMARY KEY(player_name, region_name)" + ")"); + // PVP竞技场会话表(每局PVP世界) + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_arena_sessions (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "session_number INTEGER NOT NULL UNIQUE," + + "world_name TEXT NOT NULL," + + "created_at INTEGER NOT NULL" + + ")"); + // PVP竞技场战绩表(按会话+玩家) + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_arena_stats (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "session_id INTEGER NOT NULL," + + "player_uuid TEXT NOT NULL," + + "player_name TEXT NOT NULL," + + "kills INTEGER DEFAULT 0," + + "deaths INTEGER DEFAULT 0," + + "UNIQUE(session_id, player_uuid)" + + ")"); // 菜单图标表 st.execute("CREATE TABLE IF NOT EXISTS " @@ -1480,6 +1499,177 @@ public class DatabaseManager { return list; } + // ==================== PVP竞技场会话 & 战绩持久化 ==================== + + /** 创建新PVP竞技场会话,返回生成的id */ + public int createArenaSession(String worldName) { + try { + // 先查最大session_number + int maxNum = 0; + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery("SELECT COALESCE(MAX(session_number),0) FROM pvp_arena_sessions"); + if (rs.next()) maxNum = rs.getInt(1); + rs.close(); + st.close(); + int newNum = maxNum + 1; + PreparedStatement ps = db.prepareStatement( + "INSERT INTO pvp_arena_sessions (session_number, world_name, created_at) VALUES (?,?,?)", + PreparedStatement.RETURN_GENERATED_KEYS); + ps.setInt(1, newNum); + ps.setString(2, worldName); + ps.setLong(3, System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + int id = keys.next() ? keys.getInt(1) : 1; + keys.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + /** 获取指定会话编号的信息(超过3天返回null) */ + public Map getArenaSessionByNumber(int number) { + long threeDaysMs = 3L * 24 * 60 * 60 * 1000; + try { + PreparedStatement ps = db.prepareStatement( + "SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions WHERE session_number=? AND ? - created_at <= ?"); + long now = System.currentTimeMillis(); + ps.setInt(1, number); + ps.setLong(2, now); + ps.setLong(3, threeDaysMs); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("session_number", rs.getInt("session_number")); + row.put("world_name", rs.getString("world_name")); + row.put("created_at", rs.getLong("created_at")); + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + /** 获取所有竞技场会话列表(按编号降序) */ + public List> getAllArenaSessions() { + List> list = new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions ORDER BY session_number DESC"); + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("id", rs.getInt("id")); + row.put("session_number", rs.getInt("session_number")); + row.put("world_name", rs.getString("world_name")); + row.put("created_at", rs.getLong("created_at")); + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** 记录竞技场击杀(INSERT OR IGNORE + UPDATE) */ + public void recordArenaKill(int sessionId, String uuid, String name) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)"); + ps.setInt(1, sessionId); + ps.setString(2, uuid); + ps.setString(3, name); + ps.executeUpdate(); + ps.close(); + ps = db.prepareStatement( + "UPDATE pvp_arena_stats SET kills = kills + 1, player_name=? WHERE session_id=? AND player_uuid=?"); + ps.setString(1, name); + ps.setInt(2, sessionId); + ps.setString(3, uuid); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** 记录竞技场死亡 */ + public void recordArenaDeath(int sessionId, String uuid, String name) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)"); + ps.setInt(1, sessionId); + ps.setString(2, uuid); + ps.setString(3, name); + ps.executeUpdate(); + ps.close(); + ps = db.prepareStatement( + "UPDATE pvp_arena_stats SET deaths = deaths + 1, player_name=? WHERE session_id=? AND player_uuid=?"); + ps.setString(1, name); + ps.setInt(2, sessionId); + ps.setString(3, uuid); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** 获取某玩家所有竞技场会话的总战绩 */ + public Map getArenaTotalStats(String uuid) { + Map result = new LinkedHashMap<>(); + result.put("kills", 0); + result.put("deaths", 0); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COALESCE(SUM(kills),0) AS total_kills, COALESCE(SUM(deaths),0) AS total_deaths FROM pvp_arena_stats WHERE player_uuid=?"); + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + result.put("kills", rs.getInt("total_kills")); + result.put("deaths", rs.getInt("total_deaths")); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + /** 获取某玩家在指定会话编号中的战绩 */ + public Map getArenaSessionStats(String uuid, int sessionId) { + Map result = new LinkedHashMap<>(); + result.put("kills", 0); + result.put("deaths", 0); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT kills, deaths FROM pvp_arena_stats WHERE session_id=? AND player_uuid=?"); + ps.setInt(1, sessionId); + ps.setString(2, uuid); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + result.put("kills", rs.getInt("kills")); + result.put("deaths", rs.getInt("deaths")); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } // ==================== 安全报警 ==================== diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 6609235..8a88d8d 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -5929,6 +5929,20 @@ public class Main extends JavaPlugin + count + " 个掉落物"); return true; } + // ===== 清空队列 / cleartake — 清空DB队列中未回应的web任务 ===== + if (cmdName.equals("清空队列") || cmdName.equals("cleartake")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (webManager == null) { + sender.sendMessage("§cWeb通信管理器未初始化"); + return true; + } + webManager.clearQueue(); + sender.sendMessage("§a已清空DB队列中未回应的web同步任务"); + return true; + } // /menu 开关雪球菜单 if (cmdName.equals("menu")) { if (!(sender instanceof Player)) { diff --git a/src/main/java/Sdf1_login/PVPArenaManager.java b/src/main/java/Sdf1_login/PVPArenaManager.java index 00f249b..7afc756 100644 --- a/src/main/java/Sdf1_login/PVPArenaManager.java +++ b/src/main/java/Sdf1_login/PVPArenaManager.java @@ -110,6 +110,9 @@ public class PVPArenaManager implements Listener { private final Map pvpKills = new ConcurrentHashMap<>(); private final Map pvpDeaths = new ConcurrentHashMap<>(); + // ★ 当前竞技场会话ID(持久化到DB) + private int currentSessionId = -1; + // 连杀播报(移植自 PVPManager,全服播报) private final Map arenaKillSessions = @@ -332,6 +335,11 @@ public class PVPArenaManager implements Listener { // 同步分批预生成出生点周边区块(半径2=5×5=25个,每 tick 一行,均摊不冻服) preGenerateSpawnChunksAsync(pvpWorld, 2); reapplyWorldRules(pvpWorld); + // ★ 创建DB会话记录(持久化战绩) + currentSessionId = db.createArenaSession(pvpWorldName); + if (currentSessionId > 0) { + plugin.getLogger().info("[PVP] 创建竞技场会话 #" + currentSessionId + " (world=" + pvpWorldName + ")"); + } plugin.getLogger().info("[PVP] PVP竞技场世界就绪 种子=" + seed + " 耗时=" + dt + "ms(NORMAL真实自然地形, 出生点已固化)"); return pvpWorld; @@ -738,7 +746,7 @@ public class PVPArenaManager implements Listener { // 兜底:确保世界内无玩家(理论上冷却结束时已无人) for (Player p : new ArrayList<>(world.getPlayers())) { World main = Bukkit.getWorlds().get(0); - p.teleport(main.getSpawnLocation()); + p.teleport(getBedOrSpawnLocation(p)); p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界"); } Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形) @@ -916,8 +924,7 @@ public class PVPArenaManager implements Listener { // 延迟一帧传回主世界(避免与关闭事件冲突) Bukkit.getScheduler().runTask(plugin, () -> { if (player.isOnline()) { - World main = Bukkit.getWorlds().get(0); - player.teleport(main.getSpawnLocation()); + player.teleport(getBedOrSpawnLocation(player)); player.sendMessage("§c§l[PVP] §c你未确认装备选择,已被遣返回主世界"); player.sendMessage("§7提示:进入PVP后必须选择并确认装备才能开始战斗"); player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); @@ -1027,8 +1034,7 @@ public class PVPArenaManager implements Listener { equipInteracted.remove(playerName); if (p != null && p.isOnline()) { p.closeInventory(); - World main = Bukkit.getWorlds().get(0); - p.teleport(main.getSpawnLocation()); + p.teleport(getBedOrSpawnLocation(p)); p.sendMessage("§c§l[PVP] §c装备选择超时(" + EQUIPMENT_SELECT_TIMEOUT_SECONDS + "秒),已被自动遣返"); p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f); } @@ -1156,6 +1162,12 @@ public class PVPArenaManager implements Listener { // 总榜击杀数 pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1); + // ★ 持久化到DB + if (currentSessionId > 0) { + db.recordArenaKill(currentSessionId, ((Player) killer).getUniqueId().toString(), kName); + db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), vName); + } + // ★ 击杀奖励:PVP世界每完成2次杀敌,发放16个牛排用于补血(背包满则只发能放下的) int newKills = pvpKills.get(kName); if (newKills % 2 == 0) { @@ -1238,6 +1250,10 @@ public class PVPArenaManager implements Listener { ((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName)); } pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1); + // ★ 非击杀死亡也持久化(如摔死/溺水) + if (currentSessionId > 0) { + db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), player.getName()); + } refreshPVPScoreboard(); // 死亡时保留经验(避免丢失) @@ -1254,8 +1270,13 @@ public class PVPArenaManager implements Listener { Player player = event.getPlayer(); if (!inPVPArena.contains(player.getName())) return; - World main = Bukkit.getWorlds().get(0); - event.setRespawnLocation(main.getSpawnLocation()); + // ★ 优先床/重生点,兜底主世界出生点 + Location bed = player.getRespawnLocation(); + if (bed != null && !bed.getWorld().getName().equals(pvpWorldName)) { + event.setRespawnLocation(bed); + } else { + event.setRespawnLocation(Bukkit.getWorlds().get(0).getSpawnLocation()); + } if (!equipmentConfirmed.contains(player.getName())) { // 未确认装备:身上仍是原背包,只需清状态 @@ -1345,8 +1366,7 @@ public class PVPArenaManager implements Listener { // 若仍在竞技场世界,送回主世界 if (player.getWorld().getName().equals(pvpWorldName)) { - World main = Bukkit.getWorlds().get(0); - player.teleport(main.getSpawnLocation()); + player.teleport(getBedOrSpawnLocation(player)); cleanupPlayerStats(player); inPVPArena.remove(player.getName()); player.sendMessage("§e[PVP] 已送你回主世界"); @@ -1359,8 +1379,7 @@ public class PVPArenaManager implements Listener { if (player.getWorld().getName().equals(pvpWorldName)) { plugin.getServer().getScheduler().runTaskLater(plugin, () -> { if (!player.isOnline()) return; - World main = Bukkit.getWorlds().get(0); - player.teleport(main.getSpawnLocation()); + player.teleport(getBedOrSpawnLocation(player)); cleanupPlayerStats(player); inPVPArena.remove(player.getName()); player.sendMessage("§e[PVP] 检测到你在竞技场非正常断线,已送你回主世界"); @@ -2117,9 +2136,8 @@ public class PVPArenaManager implements Listener { player.sendMessage("§e你不在PVP竞技场中"); return; } - // 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena) - World main = Bukkit.getWorlds().get(0); - player.teleport(main.getSpawnLocation()); + // 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena),优先床 + player.teleport(getBedOrSpawnLocation(player)); player.sendMessage("§a§l正在离开PVP竞技场..."); } @@ -2152,6 +2170,33 @@ public class PVPArenaManager implements Listener { saveConfig(); } + /** 获取当前会话ID */ + public int getCurrentSessionId() { + return currentSessionId; + } + + /** 获取某玩家所有会话的总战绩(从DB) */ + public Map getTotalArenaStats(String uuid) { + return db.getArenaTotalStats(uuid); + } + + /** 获取指定会话编号的信息 */ + public Map getArenaSessionByNumber(int number) { + return db.getArenaSessionByNumber(number); + } + + /** 获取某玩家在指定会话中的战绩 */ + public Map getArenaSessionStats(String uuid, int sessionId) { + return db.getArenaSessionStats(uuid, sessionId); + } + + /** 优先传送玩家到床/重生点,兜底世界出生点 */ + public Location getBedOrSpawnLocation(Player player) { + Location bed = player.getRespawnLocation(); + if (bed != null) return bed; + return Bukkit.getWorlds().get(0).getSpawnLocation(); + } + /** 重置本局公平锁定状态(世界销毁/全量重置时调用,下一局首位玩家重新定死装备) */ private void resetMatchLock() { lockedEnchantOwner = null; diff --git a/src/main/java/Sdf1_login/PVPManager.java b/src/main/java/Sdf1_login/PVPManager.java index 29ea666..6a76912 100644 --- a/src/main/java/Sdf1_login/PVPManager.java +++ b/src/main/java/Sdf1_login/PVPManager.java @@ -1076,8 +1076,15 @@ public class PVPManager implements Listener { public boolean onCommand(Player p, String[] args) { if (args.length == 0) { - return showStats(p, - p.getName()); + showStats(p, p.getName()); + // ★ 同时展示竞技场总战绩 + showArenaTotalStats(p, p.getName()); + return true; + } + // ★ 数字参数 → 查询指定会话编号的竞技场战绩 + if (args.length == 1 && args[0].matches("\\d+")) { + showArenaSessionStats(p, p.getName(), Integer.parseInt(args[0])); + return true; } switch (args[0].toLowerCase()) { case "create": @@ -1426,4 +1433,43 @@ public class PVPManager implements Listener { } return true; } + + // ★ 展示PVP竞技场总战绩 + private void showArenaTotalStats(Player p, String target) { + org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target); + String uuid = off.getUniqueId().toString(); + Map stats = plugin.getPVPArenaManager().getTotalArenaStats(uuid); + int kills = (int) stats.get("kills"); + int deaths = (int) stats.get("deaths"); + if (kills == 0 && deaths == 0) return; + double kd = deaths > 0 ? (double) kills / deaths : kills; + p.sendMessage("§6§l[PVP竞技场] " + target); + p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", kd)); + } + + // ★ 展示指定会话编号的竞技场战绩 + private void showArenaSessionStats(Player p, String target, int sessionNumber) { + org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target); + String uuid = off.getUniqueId().toString(); + // 先查会话信息(含3天有效期检查) + Map session = plugin.getPVPArenaManager().getArenaSessionByNumber(sessionNumber); + if (session == null) { + p.sendMessage("§c未找到编号 " + sessionNumber + " 的竞技场记录,或已超出3天查询范围"); + return; + } + long createdAt = (Long) session.get("created_at"); + long threeDaysMs = 3L * 24 * 60 * 60 * 1000; + if (System.currentTimeMillis() - createdAt > threeDaysMs) { + p.sendMessage("§c该记录已超出3天查询范围,不再支持查询"); + return; + } + int dbSessionId = (int) session.get("id"); + Map stats = plugin.getPVPArenaManager().getArenaSessionStats(uuid, dbSessionId); + int kills = (int) stats.get("kills"); + int deaths = (int) stats.get("deaths"); + String worldName = (String) session.get("world_name"); + String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(createdAt)); + p.sendMessage("§6§l[PVP竞技场#" + sessionNumber + "] " + target + " §7(" + dateStr + " " + worldName + ")"); + p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", deaths > 0 ? (double) kills / deaths : kills)); + } } diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index 474a291..d046081 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -310,6 +310,7 @@ public class WebManager { dbTaskQueue.offer(task); if (dbTaskQueue.size() > 50) { plugin.getLogger().warning("[DB队列] 队列积压: " + dbTaskQueue.size() + " 任务等待"); + discardOldestIfOverflow(); } } @@ -323,6 +324,50 @@ public class WebManager { } } + /** + * ★ 清空DB队列中所有未回应的web同步任务 + * (清空队列/cleartake 命令调用) + */ + public void clearQueue() { + int count = 0; + Iterator it = dbTaskQueue.iterator(); + while (it.hasNext()) { + DbTask t = it.next(); + // 只保留登录/商店等关键任务,丢弃所有同步类web任务 + if (t.name.contains("TimerA") || t.name.contains("首次") + || t.name.contains("周期") || t.name.contains("sync") + || t.name.contains("poll") || t.name.contains("push") + || t.name.contains("pull") || t.name.contains("cdk")) { + it.remove(); + count++; + } + } + plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务"); + } + + /** + * ★ 队列积压时丢弃最早的任务(优先丢弃同步类低优先级任务) + */ + public void discardOldestIfOverflow() { + if (dbTaskQueue.size() <= 50) return; + int before = dbTaskQueue.size(); + // 收集所有普通同步任务(priority >= NORMAL_PRIORITY),按创建时间排序 + List syncTasks = new ArrayList<>(); + for (DbTask t : dbTaskQueue) { + if (t != null && t.priority >= NORMAL_PRIORITY) { + syncTasks.add(t); + } + } + syncTasks.sort(Comparator.comparingLong(t -> t.createdAt)); + // 丢弃最早的一半同步任务(至少丢弃1个) + int toDrop = Math.max(1, syncTasks.size() / 2); + for (int i = 0; i < toDrop && i < syncTasks.size(); i++) { + dbTaskQueue.remove(syncTasks.get(i)); + } + int after = dbTaskQueue.size(); + plugin.getLogger().warning("[DB队列] 队列积压(" + before + "),自动丢弃" + (before - after) + "个最早的同步请求"); + } + // ==================== SSL断路器 + 降级HTTP ==================== /** diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 530d801..f4efa93 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -178,3 +178,9 @@ commands: who: description: 查看在线玩家列表 usage: /who + 清空队列: + description: 清空DB队列中未回应的web同步任务(管理员) + usage: /清空队列 + cleartake: + description: 清空DB队列中未回应的web同步任务(管理员) + usage: /cleartake