修复PVP竞技场三项问题:世界重建、进入流程与背包序列化
- 加入世界版本检测机制,磁盘残留的旧版小平台世界自动删除并重建为完整主世界地形 - 补全 PlayerChangedWorldEvent 事件处理器,进入/离开PVP世界时正确触发装备选择GUI与背包还原 - joinArena 传送后立即直接触发进入流程,杜绝跳过选装备清背包阶段 - 实现背包物品序列化(BukkitObjectOutputStream+Base64),真正支持清背包后还原玩家物品
This commit is contained in:
@@ -6,14 +6,22 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
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;
|
||||
|
||||
@@ -45,6 +53,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 final Set<String> inPVPArena = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@@ -145,45 +157,136 @@ public class PVPArenaManager implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并创建PVP世界(使用正常主世界地形生成器)
|
||||
* 检查并创建/重建PVP世界(使用正常主世界地形生成器)
|
||||
*
|
||||
* 关键修复:旧版代码生成的"小平台"世界文件夹仍残留在服务器磁盘,
|
||||
* 原逻辑检测到世界已存在就直接 return、永不重建。现在通过版本文件判断,
|
||||
* 若磁盘上的世界版本过旧(或无版本文件=旧版小平台),则卸载并删除后重新生成完整地形。
|
||||
*/
|
||||
public void ensurePVPWorldExists() {
|
||||
World pvpWorld = Bukkit.getWorld(PVP_WORLD_NAME);
|
||||
if (pvpWorld == null) {
|
||||
// 创建PVP世界 — 使用默认地形生成器(完整的树木、山丘、洞穴等)
|
||||
// 不设置自定义generator,让Bukkit使用服务器默认的世界生成器
|
||||
WorldCreator creator = new WorldCreator(PVP_WORLD_NAME);
|
||||
creator.environment(World.Environment.NORMAL);
|
||||
creator.type(WorldType.NORMAL);
|
||||
// 不设置 generator → 使用服务器的默认 ChunkGenerator(通常是带生物群系的地形)
|
||||
|
||||
pvpWorld = creator.createWorld();
|
||||
|
||||
if (pvpWorld != null) {
|
||||
// 设置PVP世界规则
|
||||
pvpWorld.setPVP(true);
|
||||
pvpWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
pvpWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
pvpWorld.setTime(6000); // 中午
|
||||
|
||||
// ★ 关键修复:先强制同步生成出生点周边区块,否则新世界区块尚未加载,
|
||||
// getBlockAt 全为空气 → findSafeSpawn 返回 null → 误铺漂浮小平台
|
||||
preGenerateSpawnChunks(pvpWorld);
|
||||
|
||||
// 设置出生点(在正常地形中找安全位置)
|
||||
Location spawnLoc = findSafeSpawn(pvpWorld);
|
||||
if (spawnLoc != null) {
|
||||
pvpWorld.setSpawnLocation(spawnLoc.getBlockX(), spawnLoc.getBlockY(), spawnLoc.getBlockZ());
|
||||
} else {
|
||||
pvpWorld.setSpawnLocation(0, 100, 0);
|
||||
// 极端兜底:仍未找到地面才铺平台(正常情况不会触发)
|
||||
buildFallbackPlatform(pvpWorld);
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[PVP] 已创建PVP竞技场世界(完整主世界地形): " + PVP_WORLD_NAME);
|
||||
} else {
|
||||
plugin.getLogger().severe("[PVP] 无法创建PVP竞技场世界!");
|
||||
if (pvpWorld != null) {
|
||||
// 世界已加载:检查版本,旧版本则重建
|
||||
if (readWorldVersion(pvpWorld) != PVP_WORLD_VERSION) {
|
||||
plugin.getLogger().info("[PVP] 检测到已加载的PVP世界版本过旧,开始重建完整地形...");
|
||||
rebuildPVPWorld();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 世界未加载:检查磁盘上是否残留旧世界目录
|
||||
File worldDir = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
|
||||
if (worldDir.exists() && worldDir.isDirectory()
|
||||
&& readWorldVersionFromFile(worldDir) != PVP_WORLD_VERSION) {
|
||||
plugin.getLogger().info("[PVP] 检测到磁盘上残留旧版PVP世界(小平台),删除并重建...");
|
||||
deleteWorldFolder(worldDir);
|
||||
}
|
||||
|
||||
// 创建全新PVP世界
|
||||
createPVPWorld();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建全新PVP世界(完整主世界地形)
|
||||
*/
|
||||
private void createPVPWorld() {
|
||||
// 使用默认地形生成器(完整的树木、山丘、洞穴等),不设置自定义 generator
|
||||
WorldCreator creator = new WorldCreator(PVP_WORLD_NAME);
|
||||
creator.environment(World.Environment.NORMAL);
|
||||
creator.type(WorldType.NORMAL);
|
||||
|
||||
World pvpWorld = creator.createWorld();
|
||||
if (pvpWorld == null) {
|
||||
plugin.getLogger().severe("[PVP] 无法创建PVP竞技场世界!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置PVP世界规则
|
||||
pvpWorld.setPVP(true);
|
||||
pvpWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
pvpWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
pvpWorld.setTime(6000); // 中午
|
||||
|
||||
// ★ 关键:先强制同步生成出生点周边区块,否则新世界区块尚未加载,
|
||||
// getBlockAt 全为空气 → findSafeSpawn 返回 null → 误铺漂浮小平台
|
||||
preGenerateSpawnChunks(pvpWorld);
|
||||
|
||||
// 设置出生点(在正常地形中找安全位置)
|
||||
Location spawnLoc = findSafeSpawn(pvpWorld);
|
||||
if (spawnLoc != null) {
|
||||
pvpWorld.setSpawnLocation(spawnLoc.getBlockX(), spawnLoc.getBlockY(), spawnLoc.getBlockZ());
|
||||
} else {
|
||||
pvpWorld.setSpawnLocation(0, 100, 0);
|
||||
// 极端兜底:仍未找到地面才铺平台(正常情况不会触发)
|
||||
buildFallbackPlatform(pvpWorld);
|
||||
}
|
||||
|
||||
// 写入版本标记,便于下次启动识别
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除世界目录
|
||||
*/
|
||||
private void deleteWorldFolder(File folder) {
|
||||
if (folder == null || !folder.exists()) return;
|
||||
File[] children = folder.listFiles();
|
||||
if (children != null) {
|
||||
for (File c : children) deleteWorldFolder(c);
|
||||
}
|
||||
folder.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前世界版本写入世界目录下的版本文件
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取已加载世界的版本(无版本文件返回 -1 = 旧版)
|
||||
*/
|
||||
private int readWorldVersion(World world) {
|
||||
return readWorldVersionFromFile(new File(Bukkit.getWorldContainer(), world.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从世界目录读取版本文件
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,16 +312,26 @@ public class PVPArenaManager implements Listener {
|
||||
* 在世界中寻找安全的出生点(优先找最高的非空气且非流体方块作为地面)
|
||||
*/
|
||||
private Location findSafeSpawn(World world) {
|
||||
int spawnX = world.getSpawnLocation().getBlockX();
|
||||
int spawnZ = world.getSpawnLocation().getBlockZ();
|
||||
// 从顶部向下扫描整列,找到最高的固体地面(跳过空气与流体)
|
||||
for (int y = 255; y >= 1; y--) {
|
||||
Block block = world.getBlockAt(spawnX, y, spawnZ);
|
||||
Material type = block.getType();
|
||||
if (!type.isAir() && !isFluid(type)) {
|
||||
return new Location(world, spawnX + 0.5, y + 1, spawnZ + 0.5);
|
||||
int baseX = world.getSpawnLocation().getBlockX();
|
||||
int baseZ = world.getSpawnLocation().getBlockZ();
|
||||
// 在出生点周边 17x17 范围内寻找最高的固体非流体地面(优先高地,避开海洋)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestY >= 0) {
|
||||
return new Location(world, bestX + 0.5, bestY + 1, bestZ + 0.5);
|
||||
}
|
||||
return null; // 未找到安全地面
|
||||
}
|
||||
|
||||
@@ -352,6 +465,24 @@ public class PVPArenaManager implements Listener {
|
||||
forceLeaveArena(player, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家切换世界事件 — 进入/离开PVP世界时触发装备选择GUI与背包还原
|
||||
*/
|
||||
@EventHandler
|
||||
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
String from = event.getFrom().getName();
|
||||
String to = player.getWorld().getName();
|
||||
|
||||
if (to.equals(PVP_WORLD_NAME) && !from.equals(PVP_WORLD_NAME)) {
|
||||
// 进入PVP世界 → 打开装备选择GUI(此时玩家仍持有原背包)
|
||||
onPlayerEnterPVPWorld(player);
|
||||
} else if (from.equals(PVP_WORLD_NAME) && !to.equals(PVP_WORLD_NAME)) {
|
||||
// 离开PVP世界 → 回收装备并还原背包
|
||||
onPlayerExitPVPWorld(player);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 事件处理器 ====================
|
||||
|
||||
/**
|
||||
@@ -499,6 +630,9 @@ public class PVPArenaManager implements Listener {
|
||||
player.getInventory().setItemInOffHand(backup.offHand);
|
||||
player.setLevel(backup.expLevel);
|
||||
player.setExp(backup.exp);
|
||||
// ★ 无论备份来自缓存还是数据库,还原后必须清理数据库备份,
|
||||
// 否则断线重连时 onPlayerJoin 会再次覆盖玩家当前背包(序列化生效后的隐藏坑)
|
||||
db.deleteInventoryBackup(playerName);
|
||||
plugin.getLogger().info("[PVP] 已还原玩家 " + playerName + " 的背包");
|
||||
} else {
|
||||
plugin.getLogger().warning("[PVP] 未找到玩家 " + playerName + " 的背包备份");
|
||||
@@ -668,6 +802,9 @@ public class PVPArenaManager implements Listener {
|
||||
// 传送至出生点
|
||||
Location spawn = pvpWorld.getSpawnLocation();
|
||||
player.teleport(spawn);
|
||||
// ★ 关键修复:直接触发进入流程,确保装备选择GUI必然弹出,
|
||||
// 不再单纯依赖 PlayerChangedWorldEvent 的异步时机(避免"直接进去跳过选装备")
|
||||
onPlayerEnterPVPWorld(player);
|
||||
player.sendMessage("§a§l正在前往PVP竞技场...");
|
||||
}
|
||||
|
||||
@@ -710,25 +847,81 @@ public class PVPArenaManager implements Listener {
|
||||
inventoryBackups.clear();
|
||||
}
|
||||
|
||||
// ==================== 序列化(TODO: 完善实现)====================
|
||||
// ==================== 序列化(基于 BukkitObjectStream + Base64)====================
|
||||
|
||||
/**
|
||||
* 序列化整个物品数组(背包/盔甲)为 Base64 字符串
|
||||
*/
|
||||
private String serializeItems(ItemStack[] items) {
|
||||
// TODO: 实现物品序列化(可用Base64+Minecraft NBT)
|
||||
return "";
|
||||
if (items == null) return "";
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream oos = new BukkitObjectOutputStream(baos);
|
||||
oos.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
oos.writeObject(item); // null 元素安全
|
||||
}
|
||||
oos.close();
|
||||
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[PVP] 背包序列化失败: " + e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化物品数组
|
||||
*/
|
||||
private ItemStack[] deserializeItems(String data) {
|
||||
// TODO: 实现物品反序列化
|
||||
return new ItemStack[0];
|
||||
if (data == null || data.isEmpty()) return new ItemStack[0];
|
||||
try {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream ois = new BukkitObjectInputStream(bais);
|
||||
int len = ois.readInt();
|
||||
ItemStack[] items = new ItemStack[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
Object obj = ois.readObject();
|
||||
items[i] = (obj instanceof ItemStack) ? (ItemStack) obj : null;
|
||||
}
|
||||
ois.close();
|
||||
return items;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[PVP] 背包反序列化失败: " + e.getMessage());
|
||||
return new ItemStack[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化单个物品(副手)
|
||||
*/
|
||||
private String serializeItem(ItemStack item) {
|
||||
// TODO: 实现物品序列化
|
||||
return "";
|
||||
if (item == null) return "";
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream oos = new BukkitObjectOutputStream(baos);
|
||||
oos.writeObject(item);
|
||||
oos.close();
|
||||
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[PVP] 物品序列化失败: " + e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化单个物品(副手)
|
||||
*/
|
||||
private ItemStack deserializeItem(String data) {
|
||||
// TODO: 实现物品反序列化
|
||||
return null;
|
||||
if (data == null || data.isEmpty()) return null;
|
||||
try {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream ois = new BukkitObjectInputStream(bais);
|
||||
Object obj = ois.readObject();
|
||||
ois.close();
|
||||
return (obj instanceof ItemStack) ? (ItemStack) obj : null;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[PVP] 物品反序列化失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user