修复: PVP世界生成改为仅检测出生点非实体方块则删除重建(插件放手不干预地形); shop子命令新增packmoney/green参数; 彻底移除商店.md文件运行时复写(onDisable与定时任务不再saveAll, saveCategory改为空操作)

This commit is contained in:
2026-07-07 22:14:24 +08:00
parent 4297e0e02d
commit a5a63dc5d8
3 changed files with 103 additions and 119 deletions
+2 -1
View File
@@ -722,7 +722,8 @@ public class Main extends JavaPlugin
public void onDisable() {
// 卸载时清理.sdf1临时文件
cleanupSdf1Files();
if (shopManager != null) shopManager.saveAll();
// ★ 2026-07-07 移除 onDisable 的 shopManager.saveAll():不再复写 .md 商品文件
// (商品目录由管理员手工维护,插件仅内存管理并单向同步给 PHP)
// 取消未执行的删除任务
if (pendingDeleteTask != null) {
pendingDeleteTask.cancel();
+48 -47
View File
@@ -208,41 +208,40 @@ public class PVPArenaManager implements Listener {
* • 下次进入再次随机生成 → 地形每次不同,破坏/砍树随删除完全复原,杜绝背图。
*/
public void ensurePVPWorldExists() {
// 有人要进来了:取消任何待执行的删除任务(保留当前这一局地形)
// 有人要进来了:取消任何待执行的删除任务(保留当前这一局世界)
cancelPendingDeletion();
World pvpWorld = Bukkit.getWorld(PVP_WORLD_NAME);
if (pvpWorld != null && !pvpWorld.getPlayers().isEmpty()) {
// 世界中仍有其他玩家在战斗 → 复用同一局地形,一起竞技(不重建)
// 世界中仍有其他玩家在战斗 → 复用同一局,一起竞技(不重建、不干预地形)
plugin.getLogger().info("[PVP] PVP世界已有玩家在场,复用当前地形(不重建)");
return;
}
// 世界不存在,或世界存在但已无人活动 → 重新随机生成全新地形。
// 这样满足"每次空场进入都随机生成地形";同时,服务器若自动从磁盘加载了
// 旧版小平台(残留世界),这里会先卸载并删除磁盘目录,用随机地形替换它。
// ★ 关键修复:先清理历史回收站目录,避免磁盘无限增长
cleanupOldTrashWorlds();
if (pvpWorld != null) {
plugin.getLogger().info("[PVP] 检测到PVP世界当前无人,卸载旧世界以重新随机生成...");
Bukkit.unloadWorld(pvpWorld, false); // false=不保存,直接丢弃旧地形(含旧版小平台)
// ★ 关键修复:Windows 上 unloadWorld 后 region 文件可能仍被 OS 锁定,
// File.delete() 会静默失败导致旧虚空世界残留,并被 createWorld() 原样重新加载
// (findSafeSpawn=null → 退化为手工小岛地形)。改用"重命名到回收站"移走,
// rename 在文件锁定时仍可靠成功,从而让 createWorld() 生成全新 NORMAL 主世界。
moveWorldToTrash(new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME));
// 世界不存在 → 创建(地形由 MC 自行生成)
if (pvpWorld == null) {
pvpWorld = createPVPWorld();
}
// 兜底:确保磁盘残留目录被清理(含旧版小平台),再随机重新生成
File worldDir = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
if (worldDir.exists() && worldDir.isDirectory()) {
plugin.getLogger().info("[PVP] 清理磁盘残留的PVP世界目录,准备随机重新生成...");
moveWorldToTrash(worldDir);
// ★ 唯一干预:检测出生点是否为非实体方块(水/熔岩/空气/虚空)。
// 若是 → 删除世界并重生(换一个随机种子,希望出生点为实体地面);
// 若否 → 完全放手,绝不改动地形、不移动出生点以外的任何方块。
if (pvpWorld != null && !isSpawnOnSolidGround(pvpWorld)) {
plugin.getLogger().info("[PVP] 出生点为非实体方块(水/虚空),删除并重生世界");
deletePVPWorld(pvpWorld);
pvpWorld = createPVPWorld();
// 重生后仍检查(最多重试若干次;极端情况下 MC 连续生成海洋/虚空才触发)
int tries = 0;
while (pvpWorld != null && !isSpawnOnSolidGround(pvpWorld) && tries < 3) {
plugin.getLogger().warning("[PVP] 重生后出生点仍非实体方块,再次删除重生 (" + (tries + 1) + ")");
deletePVPWorld(pvpWorld);
pvpWorld = createPVPWorld();
tries++;
}
if (pvpWorld != null && !isSpawnOnSolidGround(pvpWorld)) {
plugin.getLogger().warning("[PVP] 多次重出生点仍为非实体方块(极小概率),保留当前世界,请管理员手动处理");
}
}
// 创建全新随机地形世界
createPVPWorld();
}
/**
@@ -261,8 +260,8 @@ public class PVPArenaManager implements Listener {
* 3) 只读方式调用 findSafeSpawn 选附近优质固体地面做出生点
* (不改任何方块;仅当极端异常(理论上 NORMAL 必有陆地)才造保底平台)。
*/
private void createPVPWorld() {
plugin.getLogger().info("[PVP] 创建PVP世界 (纯NORMAL主世界,地形由MC自行生成)");
private World createPVPWorld() {
plugin.getLogger().info("[PVP] 创建PVP世界 (纯NORMAL主世界,地形由MC自行生成,插件不干预)");
// 清理上次残留世界,确保是全新世界
File existing = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
@@ -277,7 +276,7 @@ public class PVPArenaManager implements Listener {
World pvpWorld = creator.createWorld();
if (pvpWorld == null) {
plugin.getLogger().severe("[PVP] 无法创建PVP竞技场世界!");
return;
return null;
}
pvpWorld.setPVP(true);
@@ -286,29 +285,31 @@ public class PVPArenaManager implements Listener {
pvpWorld.setTime(6000);
pvpWorld.setKeepSpawnInMemory(true);
// 先把出生点锚定在原点,使后续预生成与出生点搜索都以原点为中心
pvpWorld.setSpawnLocation(0, 64, 0);
// 预生成出生点周边区块(半径 4 个区块),确保地形已就绪、出生点判定准确
preGenerateSpawnChunks(pvpWorld, 4);
// 预生成出生点周边区块(半径 8 个区块),确保地形已就绪、findSafeSpawn 不误判虚空
preGenerateSpawnChunks(pvpWorld, 8);
// ★ 只读:把出生“坐标”锚定到 (0,0) 列顶部实体方块之上(绝不改动任何地形方块)
int topY = getTopSolidY(pvpWorld, 0, 0);
pvpWorld.setSpawnLocation(0, topY + 1, 0);
plugin.getLogger().info("[PVP] ✅ 已创建PVP竞技场世界(NORMAL主世界) 出生点=("
+ 0 + "," + (topY + 1) + "," + 0 + ")");
// ★ 只读定位安全出生点(绝不改造地形)
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);
}
return pvpWorld;
}
// 落盘已生成的区块
try { pvpWorld.save(); } catch (Exception ignored) {}
/**
* 判断出生点是否落在“实体方块”之上(玩家可站立、非水非空气)。
* ★ 这是插件对 PVP 世界唯一的“干预判定”:只有返回 false 时才由 ensurePVPWorldExists 删除重生。
*/
private boolean isSpawnOnSolidGround(World world) {
Location s = world.getSpawnLocation();
Block under = world.getBlockAt(s.getBlockX(), s.getBlockY() - 1, s.getBlockZ());
Block feet = world.getBlockAt(s.getBlockX(), s.getBlockY(), s.getBlockZ());
Material u = under.getType();
Material f = feet.getType();
boolean solidUnder = !u.isAir() && u != Material.WATER && u != Material.LAVA;
boolean notInFluid = f != Material.WATER && f != Material.LAVA;
return solidUnder && notInFluid;
}
/**
+53 -71
View File
@@ -1189,75 +1189,16 @@ public class ShopManager implements Listener {
* 说明:整体新增的“独立分类 .md 文件”本来就不在内存 categories 中,saveAll 不会触碰它,
* 因此是天然安全的;本修复主要堵住“在已有分类里手工加商品被覆盖”的漏洞。
*/
/**
* 保存单个分类到 .md 文件。
*
* ★ 2026-07-07 改为“不再写盘”(按用户明确要求“彻底删除复写”):
* 商品目录由管理员手工维护磁盘 .md 文件,插件只在内存中管理并单向同步给 PHP
* (buildCatalogJson → WebManager.pushShopCatalog)。任何运行时写盘都会抹掉
* 管理员手工新增/修改的内容,故此处改为空操作,杜绝复写。
*/
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(
new FileOutputStream(f),
StandardCharsets.UTF_8));
pw.println("# " + cat.getName());
pw.println("");
pw.println("| ID | 品名 | 材质"
+ " | 购入价 | 售出价"
+ " | 库存"
+ " | 本小时销量 | 总销量 |");
pw.println("| --- | --- | ---"
+ " | --- | --- | ---"
+ " | --- | --- |");
for (ShopItem item : cat.getItems()) {
pw.println("| "
+ item.getId()
+ " | " + item.getDisplayName()
+ " | " + item.getMaterial().name()
+ " | " + item.getBuyPrice()
+ " | " + item.getSellPrice()
+ " | " + item.getStock()
+ " | " + item.getHourlySales()
+ " | " + item.getTotalSales()
+ " |");
}
// ★ 追加磁盘独有商品行(管理员手工新增,避免被覆盖删除)
for (String row : diskOnlyRows) pw.println(row);
pw.flush();
pw.close();
} catch (Exception e) {
// plugin.getLogger().warning(
// "[Shop] 保存失败: "
// + cat.getFileName());
}
// 不再写盘:保留方法签名仅为兼容既有调用方(add/remove/set/PHP同步回调)。
}
// ===== 创建示例分类 =====
@@ -2331,6 +2272,47 @@ public class ShopManager implements Listener {
return true;
}
// 打包费配置:shop packmoney <金额> (与 /sdf1_login set packmoney 等效)
if (sub.equals("packmoney")) {
if (a.length < 3) {
s.sendMessage("§e用法: shop packmoney <金额(0~999)>");
return true;
}
try {
int amt = Integer.parseInt(a[2]);
if (amt < 0 || amt > 999) {
s.sendMessage("§c打包费需在 0~999 之间");
return true;
}
plugin.getConfigMgr().packingFee = amt;
plugin.webManager.pushShopConfig("packmoney", String.valueOf(amt));
s.sendMessage("§a打包费已设置为 " + amt + " 债券(已同步至Web配置)");
} catch (NumberFormatException e) {
s.sendMessage("§c金额必须为数字");
}
return true;
}
// 环保单减免:shop green <1-10> (与 shop setgreen 等效,便于在 shop 子命令下管理)
if (sub.equals("green")) {
if (a.length < 3) {
s.sendMessage("§e用法: shop green <1-10>(10=不减免,1-9.99=按比例减免%)");
return true;
}
try {
double v = Double.parseDouble(a[2]);
int gv = (int) Math.floor(v);
if (gv < 0) gv = 0;
if (gv >= 10) gv = 0; // 10 视为不减免
plugin.getConfigMgr().greenDiscount = gv;
plugin.webManager.pushShopConfig("green_discount", String.valueOf(gv));
s.sendMessage("§a环保单减免已设置为 " + gv + "%(10=不减免,已同步至Web配置)");
} catch (NumberFormatException e) {
s.sendMessage("§c参数必须为数字");
}
return true;
}
// 环保单减免:shop setgreen <1-10> (10=不减免,1-9.99=按比例减免%)
if (sub.equals("setgreen")) {
if (a.length < 3) {
@@ -2377,7 +2359,7 @@ public class ShopManager implements Listener {
s.sendMessage("§e商店子命令:"
+ " reload / add / remove"
+ " / set / pro ");
+ " / set / setgreen(green) / packmoney / pro ");
return true;
}
@EventHandler(priority = EventPriority.LOW)
@@ -3084,11 +3066,11 @@ public class ShopManager implements Listener {
if (plugin.getSalesStats() != null) {
plugin.getSalesStats().report();
}
// 重置小时销量
// 重置小时销量(仅内存,无需写盘)
for (ShopCategory cat : categories)
for (ShopItem item : cat.getItems())
item.setHourlySales(0);
saveAll();
// ★ 2026-07-07 移除:不再调用 saveAll() 周期性复写 .md 商品文件
}
}.runTaskTimer(plugin, 72000L, 72000L);
}