From 4cdd32913ea5f1cf8f20e710f259cd804f9c82fd Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Mon, 6 Jul 2026 21:48:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPVP=E8=83=8C=E5=8C=85?= =?UTF-8?q?=E8=A2=AB=E8=A6=86=E7=9B=96=E9=97=AE=E9=A2=98=EF=BC=88=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E5=A4=87=E4=BB=BD=E8=A1=A8+=E6=9C=AA=E9=80=80?= =?UTF-8?q?=E5=9C=BA=E4=B8=8D=E8=A6=86=E7=9B=96=EF=BC=89=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=9A=8F=E6=9C=BA=E5=9C=B0=E5=BD=A2=E7=94=9F=E6=88=90?= =?UTF-8?q?=E3=80=81TPA=E4=BC=A0=E9=80=81=E8=AF=B7=E6=B1=82=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=9C=9F=E4=B8=8B=E9=99=90=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Sdf1_login/ConfigManager.java | 9 + src/main/java/Sdf1_login/DatabaseManager.java | 123 ++++++++++ src/main/java/Sdf1_login/PVPArenaManager.java | 218 ++++++++++-------- src/main/java/Sdf1_login/TeleportManager.java | 6 +- 4 files changed, 265 insertions(+), 91 deletions(-) diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index e55b5fe..99de129 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -235,7 +235,16 @@ public class ConfigManager { areaProtectMaxLands = parseInt(m.getOrDefault("区域防护_最大领地数", "3"), 3); tpRequestValidSeconds = parseIntFromString(m.getOrDefault("传送_请求有效秒", "90")); + // 安全下限:防止配置被误设为极小值(如把"3"当"3分钟")导致传送请求秒过期。 + // 若发现配置过小,强制下限并告警,便于定位插件设置.txt 中的异常值。 + if (tpRequestValidSeconds < 30) { + System.out.println("[Sdf1配置] 警告: 传送_请求有效秒=" + tpRequestValidSeconds + + " 过小,已强制下限为 30 秒(请检查插件设置.txt 是否误设为极小值)"); + tpRequestValidSeconds = 30; + } tpSendIntervalSeconds = parseIntFromString(m.getOrDefault("传送_发送间隔秒", "10")); + System.out.println("[Sdf1配置] 传送请求有效时间 = " + tpRequestValidSeconds + " 秒;发送间隔 = " + + tpSendIntervalSeconds + " 秒"); // rewardChannel 已标准化为 bonds/economy,默认bonds } diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 6797460..8dd29fa 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -161,6 +161,19 @@ public class DatabaseManager { + ")"); } + // PVP 背包备份表(独立表:避免与登录背包系统的 inventory_backups 互相覆盖导致原始背包丢失) + st.execute("CREATE TABLE IF NOT EXISTS " + + "pvp_inventory_backup (" + + "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 (" @@ -959,6 +972,116 @@ public class DatabaseManager { } } + // ==================== PVP 背包备份(独立表,避免与登录背包系统互相覆盖)==================== + // 登录背包系统使用 inventory_backups 表;PVP 备份若共用同一表/同一玩家名键, + // 会在换装、断线重连等时机互相覆盖,导致玩家"原始背包丢失"。故 PVP 必须独立存储。 + + /** + * 保存 PVP 背包备份(非覆盖)。 + * ★ 关键保护:只要该玩家已存在 PVP 背包备份(即尚未真正退场),绝不再次写入, + * 确保入场前的原始背包永远不被后续状态(换装/断线重连)覆盖。 + */ + public void savePvpInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + if (hasPvpInventoryBackup(name)) { + return; // 已有备份,严禁覆盖 + } + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "pvp_inventory_backup" + + " (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[] getPvpInventoryBackup(String playerName) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "pvp_inventory_backup " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT 1"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = new String[5]; + result[0] = rs.getString("contents_b64"); + result[1] = rs.getString("armor_b64"); + result[2] = rs.getString("extra_b64"); + result[3] = String.valueOf(rs.getInt("level")); + result[4] = String.valueOf(rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deletePvpInventoryBackup(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM pvp_inventory_backup " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasPvpInventoryBackup(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM pvp_inventory_backup " + + "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; + } + public boolean hasInventoryBackup( String name) { try { diff --git a/src/main/java/Sdf1_login/PVPArenaManager.java b/src/main/java/Sdf1_login/PVPArenaManager.java index 4654b54..7b28ba7 100644 --- a/src/main/java/Sdf1_login/PVPArenaManager.java +++ b/src/main/java/Sdf1_login/PVPArenaManager.java @@ -13,6 +13,7 @@ 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.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectOutputStream; @@ -20,8 +21,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -53,9 +52,10 @@ public class PVPArenaManager implements Listener { // PVP世界名称 private static final String PVP_WORLD_NAME = "pvp_arena"; - // PVP世界数据版本:每当世界生成逻辑有重大变更时 +1,触发自动重建(解决旧版"小平台"残留问题) - private static final int PVP_WORLD_VERSION = 2; - private static final String PVP_VERSION_FILE = "pvp_world_version.txt"; + // PVP世界空场冷却时间(毫秒):所有玩家退场后世界继续保留此时长, + // 冷却内若有玩家重新进入则取消删除;冷却结束且仍无人则删除世界,下次进入随机重新生成地形。 + // 目的:①战斗破坏的地形/被砍的树随世界删除而完全复原;②每次地形随机刷新,防止背图玩家单方面碾压。 + private static final long PVP_WORLD_IDLE_DELETE_MS = 5 * 60 * 1000L; // 5分钟(可按需调整) // 玩家在PVP世界中的状态 private final Set inPVPArena = ConcurrentHashMap.newKeySet(); @@ -66,6 +66,12 @@ public class PVPArenaManager implements Listener { // 玩家背包备份缓存 (玩家名 -> 备份数据) private final Map inventoryBackups = new ConcurrentHashMap<>(); + // 待执行的"空场冷却删除世界"定时任务(有人重新进入时取消) + private BukkitTask pendingDeleteTask = null; + + // 随机地形种子生成器(每次创建世界用新种子 → 地形不同) + private final Random worldSeedRandom = new Random(); + // PVP装备列表 (管理员可配置) private final List pvpEquipment = new ArrayList<>(); @@ -157,44 +163,54 @@ public class PVPArenaManager implements Listener { } /** - * 检查并创建/重建PVP世界(使用正常主世界地形生成器) + * 检查并确保PVP世界可用(按需创建,随机地形)。 * - * 关键修复:旧版代码生成的"小平台"世界文件夹仍残留在服务器磁盘, - * 原逻辑检测到世界已存在就直接 return、永不重建。现在通过版本文件判断, - * 若磁盘上的世界版本过旧(或无版本文件=旧版小平台),则卸载并删除后重新生成完整地形。 + * 生命周期模型:PVP世界是"一次性随机竞技场"—— + * • 有人要进入且世界不存在时,删除磁盘残留(含旧版小平台)后随机重新生成; + * • 玩家全部退场并经过5分钟空场冷却后,世界被删除; + * • 下次进入再次随机生成 → 地形每次不同,破坏/砍树随删除完全复原,杜绝背图。 */ public void ensurePVPWorldExists() { - World pvpWorld = Bukkit.getWorld(PVP_WORLD_NAME); + // 有人要进来了:取消任何待执行的删除任务(保留当前这一局地形) + cancelPendingDeletion(); - if (pvpWorld != null) { - // 世界已加载:检查版本,旧版本则重建 - if (readWorldVersion(pvpWorld) != PVP_WORLD_VERSION) { - plugin.getLogger().info("[PVP] 检测到已加载的PVP世界版本过旧,开始重建完整地形..."); - rebuildPVPWorld(); - } + World pvpWorld = Bukkit.getWorld(PVP_WORLD_NAME); + if (pvpWorld != null && !pvpWorld.getPlayers().isEmpty()) { + // 世界中仍有其他玩家在战斗 → 复用同一局地形,一起竞技(不重建) + plugin.getLogger().info("[PVP] PVP世界已有玩家在场,复用当前地形(不重建)"); return; } - // 世界未加载:检查磁盘上是否残留旧世界目录 + // 世界不存在,或世界存在但已无人活动 → 重新随机生成全新地形。 + // 这样满足"每次空场进入都随机生成地形";同时,服务器若自动从磁盘加载了 + // 旧版小平台(残留世界),这里会先卸载并删除磁盘目录,用随机地形替换它。 + if (pvpWorld != null) { + plugin.getLogger().info("[PVP] 检测到PVP世界当前无人,卸载旧世界以重新随机生成..."); + Bukkit.unloadWorld(pvpWorld, false); // false=不保存,直接丢弃旧地形(含旧版小平台) + deleteWorldFolder(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME)); + } + + // 兜底:确保磁盘残留目录被清理(含旧版小平台),再随机重新生成 File worldDir = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME); - if (worldDir.exists() && worldDir.isDirectory() - && readWorldVersionFromFile(worldDir) != PVP_WORLD_VERSION) { - plugin.getLogger().info("[PVP] 检测到磁盘上残留旧版PVP世界(小平台),删除并重建..."); + if (worldDir.exists() && worldDir.isDirectory()) { + plugin.getLogger().info("[PVP] 清理磁盘残留的PVP世界目录,准备随机重新生成..."); deleteWorldFolder(worldDir); } - // 创建全新PVP世界 + // 创建全新随机地形世界 createPVPWorld(); } /** - * 创建全新PVP世界(完整主世界地形) + * 创建全新PVP世界(每次随机地形) */ private void createPVPWorld() { - // 使用默认地形生成器(完整的树木、山丘、洞穴等),不设置自定义 generator + // ★ 每次使用随机种子 → 每次地形都不同,杜绝背图碾压 + long seed = worldSeedRandom.nextLong(); WorldCreator creator = new WorldCreator(PVP_WORLD_NAME); creator.environment(World.Environment.NORMAL); creator.type(WorldType.NORMAL); + creator.seed(seed); // 随机种子:不同seed = 不同生物群系与地形 World pvpWorld = creator.createWorld(); if (pvpWorld == null) { @@ -208,41 +224,30 @@ public class PVPArenaManager implements Listener { pvpWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false); pvpWorld.setTime(6000); // 中午 - // ★ 关键:先强制同步生成出生点周边区块,否则新世界区块尚未加载, - // getBlockAt 全为空气 → findSafeSpawn 返回 null → 误铺漂浮小平台 - preGenerateSpawnChunks(pvpWorld); + // ★ 关键:先强制同步生成出生点周边区块(半径3),确保地形已存在后再找安全出生点 + preGenerateSpawnChunks(pvpWorld, 3); // 设置出生点(在正常地形中找安全位置) Location spawnLoc = findSafeSpawn(pvpWorld); + if (spawnLoc == null) { + // 半径3内仍是虚空(极端情况),扩大到半径6再找一次 + preGenerateSpawnChunks(pvpWorld, 6); + spawnLoc = findSafeSpawn(pvpWorld); + } + if (spawnLoc != null) { pvpWorld.setSpawnLocation(spawnLoc.getBlockX(), spawnLoc.getBlockY(), spawnLoc.getBlockZ()); + plugin.getLogger().info("[PVP] 已创建PVP竞技场世界(随机地形 seed=" + seed + "): " + PVP_WORLD_NAME + + " 出生点=(" + spawnLoc.getBlockX() + "," + spawnLoc.getBlockY() + "," + spawnLoc.getBlockZ() + ")"); } else { + // 真正的虚空世界(很可能是服务端为 pvp_arena 强制了 void/flat 生成器): + // 退而求其次铺保底平台,但必须告警,因为这种世界不是"完整地形"。 pvpWorld.setSpawnLocation(0, 100, 0); - // 极端兜底:仍未找到地面才铺平台(正常情况不会触发) buildFallbackPlatform(pvpWorld); + plugin.getLogger().severe("[PVP] 警告:随机世界内找不到任何陆地(疑似服务端为 " + + PVP_WORLD_NAME + " 强制了 void/flat 生成器),已退化为小平台。" + + " 若是此原因,需在服务端 bukkit.yml / 世界管理插件中为该世界设置正常生成器。"); } - - // 写入版本标记,便于下次启动识别 - writeWorldVersion(pvpWorld); - - plugin.getLogger().info("[PVP] 已创建PVP竞技场世界(完整主世界地形): " + PVP_WORLD_NAME); - } - - /** - * 重建PVP世界:先把世界里的玩家传走并卸载,再删除磁盘目录后重新生成 - */ - private void rebuildPVPWorld() { - World old = Bukkit.getWorld(PVP_WORLD_NAME); - if (old != null) { - for (Player p : new ArrayList<>(old.getPlayers())) { - World main = Bukkit.getWorlds().get(0); - p.teleport(main.getSpawnLocation()); - p.sendMessage("§e[PVP] 竞技场正在重建,你已被传回主世界"); - } - Bukkit.unloadWorld(old, false); // false=不保存(即将删除) - } - deleteWorldFolder(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME)); - createPVPWorld(); } /** @@ -258,49 +263,69 @@ public class PVPArenaManager implements Listener { } /** - * 将当前世界版本写入世界目录下的版本文件 + * 取消待执行的"空场冷却删除世界"任务(有人重新进入时调用) */ - private void writeWorldVersion(World world) { - try { - File f = new File(Bukkit.getWorldContainer(), world.getName() + "/" + PVP_VERSION_FILE); - Files.write(f.toPath(), String.valueOf(PVP_WORLD_VERSION).getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - plugin.getLogger().warning("[PVP] 写入世界版本文件失败: " + e.getMessage()); + private void cancelPendingDeletion() { + if (pendingDeleteTask != null) { + pendingDeleteTask.cancel(); + pendingDeleteTask = null; } } /** - * 读取已加载世界的版本(无版本文件返回 -1 = 旧版) + * 若PVP世界已无人活动,启动5分钟空场冷却计时;冷却结束且仍无人则删除世界。 + * 冷却期内若有玩家重新进入(ensurePVPWorldExists 会 cancelPendingDeletion),则取消删除。 */ - private int readWorldVersion(World world) { - return readWorldVersionFromFile(new File(Bukkit.getWorldContainer(), world.getName())); + private void scheduleWorldDeletionIfEmpty() { + // 仍有活跃PVP玩家则不触发删除 + if (!inPVPArena.isEmpty()) return; + + cancelPendingDeletion(); + pendingDeleteTask = new BukkitRunnable() { + @Override + public void run() { + World w = Bukkit.getWorld(PVP_WORLD_NAME); + // 冷却结束时再次确认:仍有活跃玩家或仍有玩家滞留世界中则取消删除 + if (w == null || !inPVPArena.isEmpty() || !w.getPlayers().isEmpty()) { + pendingDeleteTask = null; + return; + } + plugin.getLogger().info("[PVP] PVP世界空场冷却结束,删除世界以释放资源..."); + deletePVPWorld(w); + pendingDeleteTask = null; + } + }.runTaskLater(plugin, PVP_WORLD_IDLE_DELETE_MS / 50L); // ticks = ms / 50 } /** - * 从世界目录读取版本文件 + * 卸载并删除PVP世界(含磁盘目录),下次进入将随机重新生成地形 */ - private int readWorldVersionFromFile(File worldDir) { - File f = new File(worldDir, PVP_VERSION_FILE); - if (!f.exists()) return -1; - try { - String s = new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8).trim(); - return Integer.parseInt(s); - } catch (Exception e) { - return -1; + private void deletePVPWorld(World world) { + if (world == null) return; + // 兜底:确保世界内无玩家(理论上冷却结束时已无人) + for (Player p : new ArrayList<>(world.getPlayers())) { + World main = Bukkit.getWorlds().get(0); + p.teleport(main.getSpawnLocation()); + p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界"); } + Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形) + deleteWorldFolder(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME)); + plugin.getLogger().info("[PVP] PVP世界已删除,下次进入将随机重新生成地形"); } /** - * 强制同步生成出生点周边区块(3x3),确保地形已存在后再寻找安全出生点 + * 强制同步生成出生点周边区块(半径 radius 个区块),确保地形已存在后再寻找安全出生点 * 必须在主线程调用(命令处理阶段) */ - private void preGenerateSpawnChunks(World world) { + private void preGenerateSpawnChunks(World world, int radius) { int cx = world.getSpawnLocation().getBlockX() >> 4; int cz = world.getSpawnLocation().getBlockZ() >> 4; - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { try { + // true=强制生成;再读取一个方块以触发区块填充,避免异步未就绪导致 findSafeSpawn 误判虚空 world.loadChunk(cx + dx, cz + dz, true); + world.getChunkAt(cx + dx, cz + dz).getBlock(8, 64, 8).getType(); } catch (Exception e) { plugin.getLogger().warning("[PVP] 生成区块失败: " + (cx + dx) + "," + (cz + dz)); } @@ -309,30 +334,37 @@ public class PVPArenaManager implements Listener { } /** - * 在世界中寻找安全的出生点(优先找最高的非空气且非流体方块作为地面) + * 在世界中寻找安全的出生点(螺旋向外搜索最高的非空气且非流体方块作为地面) + * 搜索半径较大(SEARCH_RADIUS 格),即使出生点恰好落在海洋/洞穴也能找到附近陆地, + * 杜绝"整片虚空→误铺小平台"。 */ private Location findSafeSpawn(World world) { int baseX = world.getSpawnLocation().getBlockX(); int baseZ = world.getSpawnLocation().getBlockZ(); - // 在出生点周边 17x17 范围内寻找最高的固体非流体地面(优先高地,避开海洋) + final int SEARCH_RADIUS = 32; // 覆盖出生点周边 64x64 格范围 int bestX = baseX, bestZ = baseZ, bestY = -1; - for (int dx = -8; dx <= 8; dx++) { - for (int dz = -8; dz <= 8; dz++) { - int x = baseX + dx, z = baseZ + dz; - for (int y = 255; y >= 1; y--) { - Block block = world.getBlockAt(x, y, z); - Material type = block.getType(); - if (!type.isAir() && !isFluid(type)) { - if (y > bestY) { bestY = y; bestX = x; bestZ = z; } - break; + for (int r = 0; r <= SEARCH_RADIUS; r++) { + // 仅遍历半径为 r 的方形环 + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue; + int x = baseX + dx, z = baseZ + dz; + for (int y = 255; y >= 1; y--) { + Block block = world.getBlockAt(x, y, z); + Material type = block.getType(); + if (!type.isAir() && !isFluid(type)) { + if (y > bestY) { bestY = y; bestX = x; bestZ = z; } + break; + } } } } + if (bestY >= 0) break; // 找到陆地立即停止 } if (bestY >= 0) { return new Location(world, bestX + 0.5, bestY + 1, bestZ + 0.5); } - return null; // 未找到安全地面 + return null; // 未找到安全地面(疑似虚空世界) } /** @@ -456,12 +488,16 @@ public class PVPArenaManager implements Listener { plugin.getLogger().info("[PVP] 玩家 " + playerName + " 离开PVP竞技场" + (isDisconnect ? "(断线)" : "")); + + // 退场后若已无人,启动空场冷却;冷却结束且仍无人则删除世界(下次随机重生) + scheduleWorldDeletionIfEmpty(); } /** * 玩家退出PVP世界(由 PlayerChangedWorldEvent 触发) */ public void onPlayerExitPVPWorld(Player player) { + // 强制离开(内部已统一触发空场冷却删除逻辑) forceLeaveArena(player, false); } @@ -520,7 +556,7 @@ public class PVPArenaManager implements Listener { Player player = event.getPlayer(); // 检查是否有待还原的PVP背包备份 - String[] data = db.getInventoryBackup(player.getName()); + String[] data = db.getPvpInventoryBackup(player.getName()); if (data != null && !data[0].isEmpty()) { plugin.getLogger().info("[PVP] 玩家 " + player.getName() + " 上次在PVP中断线,正在还原背包..."); @@ -535,7 +571,7 @@ public class PVPArenaManager implements Listener { ); inventoryBackups.put(player.getName(), backup); restoreInventory(player); - db.deleteInventoryBackup(player.getName()); + db.deletePvpInventoryBackup(player.getName()); player.sendMessage("§a§l检测到你上次在PVP中断线,背包已自动恢复"); }, 2L); } @@ -564,6 +600,8 @@ public class PVPArenaManager implements Listener { // 还没确认装备 → 身上还是原背包,不需要特殊处理 inPVPArena.remove(player.getName()); equipmentConfirmed.remove(player.getName()); + // 若因此成为最后一名退场者,启动空场冷却删除 + scheduleWorldDeletionIfEmpty(); } } } @@ -587,7 +625,7 @@ public class PVPArenaManager implements Listener { inventoryBackups.put(playerName, backup); // 同时保存到数据库(用于断线还原) - db.saveInventoryBackup( + db.savePvpInventoryBackup( playerName, serializeItems(backup.contents), serializeItems(backup.armor), @@ -610,7 +648,7 @@ public class PVPArenaManager implements Listener { // 缓存没有则从数据库获取 if (backup == null) { - String[] data = db.getInventoryBackup(playerName); + String[] data = db.getPvpInventoryBackup(playerName); if (data != null) { backup = new InventoryBackup( deserializeItems(data[0]), @@ -620,7 +658,7 @@ public class PVPArenaManager implements Listener { Float.parseFloat(data[4]) ); // 删除数据库备份 - db.deleteInventoryBackup(playerName); + db.deletePvpInventoryBackup(playerName); } } @@ -632,7 +670,7 @@ public class PVPArenaManager implements Listener { player.setExp(backup.exp); // ★ 无论备份来自缓存还是数据库,还原后必须清理数据库备份, // 否则断线重连时 onPlayerJoin 会再次覆盖玩家当前背包(序列化生效后的隐藏坑) - db.deleteInventoryBackup(playerName); + db.deletePvpInventoryBackup(playerName); plugin.getLogger().info("[PVP] 已还原玩家 " + playerName + " 的背包"); } else { plugin.getLogger().warning("[PVP] 未找到玩家 " + playerName + " 的背包备份"); diff --git a/src/main/java/Sdf1_login/TeleportManager.java b/src/main/java/Sdf1_login/TeleportManager.java index 16a9001..d70080e 100644 --- a/src/main/java/Sdf1_login/TeleportManager.java +++ b/src/main/java/Sdf1_login/TeleportManager.java @@ -994,7 +994,11 @@ public class TeleportManager implements Listener { String key = args[0].toLowerCase(); String val = args[1]; int seconds = plugin.getConfigMgr().parseIntFromString(val); - + if (seconds < 30) { + player.sendMessage("§c[传送管理] 有效时间过小,已强制为 30 秒下限(防止请求秒过期)"); + seconds = 30; + } + if ("valid".equals(key)) { plugin.getConfigMgr().tpRequestValidSeconds = seconds; plugin.getConfigMgr().saveSettings();