修复PVP背包被覆盖问题(独立备份表+未退场不覆盖)、优化随机地形生成、TPA传送请求有效期下限保护

This commit is contained in:
2026-07-06 21:48:43 +08:00
parent 122850f6f4
commit 4cdd32913e
4 changed files with 265 additions and 91 deletions
@@ -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
}
@@ -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 {
+128 -90
View File
@@ -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<String> inPVPArena = ConcurrentHashMap.newKeySet();
@@ -66,6 +66,12 @@ public class PVPArenaManager implements Listener {
// 玩家背包备份缓存 (玩家名 -> 备份数据)
private final Map<String, InventoryBackup> inventoryBackups = new ConcurrentHashMap<>();
// 待执行的"空场冷却删除世界"定时任务(有人重新进入时取消)
private BukkitTask pendingDeleteTask = null;
// 随机地形种子生成器(每次创建世界用新种子 → 地形不同)
private final Random worldSeedRandom = new Random();
// PVP装备列表 (管理员可配置)
private final List<ItemStack> 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 + " 的背包备份");
@@ -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();