diff --git a/src/main/java/Sdf1_login/AreaProtection.java b/src/main/java/Sdf1_login/AreaProtection.java index abe25d7..4138dc8 100644 --- a/src/main/java/Sdf1_login/AreaProtection.java +++ b/src/main/java/Sdf1_login/AreaProtection.java @@ -3221,6 +3221,62 @@ public class AreaProtection implements Listener { return n.endsWith("_SIGN"); } + /** + * ★ 第57批续22: 子领地创建时继承主领地权限一次。 + * 后续子领地权限由子领地主/管理员/子领地授权管理员单独调配, + * 不再跟随主领地变化。 + * 继承范围: deny* 权限开关、出入提示、游戏模式强制、公共建筑标记。 + * 不继承: owner(创建者)、坐标、parentId、createCost。 + */ + private void copyLandPermissions(AreaConfig src, AreaConfig dst) { + if (src == null || dst == null) return; + dst.denyBlockPlace = src.denyBlockPlace; + dst.denyBlockBreak = src.denyBlockBreak; + dst.denySignEdit = src.denySignEdit; + dst.denyContainer = src.denyContainer; + dst.denyPVP = src.denyPVP; + dst.denyFallDamage = src.denyFallDamage; + dst.denyHunger = src.denyHunger; + dst.denyAllDamage = src.denyAllDamage; + dst.denyMount = src.denyMount; + dst.allowDrop = src.allowDrop; + dst.denyEnderPearl = src.denyEnderPearl; + dst.denyBow = src.denyBow; + dst.denyPotion = src.denyPotion; + dst.denyExplosion = src.denyExplosion; + dst.denyMove = src.denyMove; + dst.allowPickup = src.allowPickup; + dst.denyFire = src.denyFire; + dst.denyThrownProjectiles = src.denyThrownProjectiles; + dst.denyGlowing = src.denyGlowing; + dst.denyRedstoneInteraction = src.denyRedstoneInteraction; + dst.denyDoorInteraction = src.denyDoorInteraction; + dst.denyNoteblockJukebox = src.denyNoteblockJukebox; + dst.denyLead = src.denyLead; + dst.denyCropHarvest = src.denyCropHarvest; + dst.denyFarmlandTrample = src.denyFarmlandTrample; + dst.denyEnderTeleport = src.denyEnderTeleport; + dst.denyWoolShear = src.denyWoolShear; + dst.denyAnimalFeeding = src.denyAnimalFeeding; + dst.denySpawnEgg = src.denySpawnEgg; + dst.denyWax = src.denyWax; + dst.denyMobAttack = src.denyMobAttack; + dst.denyVillagerTrade = src.denyVillagerTrade; + dst.denyRaid = src.denyRaid; + dst.denyFireSpread = src.denyFireSpread; + dst.denyAllEffects = src.denyAllEffects; + dst.denyItemFrame = src.denyItemFrame; + dst.denyEntityInteract = src.denyEntityInteract; + dst.enterMsg = src.enterMsg; + dst.leaveMsg = src.leaveMsg; + dst.enforceGameMode = src.enforceGameMode; + dst.isPublicBuilding = src.isPublicBuilding; + dst.allowVisitorTeleport = src.allowVisitorTeleport; + dst.peaceMode = src.peaceMode; + dst.peaceModeDuration = src.peaceModeDuration; + dst.clearAllBadEffects = src.clearAllBadEffects; + } + /** * 获取领地默认deny状态(按字段名映射) */ @@ -3613,6 +3669,12 @@ public class AreaProtection implements Listener { String newArea = null; String forcedMode = null; + // ★ 第57批续22: 收集所有匹配领地,选面积最小的。 + // 子领地完全包含在主领地内,ConcurrentHashMap 遍历顺序不定, + // 先 break 可能命中主领地 → 子领地 deny_move=1 不生效。 + // 面积最小 = 子领地(完全包含时面积必然更小),天然优先级正确。 + AreaConfig bestAc = null; + String bestName = null; for (Map.Entry entry : areas.entrySet()) { AreaConfig ac = entry.getValue(); @@ -3628,11 +3690,25 @@ public class AreaProtection implements Listener { if (px >= minX && px <= maxX && pz >= minZ && pz <= maxZ && py >= ac.yMin && py <= ac.yMax) { - newArea = entry.getKey(); - forcedMode = ac.enforceGameMode; - break; + if (bestAc == null) { + bestAc = ac; + bestName = entry.getKey(); + } else { + long bestArea = (long)(Math.abs(bestAc.x1 - bestAc.x2) + 1) + * (Math.abs(bestAc.z1 - bestAc.z2) + 1); + long curArea = (long)(Math.abs(ac.x1 - ac.x2) + 1) + * (Math.abs(ac.z1 - ac.z2) + 1); + if (curArea <= bestArea) { + bestAc = ac; + bestName = entry.getKey(); + } + } } } + if (bestName != null) { + newArea = bestName; + forcedMode = bestAc.enforceGameMode; + } /* plugin.getLogger().info( "[防护-移动] " + p.getName() @@ -8010,6 +8086,10 @@ public class AreaProtection implements Listener { cac.yMin = 0; cac.yMax = 255; cac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数) cac.parentId = cnIsSub ? getLandIdFromDb(cnMain.name) : 0; // ★ 第57批续21 子领地父ID + // ★ 第57批续22: 子领地创建时继承主领地权限一次(后续可单独调配) + if (cnIsSub) { + copyLandPermissions(cnMain, cac); + } saveAreaToDb(cac); areas.put(confirmName, cac); // ★ 立即更新内存 if (cnIsSub) { @@ -10705,6 +10785,10 @@ public class AreaProtection implements Listener { nc.yMin = 0; nc.yMax = 255; nc.createCost = charged; nc.parentId = apIsSub ? getLandIdFromDb(apMain.name) : 0; // ★ 第57批续21 子领地父ID + // ★ 第57批续22: 子领地创建时继承主领地权限一次(后续可单独调配) + if (apIsSub) { + copyLandPermissions(apMain, nc); + } saveAreaToDb(nc); areas.put(pe.landName, nc); pendingExpands.remove(pe.landName.toLowerCase()); diff --git a/src/main/java/Sdf1_login/WebManager.java b/src/main/java/Sdf1_login/WebManager.java index 5640a84..703fa80 100644 --- a/src/main/java/Sdf1_login/WebManager.java +++ b/src/main/java/Sdf1_login/WebManager.java @@ -5096,6 +5096,11 @@ public class WebManager { long created = longOf(row.get("created")); if (pulled != null && created > 0) { pulled.setCreated(new java.util.Date(created)); + // ★ 第57批续22续: setCreated 只改内存对象,Bukkit BanList 无 updateBan API。 + // banned-players.json 里保留的是 addBan 即当前时间, + // BanBroadcastManager 读文件 → 封禁播报全员时间混乱。 + // 直接改写文件持久化真实创建时间。 + persistBanCreatedToFile(target, created); } plugin.getLogger().info("[对账] 已收回 Web 端封禁: " + (isIp ? "IP " : "") + target); return true; @@ -5860,6 +5865,51 @@ public class WebManager { } } + /** + * ★ 第57批续22续: 封禁收回时持久化真实创建时间到 banned-players.json。 + * + * setCreated 只改内存 BanEntry 对象,Bukkit BanList 没有 updateBan API, + * 文件里保留的是 addBan 时的当前时间。BanBroadcastManager 直接读文件, + * 不持久化 → 所有被收回的封禁 created 都变成收回时间 → 封禁播报全员时间陷入同一天。 + * + * 方案:读 banned-players.json → Gson 解析 → 定位 target 对应条目 → 改 created 字段 → 写回。 + * 文件不存在/解析失败/未找到 target → 安全返回(内存 setCreated 仍有效)。 + */ + private void persistBanCreatedToFile(String target, long createdMs) { + if (target == null || target.isEmpty() || createdMs <= 0) return; + try { + java.io.File bf = new java.io.File(plugin.getServer().getWorldContainer(), "banned-players.json"); + if (!bf.exists()) return; + String txt = new String(java.nio.file.Files.readAllBytes(bf.toPath()), + java.nio.charset.StandardCharsets.UTF_8); + com.google.gson.JsonElement root = com.google.gson.JsonParser.parseString(txt); + if (!root.isJsonArray()) return; + com.google.gson.JsonArray arr = root.getAsJsonArray(); + boolean changed = false; + for (com.google.gson.JsonElement el : arr) { + if (el == null || !el.isJsonObject()) continue; + com.google.gson.JsonObject o = el.getAsJsonObject(); + String name = o.has("name") ? o.get("name").getAsString() : ""; + if (name.equals(target)) { + // Gson JsonPrimitive 无 Date 构造器,用 SimpleDateFormat 格式化为 ISO-8601 + String isoTs = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.ROOT) + .format(new java.util.Date(createdMs)); + o.add("created", new com.google.gson.JsonPrimitive(isoTs)); + changed = true; + break; + } + } + if (changed) { + java.nio.file.Files.write(bf.toPath(), + arr.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); + plugin.getLogger().info("[对账] 已持久化封禁创建时间: " + target + + " created=" + createdMs); + } + } catch (Exception e) { + plugin.getLogger().warning("[对账] 持久化封禁创建时间失败: " + e.getMessage()); + } + } + /** ★ 第57批根治:直接读 banned-players.json 判断是否为空 * MC原版可能备份{空文件后内存BanList仍残留旧数据,不能依赖 BanList */