修复: PVP世界旧虚空世界Windows删除锁致地形失效(重命名回收站强制刷新NORMAL地形) + /weblogin命令主线程同步HTTP卡服改为异步

This commit is contained in:
2026-07-07 14:26:45 +08:00
parent dd7166d73e
commit 49461ac042
2 changed files with 102 additions and 38 deletions
+53 -3
View File
@@ -221,17 +221,24 @@ public class PVPArenaManager implements Listener {
// 世界不存在,或世界存在但已无人活动 → 重新随机生成全新地形。
// 这样满足"每次空场进入都随机生成地形";同时,服务器若自动从磁盘加载了
// 旧版小平台(残留世界),这里会先卸载并删除磁盘目录,用随机地形替换它。
// ★ 关键修复:先清理历史回收站目录,避免磁盘无限增长
cleanupOldTrashWorlds();
if (pvpWorld != null) {
plugin.getLogger().info("[PVP] 检测到PVP世界当前无人,卸载旧世界以重新随机生成...");
Bukkit.unloadWorld(pvpWorld, false); // false=不保存,直接丢弃旧地形(含旧版小平台)
deleteWorldFolder(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME));
// ★ 关键修复:Windows 上 unloadWorld 后 region 文件可能仍被 OS 锁定,
// File.delete() 会静默失败导致旧虚空世界残留,并被 createWorld() 原样重新加载
// (findSafeSpawn=null → 退化为手工小岛地形)。改用"重命名到回收站"移走,
// rename 在文件锁定时仍可靠成功,从而让 createWorld() 生成全新 NORMAL 主世界。
moveWorldToTrash(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME));
}
// 兜底:确保磁盘残留目录被清理(含旧版小平台),再随机重新生成
File worldDir = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
if (worldDir.exists() && worldDir.isDirectory()) {
plugin.getLogger().info("[PVP] 清理磁盘残留的PVP世界目录,准备随机重新生成...");
deleteWorldFolder(worldDir);
moveWorldToTrash(worldDir);
}
// 创建全新随机地形世界
@@ -442,6 +449,48 @@ public class PVPArenaManager implements Listener {
folder.delete();
}
/**
* 将旧世界目录"重命名"移入回收站(而非直接删除)。
* ★ 原因:Windows 上 unloadWorld 后 region 文件可能仍被 OS 锁定,
* File.delete() 会静默失败导致旧虚空世界残留;而 File.renameTo() 移动目录在
* 文件锁定时仍可靠成功。移走后 createWorld() 因目录不存在而生成全新 NORMAL 主世界地形。
*/
private void moveWorldToTrash(File folder) {
if (folder == null || !folder.exists()) return;
File parent = folder.getParentFile();
String base = folder.getName();
long ts = System.currentTimeMillis();
File trash = new File(parent, base + "_trash_" + ts);
int i = 0;
while (trash.exists()) {
i++;
trash = new File(parent, base + "_trash_" + ts + "_" + i);
}
if (folder.renameTo(trash)) {
plugin.getLogger().info("[PVP] 旧世界已移入回收站(避免Windows删除锁): " + trash.getName());
} else {
// 极少数情况下重命名也失败,退回递归删除
plugin.getLogger().warning("[PVP] 旧世界重命名失败,退回递归删除: " + base);
deleteWorldFolder(folder);
}
}
/**
* 清理历史回收站目录(pvp_arena_trash_*),避免磁盘无限增长。
* best-effort,失败不阻断主流程。
*/
private void cleanupOldTrashWorlds() {
File container = Bukkit.getWorldContainer();
if (container == null || !container.isDirectory()) return;
File[] list = container.listFiles();
if (list == null) return;
for (File f : list) {
if (f.isDirectory() && f.getName().startsWith(PVP_WORLD_NAME + "_trash_")) {
deleteWorldFolder(f);
}
}
}
/**
* 取消待执行的"空场冷却删除世界"任务(有人重新进入时调用)
*/
@@ -489,7 +538,8 @@ public class PVPArenaManager implements Listener {
p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界");
}
Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形)
deleteWorldFolder(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME));
// ★ 同样用重命名移走,避免 Windows 删除锁导致旧世界残留
moveWorldToTrash(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME));
plugin.getLogger().info("[PVP] PVP世界已删除,下次进入将随机重新生成地形");
}
+49 -35
View File
@@ -4999,45 +4999,59 @@ public class WebManager {
// ★ 使用push_player_login_status端点,带上online=0(玩家当前不在游戏里执行/weblogin,只是要token)
// 实际上玩家就在游戏里,所以online=1
// ★ 使用push_player_login_status端点,带上online=1(玩家就在游戏里,已认证)
boolean isRegistered = plugin.getDb().userExists(playerName);
boolean syncSuccess = false;
try {
// ★ 获取玩家真实IP地址
String playerIp = "";
java.net.InetSocketAddress addr = sender.getAddress();
if (addr != null && addr.getAddress() != null) {
playerIp = addr.getAddress().getHostAddress();
}
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
+ "&web_token=" + java.net.URLEncoder.encode(token, "UTF-8")
+ "&expire_seconds=" + tokenExpireSeconds
+ "&online=1"
+ "&registered=" + (isRegistered ? "1" : "0")
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8")
+ "&login_verified=1"; // ★ 玩家在游戏里已认证,直接告诉PHP放行
final String tokenFinal = token;
String response = doGet(urlStr);
if (response != null) {
plugin.getLogger().info("[Web通信] push_player_login_status结果: " + response);
if (response.contains("\"success\":true")) {
syncSuccess = true;
// ★★★ 致命修复:原实现在主线程(命令处理)同步调用 doGet() → HttpClient.send(),
// 当 Web 后端不可达时会阻塞服务端主线程 10 秒以上,触发 "server has not responded
// for 10 seconds" 线程转储并冻结整个服务器。现改为完全异步执行 HTTP,
// 结果通过 runTask 回到主线程再向玩家反馈。 ★★★
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
boolean syncSuccess = false;
try {
// ★ 获取玩家真实IP地址
String playerIp = "";
java.net.InetSocketAddress addr = sender.getAddress();
if (addr != null && addr.getAddress() != null) {
playerIp = addr.getAddress().getHostAddress();
}
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 同步Web登录Token失败: " + e.getMessage());
}
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
+ "&web_token=" + java.net.URLEncoder.encode(tokenFinal, "UTF-8")
+ "&expire_seconds=" + tokenExpireSeconds
+ "&online=1"
+ "&registered=" + (isRegistered ? "1" : "0")
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8")
+ "&login_verified=1"; // ★ 玩家在游戏里已认证,直接告诉PHP放行
if (syncSuccess) {
sender.sendMessage("§a§l===== Web登录 =====");
sender.sendMessage("§e请点击链接登录Web端:");
sender.sendMessage("§b" + webBaseUrl + "/login.php?token=" + token);
sender.sendMessage("§7有效期: " + tokenExpireSeconds + "秒 | 一次性使用");
} else {
sender.sendMessage("§c[Web] Token同步失败,请检查Web后端是否可访问");
sender.sendMessage("§7当前后端地址: " + webBaseUrl);
}
String response = doGet(urlStr);
if (response != null) {
plugin.getLogger().info("[Web通信] push_player_login_status结果: " + response);
if (response.contains("\"success\":true")) {
syncSuccess = true;
}
}
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 同步Web登录Token失败: " + e.getMessage());
}
// ★ 回到主线程向玩家反馈结果(sendMessage 必须在主线程调用)
final boolean ok = syncSuccess;
Bukkit.getScheduler().runTask(plugin, () -> {
if (sender instanceof Player && !((Player) sender).isOnline()) return;
if (ok) {
sender.sendMessage("§a§l===== Web登录 =====");
sender.sendMessage("§e请点击链接登录Web端:");
sender.sendMessage("§b" + webBaseUrl + "/login.php?token=" + tokenFinal);
sender.sendMessage("§7有效期: " + tokenExpireSeconds + "秒 | 一次性使用");
} else {
sender.sendMessage("§c[Web] Token同步失败,请检查Web后端是否可访问");
sender.sendMessage("§7当前后端地址: " + webBaseUrl);
}
});
});
}
/**