修复:PVP世界改为纯NORMAL主世界(插件零干预地形,仅只读定位出生点);商店保存改为合并感知,避免服务器关闭时覆盖管理员手工新增的商品/分类

This commit is contained in:
2026-07-07 21:25:57 +08:00
parent 3ebd8f0d5f
commit 4297e0e02d
2 changed files with 84 additions and 29 deletions
+39 -29
View File
@@ -246,36 +246,33 @@ public class PVPArenaManager implements Listener {
}
/**
* 创建全新PVP世界 — 自然主世界地形(完整主世界:山丘/水域/洞穴/生物群系)
* 创建全新PVP世界 — 纯 NORMAL 主世界地形,插件全程不插手地形生成。
*
* ★ 2026-07-07 第四次改进:种子重试机制
* NORMAL 地形生成器是随机的,(0,0) 可能落在海洋中(findSafeSpawn score=1),
* 导致玩家出生在孤岛/小平台上。现改为:
* - 最多用 3 个不同种子尝试,只要找到一个出生点周围有正常陆地(score≥2)就接受
* - 全部 3 次都是海洋 → 在最后一个世界上强制改造出生点区域为陆地平台
*/
/**
* 创建全新PVP世界 — FLAT 世界 + 手写噪声地形(圆形自然竞技岛,无四方形切割)
*
* ★ 2026-07-07 重构:抛弃 NORMAL 随机地形(易出海洋/虚空导致小平台)
* 与 forceTerraformSpawnArea 的 81x81 四方形草地岛(明显的方形切割边界)。
* 改用 FLAT 世界(基础为实心连续地形,杜绝虚空与区块丢失),
* 再用手写噪声生成器 generatePVPTerrain 生成半径 48 的圆形起伏竞技岛,
* 边缘自然衰减入周围平地,观感自然、加载迅速、永远有合格陆地。
* ★ 2026-07-07 终版(按用户明确要求):
* 用户要求"完整主世界,插件全程不插手,由MC自己决定自己生成"。
* 因此抛弃之前所有自定义地形方案:
* - 不用 FLAT 世界;
* - 不调用 generatePVPTerrain 噪声造岛;
* - 不调用 forceTerraformSpawnArea 强制造陆;
* - 不设置种子(让 MC 每次随机决定全新主世界)。
* 插件只做三件"非地形"的事:
* 1) 设游戏规则(常昼/无天气/PVP 开启);
* 2) 预生成出生点周边区块(确保地形已就绪);
* 3) 只读方式调用 findSafeSpawn 选附近优质固体地面做出生点
* (不改任何方块;仅当极端异常(理论上 NORMAL 必有陆地)才造保底平台)。
*/
private void createPVPWorld() {
long seed = worldSeedRandom.nextLong();
plugin.getLogger().info("[PVP] 创建PVP世界 (FLAT+噪声地形 seed=" + seed + ")");
plugin.getLogger().info("[PVP] 创建PVP世界 (纯NORMAL主世界,地形由MC自行生成)");
// 清理上次残留世界,确保是全新世界
File existing = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
if (existing.exists()) moveWorldToTrash(existing);
// FLAT 世界:基础为实心连续地形,彻底消除虚空/区块丢失
// ★ 纯 NORMAL 主世界:完全交给 Minecraft 自己生成(山丘/水域/洞穴/生物群系)。
// 不调用 creator.seed(...) —— 由 MC 自行随机决定地形种子,插件对地形零干预。
WorldCreator creator = new WorldCreator(PVP_WORLD_NAME);
creator.environment(World.Environment.NORMAL);
creator.type(WorldType.FLAT);
creator.seed(seed);
creator.type(WorldType.NORMAL);
World pvpWorld = creator.createWorld();
if (pvpWorld == null) {
@@ -289,16 +286,29 @@ public class PVPArenaManager implements Listener {
pvpWorld.setTime(6000);
pvpWorld.setKeepSpawnInMemory(true);
// ★ 手写噪声地形:在原点周边生成圆形竞技岛(半径 48 格)
generatePVPTerrain(pvpWorld, seed);
// 立即落盘,确保手写地形持久化、不被后续自动保存覆盖
try { pvpWorld.save(); } catch (Exception ignored) {}
// 先把出生点锚定在原点,使后续预生成与出生点搜索都以原点为中心
pvpWorld.setSpawnLocation(0, 64, 0);
// 出生点固定在世界原点上方地形顶部
int groundY = getTopSolidY(pvpWorld, 0, 0);
pvpWorld.setSpawnLocation(0, groundY + 1, 0);
plugin.getLogger().info("[PVP] ✅ 已创建PVP竞技场世界(噪声地形 seed=" + seed
+ ") 出生点=(0," + (groundY + 1) + ",0)");
// 预生成出生点周边区块(半径 8 个区块),确保地形已就绪、findSafeSpawn 不误判虚空
preGenerateSpawnChunks(pvpWorld, 8);
// ★ 只读定位安全出生点(绝不改造地形)
Location safe = findSafeSpawn(pvpWorld);
if (safe != null) {
pvpWorld.setSpawnLocation(
safe.getBlockX(), safe.getBlockY(), safe.getBlockZ());
plugin.getLogger().info("[PVP] ✅ 已创建PVP竞技场世界(NORMAL主世界) 出生点=("
+ safe.getBlockX() + "," + safe.getBlockY() + "," + safe.getBlockZ()
+ ") 陆地质量分=" + lastSpawnLandScore);
} else {
// ★ 极端兜底:理论上 NORMAL 主世界必有陆地,仅当异常时才造保底平台
plugin.getLogger().warning("[PVP] 未找到安全陆地,启用保底出生平台");
buildFallbackPlatform(pvpWorld);
pvpWorld.setSpawnLocation(0, 121, 0);
}
// 落盘已生成的区块
try { pvpWorld.save(); } catch (Exception ignored) {}
}
/**
+45
View File
@@ -1175,11 +1175,54 @@ public class ShopManager implements Listener {
// ===== 保存为 md =====
/**
* 保存单个分类到 .md 文件(合并感知,杜绝覆盖管理员手工新增内容)。
*
* ★ 2026-07-07 修复(严重缺陷):原实现用 PrintWriter 对 .md 做整文件覆盖重写,
* 仅写出内存 categories 列表中的商品。若管理员在服务器运行期间于磁盘上手工新增了
* 商品(或修改了某个在内存中未被加载的分类文件),则整文件重写会把这些磁盘新增内容
* 抹掉;服务器 stop 时 onDisable→saveAll 全量重写会进一步把新增分类/商品“回滚”,
* 表现为“数据不同步 / 新商品被自动删除”。
*
* 新方案:写文件前先读取磁盘上已有的同文件,把“磁盘中存在但内存中不存在”的商品行原样
* 保留(追加在内存商品之后),从而减少整文件覆盖带来的内容丢失。
* 说明:整体新增的“独立分类 .md 文件”本来就不在内存 categories 中,saveAll 不会触碰它,
* 因此是天然安全的;本修复主要堵住“在已有分类里手工加商品被覆盖”的漏洞。
*/
public void saveCategory(ShopCategory cat) {
File dir = new File(
plugin.getDataFolder(), "shop");
if (!dir.exists()) dir.mkdirs();
File f = new File(dir, cat.getFileName());
// ★ 合并感知:收集磁盘上“内存中不存在”的商品行(保留管理员手工新增)
List<String> diskOnlyRows = new ArrayList<>();
if (f.exists()) {
Set<String> memIds = new HashSet<>();
for (ShopItem it : cat.getItems()) memIds.add(it.getId());
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
if (!line.startsWith("|")) continue; // 跳过标题(#)行
// 数据行形如: | ID | 品名 | 材质 | ...
String[] cols = line.split("\\|");
if (cols.length < 3) continue;
String id = cols[1].trim();
// 跳过表头行(ID)与分隔行(---)
if (id.isEmpty() || id.equals("ID") || id.equals("---"))
continue;
if (!memIds.contains(id)) {
diskOnlyRows.add(line); // 磁盘独有商品,原样保留
}
}
} catch (Exception ignored) {
// 读取失败则放弃合并(退化为仅写内存内容,与旧行为一致)
}
}
try {
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
@@ -1206,6 +1249,8 @@ public class ShopManager implements Listener {
+ " | " + item.getTotalSales()
+ " |");
}
// ★ 追加磁盘独有商品行(管理员手工新增,避免被覆盖删除)
for (String row : diskOnlyRows) pw.println(row);
pw.flush();
pw.close();
} catch (Exception e) {