修复三项问题:1)大箱子跨领地边界绕过容器权限(双箱改为左右两侧均收集定位并逐侧判定denyContainer,交互事件改用被点击方块定位并补查相邻半箱)2)tpaall语义文案修正(通知/按钮/GUI统一为全服玩家一起传送到发起者身边)3)新人验证码超时不放行改为重新出题,题库引入九九乘法表加减乘除+GUI五选一随机抽题;另注释移动豁免诊断日志

This commit is contained in:
youpaishidifu
2026-09-27 12:11:41 +08:00
parent 6f844b5714
commit f008c49e84
4 changed files with 232 additions and 88 deletions
+99 -49
View File
@@ -3580,7 +3580,7 @@ public class AreaProtection implements Listener {
int landId = getLandIdFromDb(ac.name);
PermRow row = landId > 0 ? findPermRow(landId, p.getName()) : null;
String name = p.getName();
plugin.getLogger().info("[防护][移动豁免诊断] 场景=" + scene
/* plugin.getLogger().info("[防护][移动豁免诊断] 场景=" + scene
+ " 领地=" + ac.name
+ " 玩家=" + name
+ " 码点=" + java.util.Arrays.toString(name.codePoints().toArray())
@@ -3591,7 +3591,7 @@ public class AreaProtection implements Listener {
+ " 白名单=" + isPlayerWhitelisted(name, ac)
+ " 访客行=" + (row == null ? "无"
: (row.playerName + "/" + row.role + "/exp=" + row.expiresAt + "/" + row.permissions))
+ " 领地默认denyMove=" + ac.denyMove);
+ " 领地默认denyMove=" + ac.denyMove);*/
} catch (Exception ignored) {
}
}
@@ -5376,6 +5376,38 @@ public class AreaProtection implements Listener {
}
// ===== 容器/UI方块交互:以【被点击方块】定位领地 =====
// 玩家站在领地外右键领地内容器/UI方块同样受控(旧逻辑用玩家脚下定位,
// 站领地外 ac==null 直接放行);双箱被点侧在领地外时补查相邻同材质半边
if (e.getAction() == Action.RIGHT_CLICK_BLOCK) {
Block clicked = e.getClickedBlock();
if (clicked != null && isUIBlock(clicked.getType())) {
AreaConfig cac = getArea(clicked.getWorld().getName(),
clicked.getX(), clicked.getY(), clicked.getZ());
if (cac == null && (clicked.getType() == Material.CHEST
|| clicked.getType() == Material.TRAPPED_CHEST)) {
org.bukkit.block.BlockFace[] horiz = {
org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST,
org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH};
for (org.bukkit.block.BlockFace bf : horiz) {
Block nb = clicked.getRelative(bf);
if (nb.getType() == clicked.getType()) {
cac = getArea(nb.getWorld().getName(),
nb.getX(), nb.getY(), nb.getZ());
if (cac != null) break;
}
}
}
// 公共设施连控:公共设施启用时访客自动获得容器交互权限
if (cac != null && !cac.isPublicBuilding
&& getEffectiveDeny(p, cac, "denyContainer")) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
return;
}
}
}
// ===== 区域规则检查 =====
AreaConfig ac = getArea(
p.getWorld().getName(),
@@ -5532,88 +5564,106 @@ public class AreaProtection implements Listener {
Player p = (Player) e.getPlayer();
InventoryHolder holder = e.getInventory().getHolder();
Location loc = null;
java.util.List<Location> locs = new java.util.ArrayList<>();
// ★ 双箱特殊处理:DoubleChest的holder不是BlockState,需要从left/right获取
if (holder != null && holder.getClass().getSimpleName().contains("DoubleChest")) {
try {
// DoubleChest通过反射获取left/right
Object dc = holder;
java.lang.reflect.Method getLeft = dc.getClass().getMethod("getLeft");
Object left = getLeft.invoke(dc);
if (left instanceof org.bukkit.block.BlockState) {
loc = ((org.bukkit.block.BlockState) left).getLocation();
} else if (left != null) {
java.lang.reflect.Method getBlock = left.getClass().getMethod("getBlock");
Object block = getBlock.invoke(left);
if (block instanceof org.bukkit.block.Block) {
loc = ((org.bukkit.block.Block) block).getLocation();
}
}
} catch (Exception ignored) {}
// ★ 双箱特殊处理:DoubleChest 的 holder 不是 BlockState,
// 必须同时收集左右两侧位置——任一侧在领地内即受 denyContainer 管控。
// 旧实现只反射取 left 且 getLeft() 返回 Inventory 无 getBlock(),
// 反射异常被吞后 loc=null 直接放行,导致大箱子完全绕过领地判定。
if (e.getInventory() instanceof org.bukkit.inventory.DoubleChestInventory) {
org.bukkit.inventory.DoubleChestInventory dci =
(org.bukkit.inventory.DoubleChestInventory) e.getInventory();
addChestSideLocation(locs, dci.getLeftSide());
addChestSideLocation(locs, dci.getRightSide());
} else if (holder instanceof org.bukkit.block.DoubleChest) {
org.bukkit.block.DoubleChest dc = (org.bukkit.block.DoubleChest) holder;
addChestSideLocation(locs, dc.getLeftSide());
addChestSideLocation(locs, dc.getRightSide());
}
// ★ 检测所有容器类型:箱子、熔炉、高炉、潜影盒、末影箱、讲台、铁砧、信标、漏斗、发射器、投掷器、酿造台、工作台等
else if (holder instanceof BlockState) {
BlockState bs = (BlockState) holder;
loc = bs.getLocation();
locs.add(bs.getLocation());
} else if (holder instanceof Container) {
// Container接口覆盖所有方块容器
if (holder instanceof org.bukkit.block.Block) {
org.bukkit.block.Block block = (org.bukkit.block.Block) holder;
loc = block.getLocation();
locs.add(block.getLocation());
}
}
// ★ Issue 9: 矿车容器(漏斗矿车、运输矿车)
if (holder != null && loc == null) {
if (holder != null && locs.isEmpty()) {
String className = holder.getClass().getSimpleName();
if (className.contains("Minecart")) {
// 矿车容器使用实体位置
if (holder instanceof org.bukkit.entity.Entity) {
org.bukkit.entity.Entity entity = (org.bukkit.entity.Entity) holder;
loc = entity.getLocation();
locs.add(entity.getLocation());
}
}
}
// 末影箱特殊处理:检查末影箱所在位置而非玩家位置
if (holder != null) {
String typeName = holder.getClass().getSimpleName();
if (typeName.contains("EnderChest") || typeName.contains("Ender")) {
// 末影箱也是BlockState,已经在上面处理
}
if (locs.isEmpty()) return;
// ★ 跨边界大箱子:任一侧在领地内即判定;先取第一个命中领地做打开记录
AreaConfig ac = null;
Location hitLoc = null;
for (Location loc : locs) {
if (loc == null || loc.getWorld() == null) continue;
AreaConfig a = getArea(loc.getWorld().getName(),
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
if (a != null) { ac = a; hitLoc = loc; break; }
}
if (loc == null) return;
AreaConfig ac = getArea(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
if (ac == null) return;
if (ac == null || hitLoc == null) return;
// ★ 领地记录(所有容器打开都记录,含领地主/管理员/公共设施)
String containerType;
if (loc.getBlock().getType() != org.bukkit.Material.AIR) {
containerType = loc.getBlock().getType().name();
if (hitLoc.getBlock().getType() != org.bukkit.Material.AIR) {
containerType = hitLoc.getBlock().getType().name();
} else {
containerType = holder.getClass().getSimpleName();
}
plugin.landRecordManager.recordContainerOpen(ac, p, loc, containerType, e.getInventory());
plugin.landRecordManager.recordContainerOpen(ac, p, hitLoc, containerType, e.getInventory());
if (hasPermission(p, ac, PermissionLevel.OWNER)) return;
// ★ 逐侧判定:任一侧所在领地禁止容器访问即拦截(跨边界大箱子不再漏网)
for (Location loc : locs) {
if (loc == null || loc.getWorld() == null) continue;
AreaConfig a = getArea(loc.getWorld().getName(),
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
if (a == null) continue;
if (hasPermission(p, a, PermissionLevel.OWNER)) continue;
// ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查)
if (ac.isPublicBuilding) return;
// ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查)
if (a.isPublicBuilding) continue;
// ★ 容器管理权限:denyContainer控制访客能否访问容器
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
// 对无权限玩家则走领地默认 denyContainer 配置
if (getEffectiveDeny(p, ac, "denyContainer")) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
return;
// ★ 容器管理权限:denyContainer控制访客能否访问容器
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
// 对无权限玩家则走领地默认 denyContainer 配置
if (getEffectiveDeny(p, a, "denyContainer")) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
return;
}
}
}
public void loadWhitelists() {
// ★ 双箱侧边定位:从半边(Inventory 或 InventoryHolder)的 BlockState holder 取其方块位置
private void addChestSideLocation(java.util.List<Location> locs, Object side) {
if (side == null) return;
Object sh = side;
if (side instanceof org.bukkit.inventory.Inventory) {
sh = ((org.bukkit.inventory.Inventory) side).getHolder();
}
if (sh instanceof BlockState) {
Location l = ((BlockState) sh).getLocation();
if (l != null) locs.add(l);
} else if (sh instanceof org.bukkit.block.Block) {
locs.add(((org.bukkit.block.Block) sh).getLocation());
}
}
public void loadWhitelists() {
globalPlayerWhitelist.clear();
areaPlayerWhitelist.clear();
+79 -32
View File
@@ -109,7 +109,7 @@ public class ChatFilterManager {
static class VerificationData {
long createTime;
int a, b, op; // 0=加法, 1=减法, 2=乘法
int a, b, op; // 0=加法, 1=减法, 2=乘法(九九表), 3=除法(九九表)
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
VerificationType type; // 验证码类型
boolean completed;
@@ -120,6 +120,7 @@ public class ChatFilterManager {
this.type = type;
}
/** 30秒未作答 = 超时;超时【不放行】,由上层清掉旧题重新出题 */
boolean isExpired() {
return System.currentTimeMillis() - createTime > 30000;
}
@@ -130,7 +131,8 @@ public class ChatFilterManager {
NEED_VERIFICATION, // 需要验证码
PENDING, // 验证码进行中
VERIFIED, // 已验证
FAILED // 验证失败(消息吞噬)
FAILED, // 验证失败(消息吞噬)
TIMEOUT // 超时未作答(不放行,自动重新出题)
}
/** 验证码答案缓存,供外部判断是否为答案 */
@@ -161,11 +163,12 @@ public class ChatFilterManager {
verificationData.remove(name);
return VerificationResult.VERIFIED;
}
// 验证码是否过期 — 过期后自动放行,不无限卡玩家
// ★ 30秒未作答 = 超时:不放行,清掉旧题并重新出题(绝不加入 verifiedPlayers)
if (vd.isExpired()) {
verificationData.remove(name);
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
p.sendMessage("§c§l[验证码] §c上一题超时,已为你换新题");
generateMathVerification(name);
return VerificationResult.TIMEOUT;
}
return VerificationResult.PENDING;
}
@@ -184,43 +187,76 @@ public class ChatFilterManager {
private static final ThreadLocal<Random> sharedRandom = ThreadLocal.withInitial(Random::new);
/**
* 生成随机验证码:数学题或GUI随机50%概率
* 生成随机验证码:加 / 减 / 乘 / 除 / GUI 五种每次随机抽一种。
* 乘法与除法严格取自九九乘法表。
*
* ★ 出题前必须先关掉可能还开着的旧验证码GUI:旧GUI 的 InventoryCloseEvent
* 会清 verificationData,让它先跑完,否则刚生成的新题会被清掉(点哪儿都没用)。
*/
public void generateMathVerification(String playerName) {
Random rand = sharedRandom.get();
// 50% 概率数学题,50% 概率GUI
if (rand.nextBoolean()) {
generateMathChallenge(playerName, rand);
} else {
Player online = org.bukkit.Bukkit.getPlayer(playerName);
if (online != null) {
closeVerificationGuiIfOpen(online);
}
int kind = rand.nextInt(5); // 0加 1减 2乘 3除 4GUI
if (kind == 4) {
generateGUIChallenge(playerName, rand);
} else {
generateMathChallenge(playerName, kind, rand);
}
}
/** 关掉还开着的验证码GUI(标题匹配才关,避免误关玩家自己的箱子/菜单) */
private void closeVerificationGuiIfOpen(Player p) {
try {
String title = p.getOpenInventory().getTitle();
if (title != null && title.contains("点击") && title.contains("完成验证")) {
p.closeInventory();
}
} catch (Exception ignored) {
}
}
/** 生成数学题验证码 */
private void generateMathChallenge(String playerName, Random rand) {
int op = rand.nextInt(3); // 0=加, 1=减, 2=乘
/** 生成数学题验证码(op: 0加 1减 2乘 3除;乘/除每次从九九乘法表随机抽一题) */
private void generateMathChallenge(String playerName, int op, Random rand) {
int a, b;
switch (op) {
case 0: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
case 1: a = rand.nextInt(10) + 2; b = rand.nextInt(a) + 1; break;
default: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
case 0: // 两位数加法
a = 10 + rand.nextInt(90);
b = 10 + rand.nextInt(90);
break;
case 1: // 两位数减法(保证结果仍是两位数,不出现负数)
a = 20 + rand.nextInt(80);
b = 10 + rand.nextInt(a - 19); // b ∈ [10, a-10],结果 ≥ 10
break;
case 2: // 九九乘法表:1×1 ~ 9×9
a = 1 + rand.nextInt(9);
b = 1 + rand.nextInt(9);
break;
default: // 九九乘法表除法(整除):(i×j) ÷ j = i
b = 1 + rand.nextInt(9);
a = (1 + rand.nextInt(9)) * b;
break;
}
VerificationData vd = new VerificationData(a, b, op, VerificationType.MATH);
verificationData.put(playerName, vd);
String opSymbol = switch (op) {
case 0 -> "+";
case 1 -> "-";
default -> "×";
case 2 -> "×";
default -> "÷";
};
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
Player p = org.bukkit.Bukkit.getPlayer(playerName);
if (p != null && p.isOnline()) {
p.sendMessage("§e§l[验证码] §f请回答: §e" + a + " " + opSymbol + " " + b + " = ?");
p.sendMessage("§7(30秒内输入答案)");
p.sendMessage("§7(30秒内输入答案,超时会自动换新题)");
}
});
}
@@ -366,8 +402,8 @@ public class ChatFilterManager {
p.openInventory(inv);
} catch (Exception ex) {
plugin.getLogger().warning("[验证码] GUI挑战生成失败: " + ex.getMessage());
// 降级为数学题
generateMathChallenge(playerName, rand);
// 降级为数学题(随机抽一种运算)
generateMathChallenge(playerName, rand.nextInt(4), rand);
}
}
});
@@ -391,10 +427,11 @@ public class ChatFilterManager {
VerificationData vd = verificationData.get(playerName);
if (vd == null) return VerificationResult.VERIFIED;
// ★ 超时不放行:清旧题并重新出题
if (vd.isExpired()) {
verificationData.remove(playerName);
verifiedPlayers.add(playerName);
return VerificationResult.VERIFIED;
generateMathVerification(playerName);
return VerificationResult.TIMEOUT;
}
if (vd.type == VerificationType.MATH) {
@@ -414,10 +451,11 @@ public class ChatFilterManager {
VerificationData vd = verificationData.get(playerName);
if (vd == null) return VerificationResult.VERIFIED; // 无验证码 → 放行
// ★ 超时不放行:清旧题并重新出题
if (vd.isExpired()) {
verificationData.remove(playerName);
verifiedPlayers.add(playerName); // 过期后不再要求
return VerificationResult.VERIFIED;
generateMathVerification(playerName);
return VerificationResult.TIMEOUT;
}
if (vd.type == VerificationType.MATH) {
@@ -427,7 +465,8 @@ public class ChatFilterManager {
switch (vd.op) {
case 0 -> expected = vd.a + vd.b;
case 1 -> expected = vd.a - vd.b;
default -> expected = vd.a * vd.b;
case 2 -> expected = vd.a * vd.b;
default -> expected = vd.a / vd.b; // 九九表整除
}
if (ans == expected) {
@@ -462,13 +501,20 @@ public class ChatFilterManager {
public VerificationResult checkGUIClick(String playerName, String clickedItemName) {
String name = playerName;
VerificationData vd = verificationData.get(name);
if (vd == null) return VerificationResult.VERIFIED;
// ★ 没有题目(旧题已被5秒定时器清掉)时绝不能算通过,
// 否则超时后随便点一下就绕过验证;已通过的老玩家仍按通过处理(防连点二次出题)
if (vd == null) {
return verifiedPlayers.contains(name)
? VerificationResult.VERIFIED
: VerificationResult.TIMEOUT;
}
if (!VerificationType.GUI.equals(vd.type)) return VerificationResult.PENDING;
// ★ 30秒未点选 = 超时:不放行,重新出题
if (vd.isExpired()) {
verificationData.remove(name);
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
generateMathVerification(name);
return VerificationResult.TIMEOUT;
}
// ★ 关键修复:点击的物品名必须等于目标物品名才通过
@@ -507,7 +553,8 @@ public class ChatFilterManager {
}
/**
* 清理过期的验证码
* 清理过期的验证码(只做内存回收,绝不把玩家标记为已验证;
* 玩家下一次发言/点击会走 NEED_VERIFICATION 或 TIMEOUT 分支重新出题)
*/
public void cleanupExpiredVerifications() {
long now = System.currentTimeMillis();
+15
View File
@@ -3428,6 +3428,16 @@ public class Main extends JavaPlugin
e.setCancelled(true);
return;
}
if (vr == ChatFilterManager.VerificationResult.TIMEOUT) {
// ★ 30秒超时:不放行(已在 ChatFilterManager 里清旧题并换新题)
// 这条消息:纯数字按"超时的答案"吞掉,其它照样缓存,避免玩家打的字丢失
if (!msg.trim().matches("-?\\d+")) {
chatFilter.cachePendingMessage(p.getName(), msg);
}
e.setCancelled(true);
return;
}
if (vr == ChatFilterManager.VerificationResult.PENDING) {
// 验证进行中:当前聊天框输入就是答案
@@ -4078,6 +4088,11 @@ public class Main extends JavaPlugin
}
chatFilter.pendingMessages.remove(p.getName());
}
} else if (vr == ChatFilterManager.VerificationResult.TIMEOUT) {
// ★ 超时/旧题已被清理:不放行、不清缓存消息,直接换新题
p.closeInventory();
p.sendMessage("§c§l[验证码] §c上一题超时,已为你换新题");
chatFilter.generateMathVerification(p.getName());
} else {
p.closeInventory();
chatFilter.pendingMessages.remove(p.getName());
+39 -7
View File
@@ -416,6 +416,33 @@ public class TeleportManager implements Listener {
}
}
/**
* 发送带可点击按钮的 tpaall(全服传送)通知
* ★ 语义:A 请求"全服玩家一起传送到 A 身边",接受者会被传到发起者那里,
* 绝不能复用 tpa 的"请求传送到你身边"文案(那是 A 想来到你这边)。
*/
private void sendClickableTPAllNotice(Player target, String senderName) {
if (isBedrockPlayer(target)) {
target.sendMessage("§e[传送] §f" + senderName + " §e请求全服玩家一起传送到他身边");
target.sendMessage("§7接受后,§f你 §7会被传送到 §f" + senderName + " §7身边");
target.sendMessage("§7────────────────────");
target.sendMessage("§a使用 §e/tpaccept §a接受,§e/tpdeny §a拒绝");
} else {
target.sendMessage(Component.text("§e[传送] §f" + senderName + " §e请求全服玩家一起传送到他身边"));
target.sendMessage(Component.text("§7(接受后,§f你 §7会被传送到 §f" + senderName + " §7身边)"));
target.sendMessage(Component.text("§7────────────────────"));
target.sendMessage(Component.empty()
.append(Component.text("§a[✔ 接受] ")
.clickEvent(ClickEvent.runCommand("/tpaccept " + senderName))
.hoverEvent(HoverEvent.showText(Component.text("接受 " + senderName + " 的全服传送请求"))))
.append(Component.text(" "))
.append(Component.text("§c[✘ 拒绝] ")
.clickEvent(ClickEvent.runCommand("/tpdeny " + senderName))
.hoverEvent(HoverEvent.showText(Component.text("拒绝 " + senderName + " 的全服传送请求")))));
target.sendMessage(Component.text("§7或输入 §e/tpaccept §7查看全部待处理请求"));
}
}
// ==================== CLI 交互菜单 ====================
/**
@@ -509,9 +536,9 @@ public class TeleportManager implements Listener {
// 全服传送
player.sendMessage(Component.empty()
.append(Component.text(" §e/tpaall "))
.append(Component.text("§b[请求全服传送到你身边] ")
.append(Component.text("§b[请求全服玩家一起传送到你身边] ")
.clickEvent(ClickEvent.runCommand("/tpaall"))
.hoverEvent(HoverEvent.showText(Component.text("向所有在线玩家发送传送请求")))));
.hoverEvent(HoverEvent.showText(Component.text("向全服在线玩家发送传送请求,接受后他们一起传送到你所在位置")))));
// 自动接受开关
String autoStatus = autoAccept ? "§a已开启" : "§c已关闭";
@@ -764,8 +791,13 @@ public class TeleportManager implements Listener {
removeIncomingRequest(player.getName(), targetName);
removeOutgoingRequest(targetName, player.getName());
player.sendMessage("§a[传送] 已传送到 §f" + targetName + " §a身边");
actualSender.sendMessage("§a[传送] §f" + player.getName() + " §a已接受请求");
if ("tpaall".equals(requestType)) {
player.sendMessage("§a[传送] §a已接受全服传送,你被传送到 §f" + targetName + " §a身边");
actualSender.sendMessage("§a[传送] §f" + player.getName() + " §a已接受全服传送,来到你身边");
} else {
player.sendMessage("§a[传送] 已传送到 §f" + targetName + " §a身边");
actualSender.sendMessage("§a[传送] §f" + player.getName() + " §a已接受请求");
}
teleportCooldown.remove(actualSender.getName());
return true;
@@ -1106,8 +1138,8 @@ public class TeleportManager implements Listener {
teleportRequestTimes.put(sender + ":" + name, now);
teleportRequestTypes.put(sender + ":" + name, "tpaall");
// 可点击通知
sendClickableRequestNotice(p, sender);
// 可点击通知(tpaall 专用文案:全服传送到发起者身边)
sendClickableTPAllNotice(p, sender);
count++;
}
@@ -1231,7 +1263,7 @@ public class TeleportManager implements Listener {
ItemMeta tpaAllMeta = tpaAllItem.getItemMeta();
if (tpaAllMeta != null) {
tpaAllMeta.setDisplayName("§b全服传送");
tpaAllMeta.setLore(Arrays.asList("§7请求所有玩家传送到你身边"));
tpaAllMeta.setLore(Arrays.asList("§7全服玩家接受后一起传送到你身边", "§7(A 请求全服玩家一起传送到 A 身边)"));
tpaAllItem.setItemMeta(tpaAllMeta);
}
inv.setItem(4, tpaAllItem);