fix: PVP/伤害权限修复 + 到期自动清理

1. getEffectiveDeny(): denyPVP和denyAllDamage不再豁免ADMIN/OWNER
2. 移除重复的onEntityDamageByEntity PVP逻辑
3. Timer E新增checkAndRemoveExpired()自动清理过期成员
This commit is contained in:
2026-07-03 13:41:11 +08:00
parent 4a28287dec
commit 3d9475ccec
3 changed files with 106 additions and 99 deletions
+26 -60
View File
@@ -2471,11 +2471,15 @@ public class AreaProtection implements Listener {
public boolean getEffectiveDeny(Player player, AreaConfig ac, String permName) {
// 如果ac为null(不在任何领地内),直接允许
if (ac == null) return false;
// ADMIN/OWNER不检查per-player deny
PermissionLevel level = getPermissionLevel(player, ac);
if (level == PermissionLevel.ADMIN || level == PermissionLevel.OWNER) {
return false;
// ★ denyPVP和denyAllDamage是全局安全设置,ADMIN/OWNER也不例外
boolean isSecurityPerm = "denyPVP".equals(permName) || "denyAllDamage".equals(permName);
if (!isSecurityPerm) {
// ADMIN/OWNER不检查per-player deny(安全权限除外)
PermissionLevel level = getPermissionLevel(player, ac);
if (level == PermissionLevel.ADMIN || level == PermissionLevel.OWNER) {
return false;
}
}
// 尝试读取per-player权限
@@ -4289,23 +4293,6 @@ public class AreaProtection implements Listener {
}
@EventHandler
public void onPVP(EntityDamageByEntityEvent e) {
if (!(e.getDamager() instanceof Player)) return;
if (!(e.getEntity() instanceof Player)) return;
Player p = (Player) e.getEntity();
AreaConfig ac = getArea(
p.getWorld().getName(),
p.getLocation().getBlockX(),
p.getLocation().getBlockY(),
p.getLocation().getBlockZ());
if (ac != null && getEffectiveDeny(p, ac, "denyPVP")) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f禁止PVP");
}
}
@EventHandler
public void onFood(FoodLevelChangeEvent e) {
if (!(e.getEntity() instanceof Player)) return;
@@ -9378,16 +9365,7 @@ public class AreaProtection implements Listener {
Player attacker = (Player) e.getDamager();
Player victim = (Player) e.getEntity();
// 管理员互殴不受限
if (isAreaAdmin(attacker) && isAreaAdmin(victim)) return;
// 伤害者在自己领地无denyPVP时,跳过
AreaConfig acAttacker = getArea(
attacker.getWorld().getName(),
attacker.getLocation().getBlockX(),
attacker.getLocation().getBlockY(),
attacker.getLocation().getBlockZ());
if (acAttacker == null || !getEffectiveDeny(attacker, acAttacker, "denyPVP")) return;
// 受害者领地有denyPVP → 阻断
// 受害者领地禁止PVP → 阻断
AreaConfig acVictim = getArea(
victim.getWorld().getName(),
victim.getLocation().getBlockX(),
@@ -9396,6 +9374,19 @@ public class AreaProtection implements Listener {
if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyPVP")) {
e.setCancelled(true);
attacker.sendMessage("§c§l[区域防护] §f该领地禁止PVP");
return;
}
// 攻击者领地禁止PVP → 阻断
AreaConfig acAttacker = getArea(
attacker.getWorld().getName(),
attacker.getLocation().getBlockX(),
attacker.getLocation().getBlockY(),
attacker.getLocation().getBlockZ());
if (acAttacker != null && getEffectiveDeny(attacker, acAttacker, "denyPVP")) {
e.setCancelled(true);
attacker.sendMessage("§c§l[区域防护] §f此区域禁止PVP");
return;
}
}
@@ -9477,32 +9468,7 @@ public class AreaProtection implements Listener {
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
return;
}
// ===== PVP 逻辑(原有的,不动)=====
if (!(e.getDamager() instanceof Player)) return;
if (!(e.getEntity() instanceof Player)) return;
Player attacker = (Player) e.getDamager();
Player victim = (Player) e.getEntity();
// 管理员互殴不受限
if (isAreaAdmin(attacker) && isAreaAdmin(victim)) return;
// 伤害者在自己领地无denyPVP时,跳过
AreaConfig acAttacker = getArea(
attacker.getWorld().getName(),
attacker.getLocation().getBlockX(),
attacker.getLocation().getBlockY(),
attacker.getLocation().getBlockZ());
if (acAttacker == null || !getEffectiveDeny(attacker, acAttacker, "denyPVP")) return;
// 受害者领地有denyPVP → 阻断
AreaConfig acVictim = getArea(
victim.getWorld().getName(),
victim.getLocation().getBlockX(),
victim.getLocation().getBlockY(),
victim.getLocation().getBlockZ());
if (acVictim != null && getEffectiveDeny(victim, acVictim, "denyPVP")) {
e.setCancelled(true);
attacker.sendMessage("§c§l[区域防护] §f该领地禁止PVP");
}
// PVP逻辑在onEntityDamage()中统一处理,此处不再重复
}
private AreaConfig findFrameArea(Entity frame) {
Location loc = frame.getLocation();
@@ -9591,8 +9557,8 @@ public class AreaProtection implements Listener {
(int) ent.getLocation().getBlockX(),
(int) ent.getLocation().getBlockY(),
(int) ent.getLocation().getBlockZ());
// 如果爆炸源在禁止火焰蔓延的区域,取消所有爆炸
if (ac != null && ac.denyFireSpread) {
// 如果爆炸源在禁止爆炸的区域,取消所有爆炸
if (ac != null && ac.denyExplosion) {
e.blockList().clear();
return;
}
@@ -572,6 +572,10 @@ public class UserGroupManager {
public boolean autoRenewGroup(String player, String groupName) {
UserGroupConfig cfg = getGroupConfig(groupName);
if (cfg == null || !cfg.autoRenew || cfg.renewPrice <= 0) return false;
// 检查玩家个人自动续费偏好(如果关闭则跳过)
if (!getPlayerAutoRenew(player, groupName)) {
return false;
}
// 查当前到期时间
long currentExpiry = 0;
@@ -806,6 +810,24 @@ public class UserGroupManager {
// ==================== 工具方法 ====================
/**
* 获取玩家个人的自动续费偏好(per-player)
* 如果没有设置,返回组配置的默认值
*/
public boolean getPlayerAutoRenew(String player, String groupName) {
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr != null) {
// 查玩家个人设置
boolean personal = dbMgr.getPlayerAutoRenew(player, groupName);
// 如果有个人设置,返回个人设置;否则返回组配置默认值
UserGroupConfig cfg = getGroupConfig(groupName);
return personal;
}
// fallback到组配置
UserGroupConfig cfg = getGroupConfig(groupName);
return cfg != null && cfg.autoRenew;
}
public Map<String, UserGroupConfig> getGroupConfigs() {
return groupConfigs;
}
+58 -39
View File
@@ -1716,33 +1716,6 @@ public class WebManager {
// ★ 全员下线暂停:不调度下一轮
if (timersBCPaused) {
plugin.getLogger().info("[合并C] ★ 全员下线,Timer C已暂停");
log.error("\n" +
" __ __ _ \n" +
" \\ \\ / / | | \n" +
" \\ \\ /\\ / /__ ___ ___ _ __ ___ ___ | |_ ___ \n" +
" \\ \\/ \\/ / _ \\/ __/ _ \\| '_ ` _ \\ / _ \\ | __/ _ \\ \n" +
" \\ /\\ / __/ (_| (_) | | | | | | __/ | || (_) | \n" +
" \\/ \\/ \\___|\\___\\___/|_| |_| |_|\\___| \\__\\___/ _ \n" +
" | | (_) \n" +
" ___ __ _ ___ _ _ _ _ __ _ _ __ | |_ __ _ _ __ __ ___ __ _ _ __ ___ ___ _ ____ _____ _ __ \n" +
" / __/ _` |/ _ \\ | | | | | | |/ _` | '_ \\ | __/ _` | '_ \\ \\ \\/ / |/ _` | '_ \\ / __|/ _ \\ '__\\ \\ / / _ \\ '__|\n" +
" | (_| (_| | (_) | | |_| | |_| | (_| | | | | | || (_| | | | | > <| | (_| | | | | \\__ \\ __/ | \\ V / __/ | \n" +
" \\___\\__,_|\\___/ \\__, |\\__,_|\\__,_|_| |_| \\__\\__,_|_| |_| /_/\\_\\_|\\__,_|_| |_| |___/\\___|_| \\_/ \\___|_| \n" +
" _ __/ | ___ _ _ _ _ __ \n" +
" (_) _ |___/ |__ \\ | | (_) | (_)/ _| \n" +
" _ _ __(_) _ __ ___ ___ ) | _ _ _ __ ___| |__ _ __| |_| |_ _ _ ___ _ __ \n" +
" | | '_ \\ | '_ ` _ \\ / __| / / | | | | '_ \\/ __| '_ \\| |/ _` | | _| | | | / __| '_ \\ \n" +
" | | |_) | | | | | | | (__ / /_ | |_| | |_) \\__ \\ | | | | (_| | | | | |_| || (__| | | | \n" +
" |_| .__(_) |_| |_| |_|\\___|____(_)__, | .__/|___/_| |_|_|\\__,_|_|_| \\__,_(_)___|_| |_| \n" +
" | | __/ | | \n" +
" |_| _ ____ _|___/|_|______ ___ \n" +
" | | _ |___ \\ / _ \\ / /____ / _ \\ \n" +
" _ __ ___ ___| |_(_) __) | | | |/ /_ / / (_) | \n" +
" | '_ \\ / _ \\/ __| __| |__ <| | | | '_ \\ / / \\__, | \n" +
" | |_) | (_) \\__ \\ |_ _ ___) | |_| | (_) / / / / \n" +
" | .__/ \\___/|___/\\__(_) |____/ \\___/ \\___/_/ /_/ \n" +
" | | \n" +
" |_| ");
return; // 不调度下一轮,定时器自然停止
}
@@ -1823,6 +1796,33 @@ public class WebManager {
// ★ 全员下线暂停:不调度下一轮
if (timersBCPaused) {
plugin.getLogger().info("[续费轮询E] ★ 全员下线,Timer E已暂停");
log.error("\n" +
" __ __ _ \n" +
" \\ \\ / / | | \n" +
" \\ \\ /\\ / /__ ___ ___ _ __ ___ ___ | |_ ___ \n" +
" \\ \\/ \\/ / _ \\/ __/ _ \\| '_ ` _ \\ / _ \\ | __/ _ \\ \n" +
" \\ /\\ / __/ (_| (_) | | | | | | __/ | || (_) | \n" +
" \\/ \\/ \\___|\\___\\___/|_| |_| |_|\\___| \\__\\___/ _ \n" +
" | | (_) \n" +
" ___ __ _ ___ _ _ _ _ __ _ _ __ | |_ __ _ _ __ __ ___ __ _ _ __ ___ ___ _ ____ _____ _ __ \n" +
" / __/ _` |/ _ \\ | | | | | | |/ _` | '_ \\ | __/ _` | '_ \\ \\ \\/ / |/ _` | '_ \\ / __|/ _ \\ '__\\ \\ / / _ \\ '__|\n" +
" | (_| (_| | (_) | | |_| | |_| | (_| | | | | | || (_| | | | | > <| | (_| | | | | \\__ \\ __/ | \\ V / __/ | \n" +
" \\___\\__,_|\\___/ \\__, |\\__,_|\\__,_|_| |_| \\__\\__,_|_| |_| /_/\\_\\_|\\__,_|_| |_| |___/\\___|_| \\_/ \\___|_| \n" +
" _ __/ | ___ _ _ _ _ __ \n" +
" (_) _ |___/ |__ \\ | | (_) | (_)/ _| \n" +
" _ _ __(_) _ __ ___ ___ ) | _ _ _ __ ___| |__ _ __| |_| |_ _ _ ___ _ __ \n" +
" | | '_ \\ | '_ ` _ \\ / __| / / | | | | '_ \\/ __| '_ \\| |/ _` | | _| | | | / __| '_ \\ \n" +
" | | |_) | | | | | | | (__ / /_ | |_| | |_) \\__ \\ | | | | (_| | | | | |_| || (__| | | | \n" +
" |_| .__(_) |_| |_| |_|\\___|____(_)__, | .__/|___/_| |_|_|\\__,_|_|_| \\__,_(_)___|_| |_| \n" +
" | | __/ | | \n" +
" |_| _ ____ _|___/|_|______ ___ \n" +
" | | _ |___ \\ / _ \\ / /____ / _ \\ \n" +
" _ __ ___ ___| |_(_) __) | | | |/ /_ / / (_) | \n" +
" | '_ \\ / _ \\/ __| __| |__ <| | | | '_ \\ / / \\__, | \n" +
" | |_) | (_) \\__ \\ |_ _ ___) | |_| | (_) / / / / \n" +
" | .__/ \\___/|___/\\__(_) |____/ \\___/ \\___/_/ /_/ \n" +
" | | \n" +
" |_| ");
return; // 不调度下一轮,定时器自然停止
}
@@ -1846,6 +1846,19 @@ public class WebManager {
plugin.getLogger().warning("[续费轮询E] 到期提醒异常: " + e.getMessage());
}
// ★ 自动清理过期用户组成员(每轮执行,确保不续费的玩家被移除)
try {
UserGroupManager ugmCheck = plugin.getUserGroup();
if (ugmCheck != null) {
java.util.List<java.util.Map<String, Object>> expired = ugmCheck.checkAndRemoveExpired();
if (!expired.isEmpty()) {
plugin.getLogger().info("[续费轮询E] 自动清理 " + expired.size() + " 个过期用户组成员");
}
}
} catch (Exception e) {
plugin.getLogger().warning("[续费轮询E] 过期清理异常: " + e.getMessage());
}
// 自调度下一轮(20~30秒,错峰)
scheduleTimerE(calcStaggeredDelay(TIMER_E, 20, 30));
}
@@ -1898,13 +1911,16 @@ public class WebManager {
String err = ugm.renewGroup(playerName, groupName);
// 回调PHP
// 回调PHP(使用GET更可靠)
String callbackAction = (err == null) ? "renew_group_callback" : "renew_group_callback";
String result = (err == null) ? "success" : "failed:" + err;
String callbackBody = "{\"secret\":\"" + escapeJson(secretKey) + "\",\"req_id\":\"" + escapeJson(reqId) + "\",\"result\":\"" + escapeJson(result) + "\"}";
java.util.Map<String, String> callbackParams = new java.util.LinkedHashMap<>();
callbackParams.put("secret", secretKey);
callbackParams.put("req_id", reqId);
callbackParams.put("result", result);
try {
String callbackResp = httpPost("api/land_api.php?action=" + callbackAction, callbackBody);
String callbackResp = httpGet("api/land_api.php", callbackParams);
detectPhpBusy(callbackResp);
} catch (Exception cbEx) {
plugin.getLogger().warning("[续费轮询E] 回调PHP失败: " + cbEx.getMessage());
@@ -1932,7 +1948,9 @@ public class WebManager {
String player = (String) g.get("player");
String group = (String) g.get("group");
long expiry = (long) g.get("expiry");
boolean autoRenew = (boolean) g.get("autoRenew");
boolean groupAutoRenew = (boolean) g.get("autoRenew"); // 组配置默认值
// 获取玩家个人自动续费偏好(覆盖组配置)
boolean autoRenew = ugm.getPlayerAutoRenew(player, group);
long remaining = expiry - now;
if (remaining <= 0) continue;
@@ -3570,11 +3588,11 @@ public class WebManager {
boolean exists = dbMgr.userExists(player);
String status = exists ? "valid" : "invalid";
// 推送结果回PHP
// 推送结果回PHP(使用GET更可靠)
String callbackUrl = webBaseUrl + "/api/land_api.php?action=validation_callback&secret="
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
+ "&id=" + id + "&status=" + status;
doPost(callbackUrl, "{}");
doGet(callbackUrl);
plugin.getLogger().info("[异步验证] 玩家 " + player + " → " + (exists ? "存在" : "不存在") + " (type=" + reqType + ")");
}
@@ -3791,9 +3809,9 @@ public class WebManager {
if (!deleteLandName.isEmpty() && areaProtect.getLand(deleteLandName) != null) {
areaProtect.deleteLand(deleteLandName);
plugin.getLogger().info("[Web通信] 管理面板删除领地: " + deleteLandName);
// 回调PHP确认删除成功
// 回调PHP确认删除成功(使用GET更可靠)
String cbUrl = webBaseUrl + "/api/land_api.php?action=delete_land_callback&name=" + java.net.URLEncoder.encode(deleteLandName, "UTF-8") + "&success=true&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8");
doPost(cbUrl, "{}");
doGet(cbUrl);
applied = true;
} else if (deleteLandName.isEmpty()) {
plugin.getLogger().warning("[Web通信] land_delete: 领地名为空");
@@ -3801,7 +3819,7 @@ public class WebManager {
// 领地不存在,直接回调PHP标记已处理
plugin.getLogger().info("[Web通信] land_delete: 领地 " + deleteLandName + " 不存在,标记已处理");
String cbUrl = webBaseUrl + "/api/land_api.php?action=delete_land_callback&name=" + java.net.URLEncoder.encode(deleteLandName, "UTF-8") + "&success=true&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8");
doPost(cbUrl, "{}");
doGet(cbUrl);
applied = true;
}
break;
@@ -3977,8 +3995,8 @@ public class WebManager {
+ "&transfer_id=" + transferId
+ "&result=" + java.net.URLEncoder.encode(result, "UTF-8")
+ "&reason=" + java.net.URLEncoder.encode(reason, "UTF-8");
// 用POST发送
String response = doPost(url, "{}");
// ★ 使用GET代替POST,更可靠(避免SSL body问题)
String response = doGet(url);
plugin.getLogger().fine("[过户] 回调PHP: transfer_id=" + transferId + ", result=" + result);
} catch (Exception e) {
plugin.getLogger().warning("[过户] 回调PHP失败: " + e.getMessage());
@@ -3994,7 +4012,8 @@ public class WebManager {
+ "&change_id=" + changeId
+ "&success=" + (success ? "1" : "0")
+ "&reason=" + java.net.URLEncoder.encode(reason, "UTF-8");
String response = doPost(url, "{}");
// ★ 使用GET代替POST,更可靠(避免SSL body问题)
String response = doGet(url);
plugin.getLogger().fine("[Web通信] 回调PHP改主结果: change_id=" + changeId + ", success=" + success);
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 回调PHP改主结果失败: " + e.getMessage());