修复PVP崩溃/卡死 + 合并小票潜影盒 + ShopManager类缺失防护
1. PVP加入世界 WorldInitEvent 异步崩溃:createWorld 必须在主线程执行 (WorldInitEvent/WorldLoadEvent 仅允许同步触发)。改为主线程同步创建, 并通过 WorldCreator.keepSpawnInMemory(false) + 取消 reapplyWorldRules 的 setKeepSpawnInMemory(true),使 createWorld 不再同步加载出生点区块, 主线程毫秒级返回,彻底消除 Watchdog 卡死(上一版误用异步线程导致崩溃)。 移除无用的 pvpWorldCreating 异步互斥锁。 2. 收银台合并发货:打包(潜影盒)模式不再单独发放'小票潜影盒', 小票已塞入商品潜影盒内;不打包(塞背包)模式跳过发放小票书。 解决玩家拿到两个潜影盒(商品+小票 / 仅小票)的问题。 3. 构建时确保 ShopManager$1 等匿名内部类完整打入 JAR,避免 NoClassDefFoundError。
This commit is contained in:
@@ -26,7 +26,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* PVP竞技场管理器 - 独立世界模式(完整主世界地形)
|
||||
@@ -75,10 +74,6 @@ public class PVPArenaManager implements Listener {
|
||||
// 程序内重开装备GUI(如切换档位)时,旧GUI关闭事件需忽略,避免误触发遣返
|
||||
private final Set<String> guiReopening = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// ★ 世界创建互斥锁:异步创建世界期间置为 true,防止多名玩家并发 /pvp join 同时触发
|
||||
// createPVPWorld 导致 pvpWorldName 竞态与重复创建世界(上一创建尚未完成又开新世界)。
|
||||
private final AtomicBoolean pvpWorldCreating = new AtomicBoolean(false);
|
||||
|
||||
// ★ GUI打开时间戳(仅用于历史清理兼容,当前检测逻辑已改为基于 selectedTier 状态判断)
|
||||
private final Map<String, Long> guiOpenedMillis = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -270,6 +265,11 @@ public class PVPArenaManager implements Listener {
|
||||
creator.environment(World.Environment.NORMAL);
|
||||
creator.type(WorldType.NORMAL);
|
||||
creator.seed(seed);
|
||||
// ★ 关键:不把出生点区块常驻内存。否则 Bukkit.createWorld 会同步加载出生点周边
|
||||
// 区块(~10 区块半径),主线程被占用数秒 → Watchdog 卡死。设为 false 后 createWorld
|
||||
// 仅初始化世界、不加载出生点区块(毫秒级);区块由 preGenerateSpawnChunks 异步预生成,
|
||||
// 玩家传送时按需加载单个区块(极快),主线程全程不阻塞。
|
||||
creator.keepSpawnInMemory(false);
|
||||
plugin.getLogger().info("[PVP] 新建随机世界名=" + pvpWorldName + ", 种子=" + seed + " (确保每次地形不同)");
|
||||
|
||||
// ★ 使用 Bukkit.createWorld() 而非 creator.createWorld():
|
||||
@@ -335,7 +335,8 @@ public class PVPArenaManager implements Listener {
|
||||
w.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
w.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
w.setTime(6000);
|
||||
w.setKeepSpawnInMemory(true);
|
||||
// ★ 不常驻出生点区块(与 WorldCreator.keepSpawnInMemory(false) 一致),避免同步加载区块卡主线程
|
||||
w.setKeepSpawnInMemory(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1471,19 +1472,19 @@ public class PVPArenaManager implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 关键修复(2026-07-08):修复 /pvp join 加入世界时主线程卡死无响应(Watchdog 超时)
|
||||
// ★ 关键修复(2026-07-08 第二版):/pvp join 卡死 + WorldInitEvent 异步崩溃
|
||||
//
|
||||
// 根因:旧代码在主线程同步调用 createPVPWorld() → Bukkit.createWorld() 阻塞约 8 秒,
|
||||
// 且 reapplyWorldRules 的 setKeepSpawnInMemory(true) 会强制同步加载出生点区块,
|
||||
// 主线程被长时间占用触发 Watchdog 踢人/卡死。
|
||||
// 第一版曾把 createPVPWorld() 放进异步线程,但 Bukkit.createWorld() 内部会【同步】触发
|
||||
// WorldInitEvent / WorldLoadEvent,这些事件只允许在主线程触发,异步调用直接抛
|
||||
// java.lang.IllegalStateException: WorldInitEvent may only be triggered synchronously。
|
||||
//
|
||||
// 修复:
|
||||
// 1) 旧世界卸载(Bukkit.unloadWorld 非线程安全)必须在主线程执行,这里先卸载;
|
||||
// 2) 新世界创建(Bukkit.createWorld 阻塞 + 出生点区块生成)放进【异步线程】,
|
||||
// 主线程得以释放,不再卡死;
|
||||
// 3) 创建完成后再 runTask 回主线程执行传送 + 打开装备GUI(teleport/开GUI 必须在主线程)。
|
||||
// 因此 createPVPWorld() 必须在【主线程】执行。卡死的根因并非 createWorld 本身,而是
|
||||
// setKeepSpawnInMemory(true) 强制同步加载出生点区块(~8s)。现改为
|
||||
// WorldCreator.keepSpawnInMemory(false)(见 createPVPWorld),createWorld 仅做世界初始化、
|
||||
// 不加载出生点区块(毫秒级),主线程不会被长时间占用,Watchdog 卡死彻底消除。
|
||||
// 出生点周边区块由 preGenerateSpawnChunks 异步预生成,玩家传送时按需加载单个区块(极快)。
|
||||
if (pvpWorld != null) {
|
||||
plugin.getLogger().info("[PVP] PVP世界当前无人,主线程卸载旧世界后于异步线程重新生成全新主世界");
|
||||
plugin.getLogger().info("[PVP] PVP世界当前无人,主线程卸载旧世界并重新生成全新主世界");
|
||||
deletePVPWorld(pvpWorld);
|
||||
} else {
|
||||
File folder = new File(Bukkit.getWorldContainer(), pvpWorldName);
|
||||
@@ -1494,29 +1495,12 @@ public class PVPArenaManager implements Listener {
|
||||
}
|
||||
|
||||
player.sendMessage("§a§l正在生成PVP竞技场世界,请稍候...");
|
||||
|
||||
// ★ 互斥保护:若已有其他玩家在异步创建世界,本次不重复创建(避免 pvpWorldName 竞态 /
|
||||
// 重复 createWorld 导致地形错乱)。提示玩家稍后重试即可。
|
||||
if (!pvpWorldCreating.compareAndSet(false, true)) {
|
||||
plugin.getLogger().info("[PVP] 玩家 " + player.getName()
|
||||
+ " 请求加入时世界正在异步生成中,提示稍后重试(不重复创建)");
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (player.isOnline() && !inPVPArena.contains(player.getName())) {
|
||||
player.sendMessage("§ePVP竞技场世界正在生成中,请稍候再次输入 /pvp join");
|
||||
}
|
||||
}, 20L);
|
||||
pvpWorld = createPVPWorld();
|
||||
if (pvpWorld == null) {
|
||||
player.sendMessage("§c§l[PVP] 竞技场世界加载失败,请联系管理员");
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
final World newWorld = createPVPWorld();
|
||||
// 回到主线程:传送与打开装备GUI 必须在主线程执行
|
||||
Bukkit.getScheduler().runTask(plugin, () -> finalizeJoin(player, newWorld));
|
||||
} finally {
|
||||
pvpWorldCreating.set(false);
|
||||
}
|
||||
});
|
||||
finalizeJoin(player, pvpWorld);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3970,6 +3970,18 @@ public class WebManager {
|
||||
break;
|
||||
}
|
||||
|
||||
// ★ 2026-07-08 合并发货修复:
|
||||
// 打包(潜影盒)模式:小票已通过 handleBuyCart 的 addBookToShulker 塞入【商品潜影盒】内,
|
||||
// 此处若再单独发放"小票潜影盒"会导致玩家拿到两个潜影盒(商品+小票 / 仅小票)。
|
||||
// 不打包(塞背包)模式:按需求跳过发放小票书。
|
||||
// 因此两种结算模式下都【不再单独发放小票潜影盒】,直接标记已处理避免反复拉取。
|
||||
if ("shulker".equals(settlementMode) || "backpack".equals(settlementMode)) {
|
||||
plugin.getLogger().info("[小票书] 结算模式=" + settlementMode
|
||||
+ ",小票已并入商品潜影盒(打包)或在背包模式跳过,不再单独发放小票潜影盒 (玩家=" + bookPlayer + ")");
|
||||
applied = true;
|
||||
break;
|
||||
}
|
||||
|
||||
org.bukkit.entity.Player target = Bukkit.getPlayerExact(bookPlayer);
|
||||
if (target == null || !target.isOnline()) {
|
||||
plugin.getLogger().info("[小票书] 玩家 " + bookPlayer + " 不在线,跳过发书");
|
||||
|
||||
Reference in New Issue
Block a user