修复: PVP世界生成改为仅检测出生点非实体方块则删除重建(插件放手不干预地形); shop子命令新增packmoney/green参数; 彻底移除商店.md文件运行时复写(onDisable与定时任务不再saveAll, saveCategory改为空操作)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user