Compare commits

..
43 Commits
Author SHA1 Message Date
youpaishidifu 168d4f39dc whois 补全封禁状态/理由/到期时间展示
Auto Release / build (push) Successful in 32s
2026-07-24 16:30:39 +08:00
youpaishidifu 862a2736b3 三联改:whois IP归属查询+tempban/banip封禁踢人+URL支持中文域名
Auto Release / build (push) Successful in 33s
17. whois新增IP归属查询:异步调用ip9.com.cn接口展示国家/省份/城市/运营商
18. tempban/banip订正:IP封禁时踢出所有匹配该IP的在线玩家
19. URL_PATTERN改用\p{L}支持中文域名如www.百度.com
2026-07-24 16:13:08 +08:00
youpaishidifu aaffc760f3 修复:永久确认玩家重进不再空手——进入时重新发放锁定装备
Auto Release / build (push) Successful in 30s
- 永久确认分支新增 equipFullSet 调用(背包已在进入时清空)
- SavedSelection 补充 shield/food 字段,跨次保留完整选装状态
- confirmEquipment 和 openEquipmentSelection 同步更新 6 参数构造
- 重进时自动 cancelKickTimeout + 发送就绪提示
2026-07-24 11:21:59 +08:00
youpaishidifu 8ce4d58195 PVP装备永久锁定:选装一次即锁定,退出再进入不可重选
Auto Release / build (push) Successful in 29s
- 新增 permanentlyConfirmed 集合 + savedSelections 保存选装结果
- confirmEquipment 写入永久确认状态和武器/护甲/附魔/远程选择
- openEquipmentSelection 检查 permanentlyConfirmed,有则恢复选装状态直接打开调整GUI
- 退出竞技场不再清除永久确认状态(仅食物/盾牌/弓弩可调)
- resetMatchLock 清空永久确认(世界重置时全部重新选装)
2026-07-24 10:52:57 +08:00
youpaishidifu 6149794e36 PVP竞技场选装锁定+调整GUI
Auto Release / build (push) Successful in 33s
- 确认装备后武器/护甲/附魔锁定(不可重选)
- 新增 openAdjustmentGUI 调整界面:仅食物/盾牌/弓弩可调
- handleEquipmentClick 确认模式拦截锁定选项,允许可调项立即生效
- reEquipAdjustments 只更新食物/盾牌/弓弩,不动锁定装备
- 新增 /pvp equip / /pvp adjust 命令打开调整界面
- 箭矢加 PVP_ITEM_MARKER 标签,re-adjust 时正确清理旧箭
2026-07-24 10:30:37 +08:00
youpaishidifu 0d3547dded URL拦截放宽:下划线分隔的字符串不在非法域名列表则跳过
Auto Release / build (push) Successful in 31s
- ChatFilterManager.shouldBlockUrl(): 下划线分隔的必须命中非法域名后缀才拦截
- 玩家名下划线误匹配(如 player_name)后缀不在黑名单 → 跳过不处罚
- 点分隔域名正常拦截不变
- 改进: onChat + filterLinkInContent 两处调用
2026-07-23 21:58:35 +08:00
youpaishidifu a1e1e919e1 搭桥蛋连续修复:每tick都记录桥位,不再遗漏tick2
Auto Release / build (push) Successful in 28s
- 原skip=2不更新bridgeLastLoc,skip=3用skip=1旧位导致断层
- 改前2tick每tick都记录位置,skip=3起每tick放上一tick桥位
- 方块连续排列,不再中间断开
2026-07-23 19:43:37 +08:00
youpaishidifu a145a90f41 搭桥蛋修复合自身方块碰撞:方块放上一tick位置
Auto Release / build (push) Successful in 27s
- 新增 bridgeLastLoc map 记录蛋每tick位置
- 方块不放在蛋当前位置(会撞上),改放上一tick位置
- 蛋持续飞行,仅在撞到真实障碍物时停止
2026-07-23 19:32:49 +08:00
youpaishidifu b8ccd56818 火球修复:落点产生爆炸并点燃方块
Auto Release / build (push) Successful in 30s
- fb.setYield(1.5f) 爆炸半径约1.5格
- fb.setIsIncendiary(true) 点燃爆炸范围内方块
2026-07-23 19:07:07 +08:00
youpaishidifu 89525a4e6b 搭桥蛋修复:方块改为玻璃、跳过前2tick避免生成玩家头顶
Auto Release / build (push) Successful in 30s
2026-07-23 18:13:38 +08:00
youpaishidifu 8b8823e01d PVP世界新增火球/搭桥蛋道具,奖励改為5杀一轮回
Auto Release / build (push) Successful in 31s
- 火球: PVP世界右键发射SmallFireball,命中生物造成5点伤害
- 搭桥蛋: 右键发射Egg,跟随抛物线放置玻璃方块,遇实体方块停止
- 奖励改5杀一轮回: 2杀发16熟食+1火球,5杀发16箭+1搭桥蛋+1火球+替换全身铁甲
2026-07-23 17:52:04 +08:00
youpaishidifu 38d06c3cfa chore: 删除多余的备份文件 Main.java.bak
Auto Release / build (push) Successful in 30s
2026-07-23 12:17:34 +08:00
youpaishidifu d20a525855 fix: sdf1_login get 不生效-radio块缺少闭括号导致后续子命令全部嵌套在其中
Auto Release / build (push) Successful in 29s
2026-07-23 12:17:10 +08:00
youpaishidifu faa17ded32 feat: onJoin注册封顶限制+新增/sdf1_login get自查询账号
Auto Release / build (push) Successful in 32s
2026-07-23 11:56:10 +08:00
youpaishidifu e8588ae408 fix: 禁言玩家聊天时显示详细处罚信息 全服广播不暴露违禁词
Auto Release / build (push) Successful in 29s
2026-07-23 01:12:53 +08:00
youpaishidifu 3f7c8e4001 fix: applyPunishment 通知逻辑按 chat.txt 配置的 通知管理员/全服通报 开关执行
Auto Release / build (push) Successful in 31s
2026-07-23 00:40:57 +08:00
youpaishidifu 4692b32a6b fix: applyPunishment 增加管理员通知+全服广播+详细禁言消息
Auto Release / build (push) Successful in 27s
2026-07-23 00:33:30 +08:00
youpaishidifu 5d3b2c81c9 fix: 敏感词列表配置自动补全,缺节追加预设词,已有节跳过
Auto Release / build (push) Successful in 34s
2026-07-23 00:17:52 +08:00
youpaishidifu 0ec3bc46dd fix: DurationParser NoClassDefFoundError + 新增敏感词静默过滤系统
Auto Release / build (push) Successful in 33s
2026-07-22 22:51:40 +08:00
youpaishidifu 53c805c68f fix: 领地记录翻页复用 lastQueryLocations 缓存的坐标,避免 getTargetBlockExact 不准导致查无记录
Auto Release / build (push) Successful in 31s
2026-07-22 21:36:50 +08:00
youpaishidifu 34aca10edb fix: 全面规范化白名单名称存储和比较为小写,修复大小写匹配问题
Auto Release / build (push) Successful in 33s
2026-07-22 19:46:53 +08:00
youpaishidifu cdce0c0e19 feat: PVP战绩持久化+清空队列命令+队列积压丢弃+PVP查询3天限制
Auto Release / build (push) Successful in 31s
1. PVPManager新增showArenaTotalStats/showArenaSessionStats方法
2. DatabaseManager.getArenaSessionByNumber添加3天有效期检查
3. WebManager新增clearQueue()/discardOldestIfOverflow()方法
4. Main.java新增清空队列/cleartake命令处理
5. plugin.yml注册清空队列/cleartake命令
2026-07-22 15:45:25 +08:00
youpaishidifu b488dfa04f fix: PVP竞技场NoClassDefFoundError修复 + 和平模式设置
Auto Release / build (push) Successful in 31s
1. 匿名Runnable改为lambda,消除PVPArenaManager内部类问题
2. reapplyWorldRules添加Difficulty.PEACEFUL,排除敌对生物干扰
2026-07-22 13:21:44 +08:00
youpaishidifu 9213918995 fix: 末影人行为禁用时全面拦截 - 拾取方块、AI寻路、传送、自然生成
Auto Release / build (push) Successful in 27s
2026-07-22 11:02:05 +08:00
youpaishidifu 4ae8b595e7 fix: 末影人行为权限逻辑修正 - 禁用时才拦截,开启时放行MC原生逻辑
Auto Release / build (push) Successful in 32s
2026-07-22 10:44:46 +08:00
youpaishidifu 23ab0c3457 fix: 记录分页只显示有数据的页数 - 检查下一页数据再显示下一页链接
Auto Release / build (push) Successful in 29s
2026-07-22 00:40:29 +08:00
youpaishidifu fc72e231d3 feat: 三个小改动 - 耕地破坏全局生效(含末影人抱耕地) / 末影人传送更名末影人行为(加自然生成拦截) / 领地破坏记录默认按天分页+可点击翻页
Auto Release / build (push) Successful in 32s
2026-07-22 00:26:15 +08:00
youpaishidifu c64bc718a6 实现3个新功能:耕地破坏权限(denyFarmlandTrample)+末影人传送控制(denyEnderTeleport)+容器记录按天分页
Auto Release / build (push) Successful in 27s
2026-07-21 22:49:35 +08:00
youpaishidifu e860b6ff4f 修复领地容器权限反转:删除getPermissionLevel预检查,全权交由getEffectiveDeny决定
Auto Release / build (push) Successful in 30s
2026-07-21 21:39:41 +08:00
youpaishidifu a6d70aed3c 修复领地移动权限反转:删除getPermissionLevel预检查,全权交由getEffectiveDeny决定
Auto Release / build (push) Successful in 29s
2026-07-21 21:32:27 +08:00
youpaishidifu 2fc34375d3 修复领地TP传送回退+移动权限调试日志
Auto Release / build (push) Successful in 33s
2026-07-21 21:06:30 +08:00
youpaishidifu 9958f09fcd 修复领地系统两个问题:1.移动权限侦听增加未授权玩家清出 2.删除提示只对操作者本人展示
Auto Release / build (push) Successful in 40s
2026-07-21 19:44:35 +08:00
youpaishidifu 8523e52919 清理调试日志: 高频缓冲日志精简为仅关键事件
Auto Release / build (push) Successful in 33s
2026-07-20 21:44:27 +08:00
youpaishidifu b1379350b5 修复高频出售缓冲: syncBondTransactions被定时器C绕过导致缓冲不生效,缓冲期内跳过周期推送
Auto Release / build (push) Successful in 32s
2026-07-20 21:32:06 +08:00
youpaishidifu e0644a0ec4 调试: 高频出售缓冲添加详细诊断日志,排查Linux环境缓冲不生效问题
Auto Release / build (push) Successful in 32s
2026-07-20 19:16:42 +08:00
youpaishidifu e4640d9102 移除不必要的缓冲日志
Auto Release / build (push) Successful in 26s
2026-07-20 14:52:19 +08:00
youpaishidifu 09d0be84b3 Merge remote-tracking branch 'origin/main'
Auto Release / build (push) Successful in 28s
2026-07-20 14:51:00 +08:00
youpaishidifu bc46ec27d0 移除不必要的缓冲日志 2026-07-20 14:50:19 +08:00
ypshidifu 7ab5aee87c 安全加固: .gitignore添加deploy_*.py/ps1/sh排除规则 + 支付密钥double insurance规则
Auto Release / build (push) Successful in 30s
2026-07-20 13:59:31 +08:00
youpaishidifu f19efa89a8 高频出售缓冲机制: 1分钟内单种商品≥10次时暂缓推送60秒
Auto Release / build (push) Successful in 2m39s
2026-07-20 13:18:58 +08:00
youpaishidifu 182545de59 feat: 密码错误阈值封禁(Bukkit真实封禁+邮件报警) + OAuth登录IP风控 + 配置多退少补
Auto Release / build (push) Successful in 34s
1. RiskControlManager: triggerBan改用Bukkit BanList API真实封禁玩家
   - BanList.addBan(name, reason, expiry)阻止重连加入
   - onJoin检查isBanned()双重防护
   - cleanup()定时清理过期封禁并pardon
   - unban()同步解除Bukkit封禁

2. Main.java onJoin: 新增风控封禁检查,封禁期内玩家无法加入

3. ConfigManager: loadSettings()自动补齐缺失配置项(多退少补)

4. 已有功能(上一次commit实现但未推送):
   - 密码错误阈值封禁+报警邮件
   - 正版OAuth登录IP风控
2026-07-19 14:41:04 +08:00
youpaishidifu c4a5fc9a61 fix: Add detailed logging to Create Release step for debugging
Auto Release / build (push) Successful in 27s
2026-07-19 01:23:43 +08:00
youpaishidifu caaf09fce0 fix: Gitea release tag now extracted from plugin.yml instead of build-N
Auto Release / build (push) Failing after 27s
2026-07-19 01:20:14 +08:00
18 changed files with 11239 additions and 9471 deletions
+53 -9
View File
@@ -32,25 +32,69 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
run: |
TAG="build-${{ gitea.run_number }}"
set -e
# 从 plugin.yml 提取版本号作为标签
VERSION=$(grep '^version:' src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
TAG="v${VERSION}"
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
echo "Version: ${VERSION}, Tag: ${TAG}"
echo "Token prefix: ${GITEA_TOKEN:0:8}..."
curl -s -X POST \
# 测试 token 权限
echo "=== Testing API access ==="
curl -s -w "\nHTTP_CODE:%{http_code}\n" \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}" | tail -5
# 如果同版本标签已存在,先删除旧 release
echo "=== Checking existing tag ${TAG} ==="
RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/releases/tags/${TAG}")
HTTP_CODE=$(echo "$RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
echo "Check tag HTTP: ${HTTP_CODE}"
if [ "$HTTP_CODE" = "200" ]; then
OLD_ID=$(echo "$RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
echo "Tag ${TAG} exists (release #${OLD_ID}), deleting..."
curl -s -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/releases/${OLD_ID}"
curl -s -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/tags/${TAG}"
sleep 1
elif [ "$HTTP_CODE" = "404" ]; then
echo "Tag ${TAG} not found, will create new"
else
echo "Warning: unexpected response checking tag"
fi
# 创建新 release
echo "=== Creating release ${TAG} ==="
RELEASE_RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Build ${{ gitea.run_number }}\",\"body\":\"Commit: ${{ gitea.sha }}\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
"${BASE}/releases"
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Sdf1_login ${TAG}\",\"body\":\"Version: ${VERSION}\\nCommit: ${{ gitea.sha }}\\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
"${BASE}/releases")
RELEASE_HTTP=$(echo "$RELEASE_RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
echo "Create release HTTP: ${RELEASE_HTTP}"
RELEASE_ID=$(curl -s \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/releases/tags/${TAG}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ "$RELEASE_HTTP" != "201" ]; then
echo "=== Release creation failed! ==="
echo "$RELEASE_RESP" | head -1
exit 1
fi
# 上传 JAR 资产
RELEASE_ID=$(echo "$RELEASE_RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -n "$RELEASE_ID" ]; then
echo "=== Uploading JAR to release #${RELEASE_ID} ==="
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@target/Sdf1_login.jar" \
"${BASE}/releases/${RELEASE_ID}/assets"
echo "Release ${TAG} created with JAR"
echo "✅ Release ${TAG} created with JAR"
else
echo "Release tag ${TAG} created (JAR attach may need manual check)"
echo "⚠️ Release created but could not get release ID for JAR upload"
fi
+8
View File
@@ -46,6 +46,9 @@ backup/
新建*.txt
群戏归类.txt
upload_sftp.py
deploy_*.py
deploy_*.ps1
deploy_*.sh
# PHP Web 后端
web/Web.db
@@ -58,6 +61,11 @@ web/self_update.php
web/version_check.php
web/debug_*.php
web/db/debug_*.php
# 支付密钥 - 严禁提交 (double insurance,配合 Web/.gitignore)
Web/密钥文件_git拉黑.txt
web/密钥文件_git拉黑.txt
Web/pay_secrets.php
web/pay_secrets.php
# Windows 保留设备名(无法用普通 rm 删除)
nul
@@ -565,6 +565,8 @@ public class AreaCLIManager {
case "audio": oldState = land.denyNoteblockJukebox; land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
case "lead": oldState = land.denyLead; land.denyLead = !land.denyLead; break;
case "crop_harvest": oldState = land.denyCropHarvest; land.denyCropHarvest = !land.denyCropHarvest; break;
case "farmland_trample": oldState = land.denyFarmlandTrample; land.denyFarmlandTrample = !land.denyFarmlandTrample; break;
case "ender_teleport": oldState = land.denyEnderTeleport; land.denyEnderTeleport = !land.denyEnderTeleport; break;
case "wool_shear": oldState = land.denyWoolShear; land.denyWoolShear = !land.denyWoolShear; break;
case "animal_feed": oldState = land.denyAnimalFeeding; land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
case "mob_attack": oldState = land.denyMobAttack; land.denyMobAttack = !land.denyMobAttack; break;
@@ -622,6 +624,8 @@ public class AreaCLIManager {
case "audio": return "denyNoteblockJukebox";
case "lead": return "denyLead";
case "crop_harvest": return "denyCropHarvest";
case "farmland_trample": return "denyFarmlandTrample";
case "ender_teleport": return "denyEnderTeleport";
case "wool_shear": return "denyWoolShear";
case "animal_feed": return "denyAnimalFeeding";
case "mob_attack": return "denyMobAttack";
@@ -682,6 +686,8 @@ public class AreaCLIManager {
perms.add(new PermItem("audio", "音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
perms.add(new PermItem("lead", "拴绳使用", !land.denyLead));
perms.add(new PermItem("crop_harvest", land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
perms.add(new PermItem("farmland_trample", land.denyFarmlandTrample ? "禁用耕地破坏" : "启用耕地破坏", !land.denyFarmlandTrample));
perms.add(new PermItem("ender_teleport", land.denyEnderTeleport ? "禁用末影人行为" : "启用末影人行为", !land.denyEnderTeleport));
perms.add(new PermItem("wool_shear", "剪切羊毛/生物", !land.denyWoolShear));
perms.add(new PermItem("animal_feed", land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
perms.add(new PermItem("mob_attack", land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
@@ -721,6 +727,8 @@ public class AreaCLIManager {
case "audio": return "音频";
case "lead": return "拴绳使用";
case "crop_harvest": return "农作物收获";
case "farmland_trample": return "耕地破坏";
case "ender_teleport": return "末影人传送";
case "wool_shear": return "剪切羊毛";
case "animal_feed": return "投喂动物";
case "mob_attack": return "攻击生物";
@@ -1135,6 +1143,8 @@ public class AreaCLIManager {
{"audio", "音频(音符盒/唱片机)", "denyNoteblockJukebox"},
{"lead", "拴绳使用", "denyLead"},
{"crop_harvest", "农作物收获", "denyCropHarvest"},
{"farmland_trample", "耕地破坏", "denyFarmlandTrample"},
{"ender_teleport", "末影人行为", "denyEnderTeleport"},
{"wool_shear", "剪切羊毛/生物", "denyWoolShear"},
{"animal_feed", "投喂动物", "denyAnimalFeeding"},
{"mob_attack", "攻击生物", "denyMobAttack"},
@@ -1173,6 +1183,14 @@ public class AreaCLIManager {
if (field.equals("denySignEdit")) {
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
}
// ★ denyFarmlandTrample显示名称
if (field.equals("denyFarmlandTrample")) {
name = landDefault ? "禁用耕地破坏" : "启用耕地破坏";
}
// ★ denyEnderTeleport显示名称(末影人行为)
if (field.equals("denyEnderTeleport")) {
name = landDefault ? "禁用末影人行为" : "启用末影人行为";
}
// 如果有per-player覆盖,使用覆盖值;否则使用领地默认
if (playerPerms.containsKey(field)) {
@@ -1220,6 +1238,8 @@ public class AreaCLIManager {
case "denyNoteblockJukebox": return land.denyNoteblockJukebox;
case "denyLead": return land.denyLead;
case "denyCropHarvest": return land.denyCropHarvest;
case "denyFarmlandTrample": return land.denyFarmlandTrample;
case "denyEnderTeleport": return land.denyEnderTeleport;
case "denyWoolShear": return land.denyWoolShear;
case "denyAnimalFeeding": return land.denyAnimalFeeding;
case "denyGlowing": return land.denyGlowing;
+12 -1
View File
@@ -498,7 +498,8 @@ public class AreaGUIManager implements Listener {
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
{"攻击生物", "denyMobAttack"},
{"禁止编辑告示牌", "denySignEdit"},
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"}
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"},
{"耕地破坏", "denyFarmlandTrample"}, {"末影人行为", "denyEnderTeleport"}
};
for (String[] def : permDefs) {
@@ -529,6 +530,12 @@ public class AreaGUIManager implements Listener {
if (field.equals("denySignEdit")) {
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
}
if (field.equals("denyFarmlandTrample")) {
name = landDefault ? "禁用耕地破坏" : "启用耕地破坏";
}
if (field.equals("denyEnderTeleport")) {
name = landDefault ? "禁用末影人行为" : "启用末影人行为";
}
// ★ 对于 allowPickup/allowDrop,enabled=true 表示"允许操作"(即 allow=true)
// 其他权限,enabled = true 表示"允许操作"(即 deny=false)
@@ -691,6 +698,8 @@ public class AreaGUIManager implements Listener {
perms.add(new PermEntry(land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
perms.add(new PermEntry(land.denySignEdit ? "禁止编辑告示牌" : "允许编辑告示牌", !land.denySignEdit));
perms.add(new PermEntry("玩家发光", !land.denyGlowing));
perms.add(new PermEntry(land.denyFarmlandTrample ? "禁用耕地破坏" : "启用耕地破坏", !land.denyFarmlandTrample));
perms.add(new PermEntry(land.denyEnderTeleport ? "禁用末影人行为" : "启用末影人行为", !land.denyEnderTeleport));
perms.add(new PermEntry("传送", land.allowVisitorTeleport));
perms.add(new PermEntry("和平模式", land.peaceMode));
perms.add(new PermEntry("公共建筑设施", land.isPublicBuilding));
@@ -752,6 +761,8 @@ public class AreaGUIManager implements Listener {
case "禁止攻击生物": case "允许攻击生物": land.denyMobAttack = !land.denyMobAttack; break;
case "禁止编辑告示牌": case "允许编辑告示牌": land.denySignEdit = !land.denySignEdit; break;
case "玩家发光": land.denyGlowing = !land.denyGlowing; break;
case "禁用耕地破坏": case "启用耕地破坏": land.denyFarmlandTrample = !land.denyFarmlandTrample; break;
case "禁用末影人行为": case "启用末影人行为": land.denyEnderTeleport = !land.denyEnderTeleport; break;
case "传送": land.allowVisitorTeleport = !land.allowVisitorTeleport; break;
case "和平模式": land.peaceMode = !land.peaceMode; break;
case "公共建筑设施": land.isPublicBuilding = !land.isPublicBuilding; break;
+140 -32
View File
@@ -46,6 +46,7 @@ import java.util.HashSet;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.event.entity.EntitySpawnEvent;
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
import org.bukkit.entity.Item;
import org.bukkit.event.inventory.InventoryOpenEvent;
@@ -322,18 +323,19 @@ public class AreaProtection implements Listener {
private boolean isExemptFromGameMode(
Player p, String areaName) {
String name = p.getName();
String lower = name.toLowerCase();
// 管理员豁免
if (isAreaAdmin(p)) {
return true;
}
if (globalPlayerWhitelist.contains(name)) {
if (globalPlayerWhitelist.contains(lower)) {
return true;
}
Set<String> aw =
areaPlayerWhitelist.get(areaName);
if (aw != null && aw.contains(name)) {
if (aw != null && aw.contains(lower)) {
return true;
}
AreaConfig ac = areas.get(areaName);
@@ -1220,6 +1222,8 @@ public class AreaProtection implements Listener {
try { ac.denyNoteblockJukebox = rs.getInt("deny_noteblock_jukebox") == 1; } catch (Exception ignored) {}
try { ac.denyLead = rs.getInt("deny_lead") == 1; } catch (Exception ignored) {}
try { ac.denyCropHarvest = rs.getInt("deny_crop_harvest") == 1; } catch (Exception ignored) {}
try { ac.denyFarmlandTrample = rs.getInt("deny_farmland_trample") == 1; } catch (Exception ignored) {}
try { ac.denyEnderTeleport = rs.getInt("deny_ender_teleport") == 1; } catch (Exception ignored) {}
try { ac.denyWoolShear = rs.getInt("deny_wool_shear") == 1; } catch (Exception ignored) {}
try { ac.denyAnimalFeeding = rs.getInt("deny_animal_feeding") == 1; } catch (Exception ignored) {}
try { ac.denyContainer = rs.getInt("deny_container") == 1; } catch (Exception ignored) {}
@@ -1496,6 +1500,8 @@ public class AreaProtection implements Listener {
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_noteblock_jukebox INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_lead INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_crop_harvest INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_farmland_trample INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_ender_teleport INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wool_shear INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_animal_feeding INTEGER DEFAULT 0"); } catch (Exception ignored) {}
// ★ 传送点列
@@ -2882,6 +2888,8 @@ public class AreaProtection implements Listener {
case "denyNoteblockJukebox": return ac.denyNoteblockJukebox;
case "denyLead": return ac.denyLead;
case "denyCropHarvest": return ac.denyCropHarvest;
case "denyFarmlandTrample": return ac.denyFarmlandTrample;
case "denyEnderTeleport": return ac.denyEnderTeleport;
case "denyWoolShear": return ac.denyWoolShear;
case "denyAnimalFeeding": return ac.denyAnimalFeeding;
case "denyMobAttack": return ac.denyMobAttack;
@@ -3230,6 +3238,8 @@ public class AreaProtection implements Listener {
AreaConfig ac = areas.get(newArea);
if (ac != null) {
// ★ denyMove检查:支持per-player独立权限
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
// 对无权限玩家则走领地默认 denyMove 配置
if (getEffectiveDeny(p, ac, "denyMove")) {
Location safeLoc = findSafeExitLocation(p.getLocation(), ac);
if (safeLoc != null) {
@@ -3390,6 +3400,7 @@ public class AreaProtection implements Listener {
AreaConfig newAc = areas.get(newArea);
if (newAc != null) {
// ★ denyMove检查:支持per-player独立权限
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免
if (getEffectiveDeny(p, newAc, "denyMove")) {
Location safeLoc = findSafeExitLocation(event.getFrom(), newAc);
if (safeLoc != null) {
@@ -5342,6 +5353,14 @@ public class AreaProtection implements Listener {
e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ());
if (ac != null && ac.denyFire) e.setCancelled(true);
}
// ★ 末影人行为控制:禁止末影人抱走方块
if (e.getEntityType().name().equals("ENDERMAN")) {
AreaConfig ac = getArea(e.getBlock().getWorld().getName(),
e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ());
if (ac != null && ac.denyEnderTeleport) {
e.setCancelled(true);
}
}
}
// ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 =====
@@ -5456,24 +5475,14 @@ public class AreaProtection implements Listener {
// ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查)
if (ac.isPublicBuilding) return;
// ★ 漏洞封堵:完全无该领地权限的外部玩家(getPermissionLevel 返回 null),
// 无论容器管理(denyContainer)开关如何,一律禁止访问领地内容器。
// 否则 denyContainer=false 时所有玩家(含未授权)开箱无任何限制。
if (getPermissionLevel(p, ac) == null) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f你没有该领地的访问权限,无法使用容器");
return;
}
// ★ 容器管理权限:denyContainer=true时非领主/管理员不能访问容器
// ★ 容器管理权限:denyContainer控制访客能否访问容器
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
// 对无权限玩家则走领地默认 denyContainer 配置
if (getEffectiveDeny(p, ac, "denyContainer")) {
e.setCancelled(true);
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
return;
}
// ★ 容器交互已启用时,无权限级别的玩家也允许访问(访客权限)
// 只有denyContainer=true时才检查权限级别
}
public void loadWhitelists() {
@@ -5691,6 +5700,76 @@ public class AreaProtection implements Listener {
}
}
// ★ 耕地破坏(玩家在耕地上跳跃/掉落时触发)
@EventHandler
public void onFarmlandTrample(PlayerInteractEvent e) {
if (e.getAction() != Action.PHYSICAL) return;
Block block = e.getClickedBlock();
if (block == null || block.getType() != Material.FARMLAND) return;
Player p = e.getPlayer();
AreaConfig ac = getArea(
p.getWorld().getName(),
block.getX(), block.getY(), block.getZ());
if (ac != null && ac.denyFarmlandTrample) {
e.setCancelled(true);
}
}
// ★ 末影人行为控制(禁止末影人传送进入领地、在领地内传送、自然进入领地)
@EventHandler
public void onEnderTeleport(EntityTeleportEvent e) {
if (!e.getEntityType().name().equals("ENDERMAN")) return;
Location from = e.getFrom();
Location to = e.getTo();
// 检查起点是否在禁止末影人传送的领地内
if (from != null) {
AreaConfig fromAc = getArea(from.getWorld().getName(),
from.getBlockX(), from.getBlockY(), from.getBlockZ());
if (fromAc != null && fromAc.denyEnderTeleport) {
e.setCancelled(true);
return;
}
}
// 检查终点是否在禁止末影人传送的领地内
if (to != null) {
AreaConfig toAc = getArea(to.getWorld().getName(),
to.getBlockX(), to.getBlockY(), to.getBlockZ());
if (toAc != null && toAc.denyEnderTeleport) {
e.setCancelled(true);
return;
}
}
// 如果起点和终点都不在禁止传送的领地内,不干预(让MC自身逻辑处理)
// 注意:如果起点在允许传送的领地内,终点在禁止传送的领地内,已在上面处理
// 如果起点在禁止传送的领地内,终点在允许传送的领地内,已在上面处理
}
// ★ 末影人行为控制(禁止末影人AI寻路进入领地)
@EventHandler
public void onEndermanPathfind(EntityPathfindEvent e) {
if (!e.getEntity().getType().name().equals("ENDERMAN")) return;
Location loc = e.getLoc();
if (loc == null) return;
AreaConfig ac = getArea(loc.getWorld().getName(),
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
if (ac != null && ac.denyEnderTeleport) {
e.setCancelled(true);
}
}
// ★ 末影人生成控制(自然生成时禁止进入领地)
@EventHandler
public void onEndermanSpawn(CreatureSpawnEvent e) {
if (!e.getEntityType().name().equals("ENDERMAN")) return;
if (e.getSpawnReason() != CreatureSpawnEvent.SpawnReason.NATURAL) return;
Location loc = e.getLocation();
AreaConfig ac = getArea(loc.getWorld().getName(),
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
if (ac != null && ac.denyEnderTeleport) {
e.setCancelled(true);
}
}
// ★ 判断方块是否为农作物
private boolean isCropBlock(Material mat) {
String name = mat.name();
@@ -7496,13 +7575,6 @@ public class AreaProtection implements Listener {
if (online != null && online.isOnline()) {
online.sendMessage("§a§l[防护] §f领地 §e" + finalAreaName + " §f等待期结束,已自动删除");
}
// 通知所有在线管理员
for (Player admin : Bukkit.getOnlinePlayers()) {
if (admin.getUniqueId().equals(uid)) continue;
if (isAreaAdmin(admin)) {
admin.sendMessage("§7[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除");
}
}
}
}
}.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick
@@ -7624,7 +7696,7 @@ public class AreaProtection implements Listener {
+ " 已在全局白名单中");
return true;
}
globalPlayerWhitelist.add(name);
globalPlayerWhitelist.add(name.toLowerCase());
saveWhitelists();
sender.sendMessage("§a已全局加白: " + name);
return true;
@@ -7639,7 +7711,7 @@ public class AreaProtection implements Listener {
String areaName = parsed[0];
String playerName = parsed[1];
if (areaName.equalsIgnoreCase("global")) {
globalPlayerWhitelist.add(playerName);
globalPlayerWhitelist.add(playerName.toLowerCase());
saveWhitelists();
sender.sendMessage("§a已全局加白: "
+ playerName);
@@ -7648,7 +7720,7 @@ public class AreaProtection implements Listener {
.computeIfAbsent(areaName,
k -> ConcurrentHashMap
.newKeySet());
wl.add(playerName);
wl.add(playerName.toLowerCase());
saveWhitelists();
sender.sendMessage("§a已加白: "
+ playerName + " → " + areaName);
@@ -8577,6 +8649,8 @@ public class AreaProtection implements Listener {
int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2;
int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2;
World w = Bukkit.getWorld(ac.world);
plugin.getLogger().info("[防护-TP] ★★★ 无传送点,传送到领地中心: 区域=" + ac.name
+ " 世界=" + ac.world + " 中心=(" + cx + "," + cz + ") worldObj=" + (w != null ? "OK" : "NULL"));
if (w == null) {
sender.sendMessage("§c世界不存在: " + ac.world);
return true;
@@ -8601,8 +8675,35 @@ public class AreaProtection implements Listener {
}
} else {
World w = Bukkit.getWorld(ac.warpWorld);
plugin.getLogger().info("[防护-TP] ★★★ 有传送点: 区域=" + ac.name
+ " warpWorld=" + ac.warpWorld + " warpPos=(" + ac.warpX + "," + ac.warpY + "," + ac.warpZ + ")"
+ " worldObj=" + (w != null ? "OK" : "NULL"));
if (w == null) {
sender.sendMessage("§c传送点所在世界不存在");
// 传送点世界不存在,回退到领地中心传送
plugin.getLogger().warning("[防护-TP] 传送点世界不存在,回退到领地中心: " + ac.warpWorld);
int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2;
int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2;
World fallback = Bukkit.getWorld(ac.world);
if (fallback == null) {
sender.sendMessage("§c世界不存在: " + ac.world);
return true;
}
// 找到安全位置
Location safeLoc = null;
for (int y = fallback.getMaxHeight(); y >= fallback.getMinHeight(); y--) {
Location check = new Location(fallback, cx, y, cz);
if (!check.getBlock().getType().isSolid()) continue;
if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue;
if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue;
safeLoc = check.clone().add(0, 1, 0);
break;
}
if (safeLoc != null) {
p.teleport(safeLoc);
sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(传送点世界不存在)");
} else {
sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)");
}
return true;
}
Location warp = new Location(w, ac.warpX, ac.warpY, ac.warpZ, ac.warpYaw, ac.warpPitch);
@@ -8737,8 +8838,8 @@ public class AreaProtection implements Listener {
+ "confiscate_msg, enable_announce, announce_template, txt_content, created_at, "
+ "deny_thrown_projectiles, deny_glowing, deny_redstone_interaction, deny_door_interaction, "
+ "deny_noteblock_jukebox, deny_lead, deny_crop_harvest, deny_wool_shear, deny_animal_feeding, "
+ "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport, deny_farmland_trample, deny_ender_teleport) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ "ON CONFLICT(name) DO UPDATE SET "
+ "owner=excluded.owner, world=excluded.world, x1=excluded.x1, z1=excluded.z1, x2=excluded.x2, z2=excluded.z2, y_min=excluded.y_min, y_max=excluded.y_max, "
+ "confiscate_items=excluded.confiscate_items, deny_use_items=excluded.deny_use_items, give_effects=excluded.give_effects, clear_effects=excluded.clear_effects, clear_all_bad=excluded.clear_all_bad, "
@@ -8751,7 +8852,7 @@ public class AreaProtection implements Listener {
+ "confiscate_msg=excluded.confiscate_msg, enable_announce=excluded.enable_announce, announce_template=excluded.announce_template, txt_content=excluded.txt_content, created_at=excluded.created_at, "
+ "deny_thrown_projectiles=excluded.deny_thrown_projectiles, deny_glowing=excluded.deny_glowing, deny_redstone_interaction=excluded.deny_redstone_interaction, deny_door_interaction=excluded.deny_door_interaction, "
+ "deny_noteblock_jukebox=excluded.deny_noteblock_jukebox, deny_lead=excluded.deny_lead, deny_crop_harvest=excluded.deny_crop_harvest, deny_wool_shear=excluded.deny_wool_shear, deny_animal_feeding=excluded.deny_animal_feeding, "
+ "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport");
+ "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport, deny_farmland_trample=excluded.deny_farmland_trample, deny_ender_teleport=excluded.deny_ender_teleport");
stmt.setString(1, ac.name);
stmt.setString(2, ac.owner != null ? ac.owner : "");
@@ -8820,6 +8921,8 @@ public class AreaProtection implements Listener {
stmt.setInt(65, ac.denySignEdit ? 1 : 0);
stmt.setInt(66, ac.isPublicBuilding ? 1 : 0);
stmt.setInt(67, ac.allowVisitorTeleport ? 1 : 0);
stmt.setInt(68, ac.denyFarmlandTrample ? 1 : 0);
stmt.setInt(69, ac.denyEnderTeleport ? 1 : 0);
stmt.executeUpdate();
stmt.close();
// ★ 领地设置变更:立即触发PHP同步(防抖10秒)
@@ -10425,7 +10528,7 @@ public class AreaProtection implements Listener {
public boolean addPlayerToGlobalWhitelist(
String playerName) {
boolean ok = globalPlayerWhitelist.add(playerName);
boolean ok = globalPlayerWhitelist.add(playerName.toLowerCase());
if (ok) saveWhitelists();
return ok;
}
@@ -10433,6 +10536,7 @@ public class AreaProtection implements Listener {
public boolean removePlayerFromGlobalWhitelist(
String playerName) {
boolean ok = globalPlayerWhitelist.remove(playerName);
if (!ok) ok = globalPlayerWhitelist.remove(playerName.toLowerCase());
if (ok) saveWhitelists();
return ok;
}
@@ -10441,7 +10545,7 @@ public class AreaProtection implements Listener {
String areaName, String playerName) {
Set<String> wl =
areaPlayerWhitelist.get(areaName);
return wl != null && wl.contains(playerName);
return wl != null && wl.contains(playerName.toLowerCase());
}
public boolean addPlayerToAreaWhitelist(
@@ -10449,7 +10553,7 @@ public class AreaProtection implements Listener {
Set<String> set = areaPlayerWhitelist
.computeIfAbsent(areaName,
k -> ConcurrentHashMap.newKeySet());
boolean ok = set.add(playerName);
boolean ok = set.add(playerName.toLowerCase());
if (ok) saveWhitelists();
return ok;
}
@@ -11419,6 +11523,10 @@ public class AreaProtection implements Listener {
public boolean denyLead = false;
// ★ 农作物状态检测(播种&收获)
public boolean denyCropHarvest = false;
// ★ 耕地破坏(玩家跳跃踩踏耕地 → 变为泥土)
public boolean denyFarmlandTrample = false;
// ★ 末影人传送控制
public boolean denyEnderTeleport = false;
// ★ 采集羊毛和乐魂挽具
public boolean denyWoolShear = false;
// ★ 投喂动物
+4 -4
View File
@@ -13,7 +13,7 @@ public class BondManager {
// ===== 交易监听器 =====
public interface TransactionListener {
void onTransaction(String playerName, String type, int amount);
void onTransaction(String playerName, String type, int amount, String targetPlayer);
}
private final List<TransactionListener> transactionListeners = new ArrayList<>();
@@ -21,10 +21,10 @@ public class BondManager {
transactionListeners.add(listener);
}
private void fireTransactionEvent(String playerName, String type, int amount) {
private void fireTransactionEvent(String playerName, String type, int amount, String targetPlayer) {
for (TransactionListener listener : transactionListeners) {
try {
listener.onTransaction(playerName, type, amount);
listener.onTransaction(playerName, type, amount, targetPlayer);
} catch (Exception e) {
e.printStackTrace();
}
@@ -382,7 +382,7 @@ public class BondManager {
ps.executeUpdate();
ps.close();
// 触发交易监听器
fireTransactionEvent(player, type, amount);
fireTransactionEvent(player, type, amount, targetPlayer);
} catch (SQLException e) {
e.printStackTrace();
}
File diff suppressed because it is too large Load Diff
@@ -49,6 +49,10 @@ public class ConfigManager {
public double providerEconomyPerScore = 50.0;
public int requesterPointsReward = 10;
// ===== 密码错误封禁配置 =====
public int passwordFailThreshold = 5; // 密码错误阈值(达到后封禁)
public int passwordBanDuration = 300; // 封禁时长(秒),默认5分钟
public boolean garbageEnabled = true;
public int garbageInterval = 300;
public int garbageMaxRounds = 1;
@@ -395,6 +399,8 @@ public class ConfigManager {
{"区域防护_最大领地数", String.valueOf(areaProtectMaxLands)},
{"传送_请求有效秒", String.valueOf(tpRequestValidSeconds)},
{"传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds)},
{"密码错误封禁阈值", String.valueOf(passwordFailThreshold)},
{"密码错误封禁时长秒", String.valueOf(passwordBanDuration)},
};
for (String[] nk : newKeys) {
if (!m.containsKey(nk[0])) { hasNewKey = true; }
@@ -435,6 +441,10 @@ public class ConfigManager {
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
// 密码错误封禁配置
passwordFailThreshold = parseInt(m.getOrDefault("密码错误封禁阈值", "5"), 5);
passwordBanDuration = parseInt(m.getOrDefault("密码错误封禁时长秒", "300"), 300);
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
@@ -542,6 +552,8 @@ public class ConfigManager {
managed.put("奖励发放方式", "债券");
managed.put("传送_请求有效秒", String.valueOf(tpRequestValidSeconds));
managed.put("传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds));
managed.put("密码错误封禁阈值", String.valueOf(passwordFailThreshold));
managed.put("密码错误封禁时长秒", String.valueOf(passwordBanDuration));
// ★ 保留非ConfigManager管理的配置行(如web通信-*等)
// ★ 同时保留连续的注释块(如 "# ===== Web通信配置 =====")
@@ -632,6 +644,9 @@ public class ConfigManager {
L.add("区域防护_最大领地数=3");
L.add("传送_请求有效秒=90");
L.add("传送_发送间隔秒=10");
// 密码错误封禁
L.add("密码错误封禁阈值=5");
L.add("密码错误封禁时长秒=300");
// Web通信配置(默认禁用,需手动开启)
L.add("");
L.add("# ===== Web通信配置 =====");
+297 -10
View File
@@ -107,6 +107,9 @@ public class DatabaseManager {
// 禁言理由
safeAdd(st, "mute_reason",
"TEXT DEFAULT ''");
// ★ 正版OAuth登录IP(用于异地登录风控)
safeAdd(st, "last_oauth_ip",
"TEXT DEFAULT ''");
// ===== 背包备份表:安全迁移 =====
// 检查旧表是否存在(用 inventory_data 列判断)
@@ -225,6 +228,25 @@ public class DatabaseManager {
+ "PRIMARY KEY(player_name, region_name)"
+ ")");
// PVP竞技场会话表(每局PVP世界)
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_arena_sessions ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "session_number INTEGER NOT NULL UNIQUE,"
+ "world_name TEXT NOT NULL,"
+ "created_at INTEGER NOT NULL"
+ ")");
// PVP竞技场战绩表(按会话+玩家)
st.execute("CREATE TABLE IF NOT EXISTS "
+ "pvp_arena_stats ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "session_id INTEGER NOT NULL,"
+ "player_uuid TEXT NOT NULL,"
+ "player_name TEXT NOT NULL,"
+ "kills INTEGER DEFAULT 0,"
+ "deaths INTEGER DEFAULT 0,"
+ "UNIQUE(session_id, player_uuid)"
+ ")");
// 菜单图标表
st.execute("CREATE TABLE IF NOT EXISTS "
@@ -1477,6 +1499,177 @@ public class DatabaseManager {
return list;
}
// ==================== PVP竞技场会话 & 战绩持久化 ====================
/** 创建新PVP竞技场会话,返回生成的id */
public int createArenaSession(String worldName) {
try {
// 先查最大session_number
int maxNum = 0;
Statement st = db.createStatement();
ResultSet rs = st.executeQuery("SELECT COALESCE(MAX(session_number),0) FROM pvp_arena_sessions");
if (rs.next()) maxNum = rs.getInt(1);
rs.close();
st.close();
int newNum = maxNum + 1;
PreparedStatement ps = db.prepareStatement(
"INSERT INTO pvp_arena_sessions (session_number, world_name, created_at) VALUES (?,?,?)",
PreparedStatement.RETURN_GENERATED_KEYS);
ps.setInt(1, newNum);
ps.setString(2, worldName);
ps.setLong(3, System.currentTimeMillis());
ps.executeUpdate();
ResultSet keys = ps.getGeneratedKeys();
int id = keys.next() ? keys.getInt(1) : 1;
keys.close();
ps.close();
return id;
} catch (SQLException e) {
e.printStackTrace();
return -1;
}
}
/** 获取指定会话编号的信息(超过3天返回null) */
public Map<String, Object> getArenaSessionByNumber(int number) {
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions WHERE session_number=? AND ? - created_at <= ?");
long now = System.currentTimeMillis();
ps.setInt(1, number);
ps.setLong(2, now);
ps.setLong(3, threeDaysMs);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("session_number", rs.getInt("session_number"));
row.put("world_name", rs.getString("world_name"));
row.put("created_at", rs.getLong("created_at"));
rs.close();
ps.close();
return row;
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/** 获取所有竞技场会话列表(按编号降序) */
public List<Map<String, Object>> getAllArenaSessions() {
List<Map<String, Object>> list = new ArrayList<>();
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions ORDER BY session_number DESC");
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("session_number", rs.getInt("session_number"));
row.put("world_name", rs.getString("world_name"));
row.put("created_at", rs.getLong("created_at"));
list.add(row);
}
rs.close();
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
/** 记录竞技场击杀(INSERT OR IGNORE + UPDATE) */
public void recordArenaKill(int sessionId, String uuid, String name) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ps.setString(3, name);
ps.executeUpdate();
ps.close();
ps = db.prepareStatement(
"UPDATE pvp_arena_stats SET kills = kills + 1, player_name=? WHERE session_id=? AND player_uuid=?");
ps.setString(1, name);
ps.setInt(2, sessionId);
ps.setString(3, uuid);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/** 记录竞技场死亡 */
public void recordArenaDeath(int sessionId, String uuid, String name) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ps.setString(3, name);
ps.executeUpdate();
ps.close();
ps = db.prepareStatement(
"UPDATE pvp_arena_stats SET deaths = deaths + 1, player_name=? WHERE session_id=? AND player_uuid=?");
ps.setString(1, name);
ps.setInt(2, sessionId);
ps.setString(3, uuid);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/** 获取某玩家所有竞技场会话的总战绩 */
public Map<String, Object> getArenaTotalStats(String uuid) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kills", 0);
result.put("deaths", 0);
try {
PreparedStatement ps = db.prepareStatement(
"SELECT COALESCE(SUM(kills),0) AS total_kills, COALESCE(SUM(deaths),0) AS total_deaths FROM pvp_arena_stats WHERE player_uuid=?");
ps.setString(1, uuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("kills", rs.getInt("total_kills"));
result.put("deaths", rs.getInt("total_deaths"));
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
/** 获取某玩家在指定会话编号中的战绩 */
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("kills", 0);
result.put("deaths", 0);
try {
PreparedStatement ps = db.prepareStatement(
"SELECT kills, deaths FROM pvp_arena_stats WHERE session_id=? AND player_uuid=?");
ps.setInt(1, sessionId);
ps.setString(2, uuid);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
result.put("kills", rs.getInt("kills"));
result.put("deaths", rs.getInt("deaths"));
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// ==================== 安全报警 ====================
@@ -2349,22 +2542,116 @@ public class DatabaseManager {
return list;
}
/** 查询某坐标最近的容器记录(按时间倒序) */
public List<Map<String, Object>> getLandContainerLogAt(
String world, int x, int y, int z, int limit) {
/** 查询某坐标指定天数前的方块记录(daysAgo=0今天,1昨天,-1所有) */
public List<Map<String, Object>> getLandBlockLogAt(
String world, int x, int y, int z, int limit, int daysAgo) {
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT time, player_name, action, container_type,"
+ " detail, amount"
+ " FROM land_container_log"
+ " WHERE world=? AND x=? AND y=? AND z=?"
+ " ORDER BY time DESC LIMIT ?");
StringBuilder sql = new StringBuilder(
"SELECT time, player_name, action, material, detail"
+ " FROM land_block_log"
+ " WHERE world=? AND x=? AND y=? AND z=?");
if (daysAgo >= 0) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
long dayStart = cal.getTimeInMillis();
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
long dayEnd = cal.getTimeInMillis();
sql.append(" AND time >= ? AND time < ?");
}
sql.append(" ORDER BY time DESC LIMIT ?");
PreparedStatement ps = db.prepareStatement(sql.toString());
ps.setString(1, world);
ps.setInt(2, x);
ps.setInt(3, y);
ps.setInt(4, z);
ps.setInt(5, limit > 0 ? limit : 20);
int idx = 5;
if (daysAgo >= 0) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
long dayStart = cal.getTimeInMillis();
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
long dayEnd = cal.getTimeInMillis();
ps.setLong(idx++, dayStart);
ps.setLong(idx++, dayEnd);
}
ps.setInt(idx, limit > 0 ? limit : 20);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("time", rs.getLong("time"));
row.put("player_name", rs.getString("player_name"));
row.put("action", rs.getString("action"));
row.put("material", rs.getString("material"));
row.put("detail", rs.getString("detail"));
list.add(row);
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
/** 查询某坐标最近的容器记录(按时间倒序) */
public List<Map<String, Object>> getLandContainerLogAt(
String world, int x, int y, int z, int limit) {
return getLandContainerLogAt(world, x, y, z, limit, -1);
}
/** 查询某坐标指定天数前的容器记录(daysAgo=0今天,1昨天,-1所有) */
public List<Map<String, Object>> getLandContainerLogAt(
String world, int x, int y, int z, int limit, int daysAgo) {
List<Map<String, Object>> list = new ArrayList<>();
try {
StringBuilder sql = new StringBuilder(
"SELECT time, player_name, action, container_type,"
+ " detail, amount"
+ " FROM land_container_log"
+ " WHERE world=? AND x=? AND y=? AND z=?");
if (daysAgo >= 0) {
// 计算当天起始和结束毫秒时间戳
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
long dayStart = cal.getTimeInMillis();
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
long dayEnd = cal.getTimeInMillis();
sql.append(" AND time >= ? AND time < ?");
}
sql.append(" ORDER BY time DESC LIMIT ?");
PreparedStatement ps = db.prepareStatement(sql.toString());
ps.setString(1, world);
ps.setInt(2, x);
ps.setInt(3, y);
ps.setInt(4, z);
int idx = 5;
if (daysAgo >= 0) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
cal.set(java.util.Calendar.MINUTE, 0);
cal.set(java.util.Calendar.SECOND, 0);
cal.set(java.util.Calendar.MILLISECOND, 0);
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
long dayStart = cal.getTimeInMillis();
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
long dayEnd = cal.getTimeInMillis();
ps.setLong(idx++, dayStart);
ps.setLong(idx++, dayEnd);
}
ps.setInt(idx, limit > 0 ? limit : 20);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
@@ -29,6 +29,10 @@ public class IPGroupManager {
return can;
}
public int getMaxAccounts() {
return maxAccounts;
}
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
+102 -13
View File
@@ -16,6 +16,10 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -40,6 +44,9 @@ public class LandRecordManager implements Listener {
/** 回声碎片物品材质(1.19+ 存在) */
public static final Material ECHO_SHARD_MATERIAL = Material.ECHO_SHARD;
/** 玩家最后一次查询的坐标缓存,翻页导航时复用,避免 getTargetBlockExact 不准 */
private final Map<UUID, Location> lastQueryLocations = new ConcurrentHashMap<>();
/** 记录查询默认条数 */
private static final int QUERY_LIMIT = 20;
/** 记录保留天数 */
@@ -289,24 +296,37 @@ public class LandRecordManager implements Listener {
p.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
return;
}
queryAndPrint(p, target.getLocation());
Location loc = target.getLocation();
lastQueryLocations.put(p.getUniqueId(), loc);
queryAndPrint(p, loc);
}
private void queryAndPrint(Player p, Location loc) {
queryAndPrint(p, loc, 0);
}
private void queryAndPrint(Player p, Location loc, int page) {
String world = loc.getWorld().getName();
int x = loc.getBlockX(), y = loc.getBlockY(),
z = loc.getBlockZ();
// 按天分页:page=0今日,1昨日,2前日...
List<Map<String, Object>> blocks =
plugin.getDb().getLandBlockLogAt(
world, x, y, z, QUERY_LIMIT);
world, x, y, z, QUERY_LIMIT, page);
// 按天分页:page=0今日,1昨日,2前日...
List<Map<String, Object>> containers =
plugin.getDb().getLandContainerLogAt(
world, x, y, z, QUERY_LIMIT);
world, x, y, z, QUERY_LIMIT, page);
if (blocks.isEmpty() && containers.isEmpty()) {
p.sendMessage("§6§l[回声碎片] §f坐标 ("
+ x + "," + y + "," + z + ") 暂无领地记录");
if (page > 0) {
p.sendMessage("§6§l[回声碎片] §e坐标 ("
+ x + "," + y + "," + z + ") 第" + (page + 1) + "页无记录");
} else {
p.sendMessage("§6§l[回声碎片] §f坐标 ("
+ x + "," + y + "," + z + ") 暂无领地记录");
}
return;
}
@@ -319,19 +339,24 @@ public class LandRecordManager implements Listener {
if ("take".equals(r.get("action")) || "put".equals(r.get("action"))) {
materials.add((String) r.get("detail"));
}
// 容器类型(如 CHEST)也要翻译
String ct = (String) r.get("container_type");
if (ct != null && !ct.isEmpty()) {
materials.add(ct);
}
}
// 检查下一页是否有数据(决定是否显示"下一页"链接)
boolean hasNext = false;
if (!plugin.getDb().getLandBlockLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
if (!plugin.getDb().getLandContainerLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
final int currentPage = page;
final boolean hasNextPage = hasNext;
// 异步批量翻译
MaterialTranslator.translateBatch(materials)
.thenAccept(translations -> {
// 回到主线程发送消息
plugin.getServer().getScheduler().runTask(plugin, () -> {
printResults(p, x, y, z, blocks, containers, translations);
printResults(p, x, y, z, blocks, containers, translations, currentPage, hasNextPage);
});
});
}
@@ -340,8 +365,17 @@ public class LandRecordManager implements Listener {
List<Map<String, Object>> blocks,
List<Map<String, Object>> containers,
Map<String, String> translations) {
printResults(p, x, y, z, blocks, containers, translations, 0, false);
}
private void printResults(Player p, int x, int y, int z,
List<Map<String, Object>> blocks,
List<Map<String, Object>> containers,
Map<String, String> translations,
int page, boolean hasNext) {
String pageLabel = page == 0 ? "今日" : (page == 1 ? "昨日" : (page + 1) + "日前");
p.sendMessage("§6§l[回声碎片] §e坐标 ("
+ x + "," + y + "," + z + ") 的记录:");
+ x + "," + y + "," + z + ") 的记录§7(" + pageLabel + "):");
if (!blocks.isEmpty()) {
p.sendMessage("§b— 方块记录 —");
@@ -368,7 +402,6 @@ public class LandRecordManager implements Listener {
.append(" §f").append(r.get("player_name"))
.append(" ").append(act)
.append(" §e").append(mat).append(" §a").append(zh);
// ★ 告示牌内容显示
String detail = (String) r.get("detail");
if (detail != null && !detail.isEmpty()
&& ("sign_place".equals(action) || "sign_edit".equals(action))) {
@@ -379,7 +412,7 @@ public class LandRecordManager implements Listener {
}
if (!containers.isEmpty()) {
p.sendMessage("§b— 容器记录 —");
p.sendMessage("§b— 容器记录 §7(" + pageLabel + ")—");
for (Map<String, Object> r : containers) {
String act = (String) r.get("action");
if ("open".equals(act)) {
@@ -405,6 +438,24 @@ public class LandRecordManager implements Listener {
}
}
}
// ★ 可点击翻页导航
Component nav = Component.text(" §7[ ");
if (page > 0) {
nav = nav.append(Component.text("§e← 上一页")
.clickEvent(ClickEvent.runCommand("/landrec log " + page))
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看前一日记录"))));
nav = nav.append(Component.text(" §7| "));
}
nav = nav.append(Component.text("§f第" + (page + 1) + "页"));
if (hasNext) {
nav = nav.append(Component.text(" §7| ")
.append(Component.text("§e下一页 →")
.clickEvent(ClickEvent.runCommand("/landrec log " + (page + 2)))
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看后一日记录")))));
}
nav = nav.append(Component.text(" §7]"));
p.sendMessage(nav);
}
private String fmt(long ms) {
@@ -421,7 +472,7 @@ public class LandRecordManager implements Listener {
}
String sub = args[0];
if (sub.equalsIgnoreCase("give")) {
// 发放权限:给自己(self)任意玩家可发;发给他人需 OP 或领地管理员
// 发放权限...
boolean isOp = sender.isOp();
boolean isAreaAdmin = (sender instanceof Player)
&& plugin.areaProtection.isAreaAdmin((Player) sender);
@@ -448,6 +499,42 @@ public class LandRecordManager implements Listener {
sender.sendMessage("§a§l[回声碎片] §f已发放回声碎片给 " + target.getName());
return true;
}
if (sub.equalsIgnoreCase("log")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c§l[回声碎片] §f此命令仅限玩家使用");
return true;
}
Player p = (Player) sender;
int page = 0; // 默认今日
boolean isNav = false; // true=从翻页链接跳转,复用 lastQueryLocations
if (args.length >= 2) {
try {
page = Math.max(0, Integer.parseInt(args[1]) - 1);
isNav = true;
} catch (NumberFormatException ignored) {
sender.sendMessage("§c§l[回声碎片] §f页码格式错误");
return true;
}
}
// 翻页时复用上次查询坐标,避免 getTargetBlockExact 玩家未对准
Location navLoc = isNav ? lastQueryLocations.get(p.getUniqueId()) : null;
if (navLoc != null) {
queryAndPrint(p, navLoc, page);
return true;
}
// 首次查询或缓存过期,取准心方块
Block target = p.getTargetBlockExact(6);
if (target == null || target.getType() == Material.AIR) {
sender.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
return true;
}
Location loc = target.getLocation();
if (!isNav) {
lastQueryLocations.put(p.getUniqueId(), loc);
}
queryAndPrint(p, loc, page);
return true;
}
sendHelp(sender);
return true;
}
@@ -456,6 +543,8 @@ public class LandRecordManager implements Listener {
sender.sendMessage("§6§l[回声碎片] §e指令:");
sender.sendMessage(" §7/landrec give §f- 给自己发放一枚回声碎片");
sender.sendMessage(" §7/landrec give <玩家> §f- 向他人发放(需 OP/领地管理员)");
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处记录");
sender.sendMessage(" §7/landrec log §f- 查看当前方块今日记录");
sender.sendMessage(" §7/landrec log <页码> §f- 查看指定日期的记录(1=今日 2=昨日 ...)");
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处今日记录");
}
}
@@ -154,6 +154,16 @@ public class LoginManager {
.msg("not_registered"));
return true;
}
// ★ 风控检查:封禁期内拒绝登录
if (plugin.getRiskControl().isBanned(name)) {
int remaining = plugin.getRiskControl()
.getBanRemainingSeconds(name);
p.sendMessage("§c§l[安全风控] §f账号因密码错误过多已被封禁,"
+ "剩余 §e" + remaining + " §f秒");
return true;
}
if (args.length < 1) {
p.sendMessage("§c用法: /login <密码>");
return true;
@@ -176,6 +186,8 @@ public class LoginManager {
plugin.getDb().checkPassword(
name, hash);
if (matchMain) {
// ★ 登录成功,重置风控
plugin.getRiskControl().onLoginSuccess(name);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
@@ -225,6 +237,8 @@ public class LoginManager {
.checkPasswordWithFallback(
name, hash);
if (pwdResult != null) {
// ★ 临时密码登录成功,也重置风控
plugin.getRiskControl().onLoginSuccess(name);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
@@ -256,8 +270,19 @@ public class LoginManager {
}
// ★ 密码错误,记录风控
int remaining = plugin.getRiskControl()
.recordFailAttempt(p);
if (remaining < 0) {
// 已被封禁(triggerBan内部已踢出)
return true;
}
p.sendMessage(plugin.getConfig2()
.msg("login_failed"));
if (remaining > 0) {
p.sendMessage("§c§l[风控] §f剩余 §e"
+ remaining + " §f次尝试机会");
}
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_login] handleLogin异常: "
File diff suppressed because it is too large Load Diff
+615 -61
View File
@@ -18,9 +18,17 @@ import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.scoreboard.*;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.bukkit.entity.Egg;
import org.bukkit.entity.SmallFireball;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -73,6 +81,19 @@ public class PVPArenaManager implements Listener {
// 已完成装备选择的玩家(备份+清空+发装备已执行完毕)
private final Set<String> equipmentConfirmed = ConcurrentHashMap.newKeySet();
// ★ 永久确认玩家(选装一次即锁定,退出再进入也不能重新选装,仅调整食物/盾牌/弓弩)
private final Set<String> permanentlyConfirmed = ConcurrentHashMap.newKeySet();
// ★ 永久确认玩家的选装结果(跨次保留,供调整GUI使用)
private static class SavedSelection {
int weaponTier, armorTier, rangedWeapon, food;
boolean enchant, shield;
SavedSelection(int w, int a, int r, boolean e, boolean sh, int f) {
weaponTier=w; armorTier=a; rangedWeapon=r; enchant=e; shield=sh; food=f;
}
}
private final Map<String, SavedSelection> savedSelections = new ConcurrentHashMap<>();
// 程序内重开装备GUI(如切换档位)时,旧GUI关闭事件需忽略,避免误触发遣返
private final Set<String> guiReopening = ConcurrentHashMap.newKeySet();
@@ -110,6 +131,9 @@ public class PVPArenaManager implements Listener {
private final Map<String, Integer> pvpKills = new ConcurrentHashMap<>();
private final Map<String, Integer> pvpDeaths = new ConcurrentHashMap<>();
// ★ 当前竞技场会话ID(持久化到DB)
private int currentSessionId = -1;
// 连杀播报(移植自 PVPManager,全服播报)
private final Map<String, PVPManager.KillSession>
arenaKillSessions =
@@ -118,6 +142,18 @@ public class PVPArenaManager implements Listener {
ConcurrentHashMap.newKeySet();
private static final String ARENA_LABEL = "PVP竞技场";
// ★ PVP特殊道具常量
private static final String FIRE_CHARGE_NAME = "§c§lPVP火球";
private static final String BRIDGE_EGG_NAME = "§a§lPVP搭桥蛋";
private static final Material BRIDGE_BLOCK = Material.GLASS;
// 搭桥蛋追踪:egg实体ID -> 跟随任务的ID
private final Map<Integer, Integer> bridgeEggTrackers = new ConcurrentHashMap<>();
// 搭桥蛋已跳过的tick数(前2tick跳过,避免生成在玩家身上)
private final Map<Integer, Integer> bridgeEggSkipTicks = new ConcurrentHashMap<>();
// 搭桥蛋上一tick的位置(方块放这里,避免蛋撞自己刚放的方块)
private final Map<Integer, Location> bridgeLastLoc = new ConcurrentHashMap<>();
// 记分板条目缓存:玩家名 -> 侧边栏条目字符串(用于精确清除)
private final Map<String, String> scoreEntries = new ConcurrentHashMap<>();
@@ -332,6 +368,11 @@ public class PVPArenaManager implements Listener {
// 同步分批预生成出生点周边区块(半径2=5×5=25个,每 tick 一行,均摊不冻服)
preGenerateSpawnChunksAsync(pvpWorld, 2);
reapplyWorldRules(pvpWorld);
// ★ 创建DB会话记录(持久化战绩)
currentSessionId = db.createArenaSession(pvpWorldName);
if (currentSessionId > 0) {
plugin.getLogger().info("[PVP] 创建竞技场会话 #" + currentSessionId + " (world=" + pvpWorldName + ")");
}
plugin.getLogger().info("[PVP] PVP竞技场世界就绪 种子=" + seed + " 耗时=" + dt + "ms(NORMAL真实自然地形, 出生点已固化)");
return pvpWorld;
@@ -389,20 +430,17 @@ public class PVPArenaManager implements Listener {
// 用数组持有任务引用,供 lambda 内部自取消(Bukkit 无 getCurrentTask)
final BukkitTask[] taskHolder = new BukkitTask[1];
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() {
@Override
public void run() {
int r = row.getAndIncrement();
if (r > radius) {
taskHolder[0].cancel();
return;
}
for (int dz = -radius; dz <= radius; dz++) {
try {
world.loadChunk(cx + r, cz + dz, true);
} catch (Exception e) {
// 单个 chunk 失败不阻断整体
}
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, (Runnable) () -> {
int r = row.getAndIncrement();
if (r > radius) {
taskHolder[0].cancel();
return;
}
for (int dz = -radius; dz <= radius; dz++) {
try {
world.loadChunk(cx + r, cz + dz, true);
} catch (Exception e) {
// 单个 chunk 失败不阻断整体
}
}
}, 1L, 1L); // 延迟 1tick 启动(给世界加载留缓冲),每 1tick 一行
@@ -497,6 +535,8 @@ public class PVPArenaManager implements Listener {
private void reapplyWorldRules(World w) {
w.setPVP(true);
// ★ 和平模式:排除敌对生物干扰PVP
w.setDifficulty(Difficulty.PEACEFUL);
w.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
w.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
w.setTime(6000);
@@ -739,7 +779,7 @@ public class PVPArenaManager implements Listener {
// 兜底:确保世界内无玩家(理论上冷却结束时已无人)
for (Player p : new ArrayList<>(world.getPlayers())) {
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
p.teleport(getBedOrSpawnLocation(p));
p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界");
}
Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形)
@@ -917,8 +957,7 @@ public class PVPArenaManager implements Listener {
// 延迟一帧传回主世界(避免与关闭事件冲突)
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline()) {
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
player.sendMessage("§c§l[PVP] §c你未确认装备选择,已被遣返回主世界");
player.sendMessage("§7提示:进入PVP后必须选择并确认装备才能开始战斗");
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
@@ -982,6 +1021,17 @@ public class PVPArenaManager implements Listener {
equipmentConfirmed.add(playerName);
cancelKickTimeout(playerName);
// ★ 永久锁定:记录确认状态和选装结果,退出再进入也不能重新选装
permanentlyConfirmed.add(playerName);
savedSelections.put(playerName, new SavedSelection(
selWeaponTier.getOrDefault(playerName, 0),
selArmorTier.getOrDefault(playerName, 0),
selRangedWeapon.getOrDefault(playerName, 0),
selEnchant.getOrDefault(playerName, false),
selShield.getOrDefault(playerName, true),
selFood.getOrDefault(playerName, FOOD_DEFAULT)
));
player.sendMessage("§a§l装备已就绪,开始战斗!");
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f);
plugin.getLogger().info("[PVP] 玩家 " + playerName + " 装备确认完毕");
@@ -1028,8 +1078,7 @@ public class PVPArenaManager implements Listener {
equipInteracted.remove(playerName);
if (p != null && p.isOnline()) {
p.closeInventory();
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
p.teleport(getBedOrSpawnLocation(p));
p.sendMessage("§c§l[PVP] §c装备选择超时(" + EQUIPMENT_SELECT_TIMEOUT_SECONDS + "秒),已被自动遣返");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
}
@@ -1128,6 +1177,89 @@ public class PVPArenaManager implements Listener {
// ==================== 事件处理器 ====================
/**
* PVP火球 — 右键火焰弹发射SmallFireball,造成伤害
*/
@EventHandler
public void onFireBallUse(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Player player = event.getPlayer();
if (!inPVPArena.contains(player.getName()) || !equipmentConfirmed.contains(player.getName())) return;
ItemStack item = player.getInventory().getItemInMainHand();
if (item.getType() != Material.FIRE_CHARGE) return;
event.setCancelled(true);
// 消耗1个火焰弹
if (item.getAmount() > 1) {
item.setAmount(item.getAmount() - 1);
} else {
player.getInventory().setItemInMainHand(null);
}
// 发射火焰弹(落点爆炸 + 点燃方块)
Vector dir = player.getLocation().getDirection().normalize().multiply(2.0);
SmallFireball fb = player.launchProjectile(SmallFireball.class, dir);
fb.setYield(1.5f); // 爆炸威力约1.5格半径
fb.setIsIncendiary(true); // 点燃爆炸范围内方块
player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_SHOOT, 0.5f, 1.2f);
}
/**
* 搭桥蛋 — 右键扔出蛋后,每隔1tick在蛋当前位置放置方块,形成桥梁
*/
@EventHandler
public void onBridgeEggLaunch(ProjectileLaunchEvent event) {
if (!(event.getEntity() instanceof Egg egg)) return;
if (!(egg.getShooter() instanceof Player player)) return;
if (!inPVPArena.contains(player.getName())) return;
int eid = egg.getEntityId();
int taskId = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (egg.isDead() || !egg.isValid()) {
Integer tid = bridgeEggTrackers.remove(eid);
if (tid != null) Bukkit.getScheduler().cancelTask(tid);
bridgeEggSkipTicks.remove(eid);
bridgeLastLoc.remove(eid);
return;
}
int skip = bridgeEggSkipTicks.merge(eid, 1, Integer::sum);
// 前2tick只记录位置不放置(蛋在玩家体内/附近),保证 skip=3 起桥位已就绪
if (skip <= 2) {
bridgeLastLoc.put(eid, egg.getLocation().clone());
return;
}
// 方块放在蛋的上一tick位置(蛋已离开,不会撞自己放的桥)
Location prevLoc = bridgeLastLoc.get(eid);
if (prevLoc != null) {
Block b = prevLoc.getBlock();
if (b.getType().isAir()) {
b.setType(BRIDGE_BLOCK);
}
}
// 记录当前位置供下一tick使用
bridgeLastLoc.put(eid, egg.getLocation().clone());
}, 1L, 1L).getTaskId();
bridgeEggTrackers.put(eid, taskId);
}
/**
* 搭桥蛋命中 — 停止方块追踪
*/
@EventHandler
public void onBridgeEggHit(ProjectileHitEvent event) {
if (!(event.getEntity() instanceof Egg)) return;
int eid = event.getEntity().getEntityId();
Integer taskId = bridgeEggTrackers.remove(eid);
if (taskId != null) {
Bukkit.getScheduler().cancelTask(taskId);
}
bridgeEggSkipTicks.remove(eid);
bridgeLastLoc.remove(eid);
}
/**
* ★ 选装前无敌:玩家在PVP世界中未完成装备选择时,免疫一切伤害
*/
@@ -1157,41 +1289,89 @@ public class PVPArenaManager implements Listener {
// 总榜击杀数
pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1);
// ★ 击杀奖励:PVP世界每完成2次杀敌,发放16个牛排用于补血(背包满则只发能放下的)
// ★ 持久化到DB
if (currentSessionId > 0) {
db.recordArenaKill(currentSessionId, ((Player) killer).getUniqueId().toString(), kName);
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), vName);
}
// ★ 击杀奖励:5杀一个轮回
// 2杀:16熟食补血 + 1火焰弹
// 5杀:16箭 + 1搭桥蛋 + 1火焰弹 + 替换全身装备(头盔胸甲裤子靴子)
int newKills = pvpKills.get(kName);
if (newKills % 2 == 0) {
int cyclePos = newKills % 5;
if (cyclePos == 2 && newKills > 0) {
// ▼ 2杀奖励 ▼
Player killerP = (Player) killer;
Map<Integer, ItemStack> leftover = killerP.getInventory().addItem(new ItemStack(Material.COOKED_BEEF, 16));
int given = 16;
for (ItemStack rem : leftover.values()) given -= rem.getAmount();
if (given > 0) {
killerP.sendMessage("§a§l[PVP] §f完成 2 次杀敌!获得 §6" + given + " 个牛排 §7(补血)");
// 16熟食
ItemStack food = new ItemStack(Material.COOKED_BEEF, 16);
ItemMeta foodMeta = food.getItemMeta();
if (foodMeta != null) {
foodMeta.setDisplayName("§c§lPVP熟食补血");
food.setItemMeta(foodMeta);
}
Map<Integer, ItemStack> leftover = killerP.getInventory().addItem(food);
int givenFood = 16;
for (ItemStack rem : leftover.values()) givenFood -= rem.getAmount();
// 1火焰弹
ItemStack fc = new ItemStack(Material.FIRE_CHARGE, 1);
ItemMeta fcMeta = fc.getItemMeta();
if (fcMeta != null) {
fcMeta.setDisplayName(FIRE_CHARGE_NAME);
fc.setItemMeta(fcMeta);
}
killerP.getInventory().addItem(fc);
if (givenFood > 0) {
killerP.sendMessage("§a§l[PVP] §f击杀达 §e2 §f次!获得 §6" + givenFood + " 个熟食§7(补血) §c+ 火焰弹§7×1");
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f);
}
}
// ★ 击杀≥10奖励:替换完整铁甲 + 喷溅瞬间治疗2 ×1(一次性)
if (newKills >= 10) {
if (cyclePos == 0 && newKills > 0) {
// ▼ 5杀奖励 ▼
Player killerP = (Player) killer;
if (!killerP.hasPermission("sdf1.kit.pvp.reward10")) {
// 替换完整铁甲
killerP.getInventory().setHelmet(new ItemStack(Material.IRON_HELMET));
killerP.getInventory().setChestplate(new ItemStack(Material.IRON_CHESTPLATE));
killerP.getInventory().setLeggings(new ItemStack(Material.IRON_LEGGINGS));
killerP.getInventory().setBoots(new ItemStack(Material.IRON_BOOTS));
// 喷溅瞬间治疗2 ×1(正确NBT)
ItemStack potion = new ItemStack(Material.SPLASH_POTION, 1);
PotionMeta potionMeta = (PotionMeta) potion.getItemMeta();
potionMeta.setBasePotionType(org.bukkit.potion.PotionType.HEALING);
potionMeta.addCustomEffect(new org.bukkit.potion.PotionEffect(
org.bukkit.potion.PotionEffectType.INSTANT_HEALTH, 1, 1, false, false), true);
potion.setItemMeta(potionMeta);
killerP.getInventory().addItem(potion);
// 授予临时权限标记(本次重启内有效,防止重复发放)
killerP.addAttachment(plugin, "sdf1.kit.pvp.reward10", true);
killerP.sendMessage("§a§l[PVP] §f击杀达 §610 §f次!获得 §b铁甲套装 §f+ §c喷溅瞬间治疗II ×1");
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
// 16箭
ItemStack arrows = new ItemStack(Material.ARROW, 16);
ItemMeta aMeta = arrows.getItemMeta();
if (aMeta != null) {
aMeta.setDisplayName("§b§lPVP箭矢");
arrows.setItemMeta(aMeta);
}
killerP.getInventory().addItem(arrows);
// 1搭桥蛋
ItemStack bridgeEgg = new ItemStack(Material.EGG, 1);
ItemMeta eMeta = bridgeEgg.getItemMeta();
if (eMeta != null) {
eMeta.setDisplayName(BRIDGE_EGG_NAME);
eMeta.setLore(Arrays.asList("§7右键发射,沿抛物线生成放置方块"));
bridgeEgg.setItemMeta(eMeta);
}
killerP.getInventory().addItem(bridgeEgg);
// 1火焰弹
ItemStack fc5 = new ItemStack(Material.FIRE_CHARGE, 1);
ItemMeta fc5Meta = fc5.getItemMeta();
if (fc5Meta != null) {
fc5Meta.setDisplayName(FIRE_CHARGE_NAME);
fc5.setItemMeta(fc5Meta);
}
killerP.getInventory().addItem(fc5);
// 替换全身装备(用玩家当前选中的护甲档位,无选中默认铁)
int tier = selArmorTier.getOrDefault(killerP.getName(), 0);
Material[] armorSet = ARMOR_SETS[tier];
killerP.getInventory().setHelmet(new ItemStack(armorSet[0]));
killerP.getInventory().setChestplate(new ItemStack(armorSet[1]));
killerP.getInventory().setLeggings(new ItemStack(armorSet[2]));
killerP.getInventory().setBoots(new ItemStack(armorSet[3]));
killerP.sendMessage("§a§l[PVP] §f击杀达 §e5 §f次!获得 §b箭矢×16 §a+ 搭桥蛋 §c+ 火焰弹 §f+ 全新" + ARMOR_TIER_NAMES[tier] + "护甲");
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
}
// 击杀者连杀会话
@@ -1239,6 +1419,10 @@ public class PVPArenaManager implements Listener {
((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName));
}
pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1);
// ★ 非击杀死亡也持久化(如摔死/溺水)
if (currentSessionId > 0) {
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), player.getName());
}
refreshPVPScoreboard();
// 死亡时保留经验(避免丢失)
@@ -1255,8 +1439,13 @@ public class PVPArenaManager implements Listener {
Player player = event.getPlayer();
if (!inPVPArena.contains(player.getName())) return;
World main = Bukkit.getWorlds().get(0);
event.setRespawnLocation(main.getSpawnLocation());
// ★ 优先床/重生点,兜底主世界出生点
Location bed = player.getRespawnLocation();
if (bed != null && !bed.getWorld().getName().equals(pvpWorldName)) {
event.setRespawnLocation(bed);
} else {
event.setRespawnLocation(Bukkit.getWorlds().get(0).getSpawnLocation());
}
if (!equipmentConfirmed.contains(player.getName())) {
// 未确认装备:身上仍是原背包,只需清状态
@@ -1346,8 +1535,7 @@ public class PVPArenaManager implements Listener {
// 若仍在竞技场世界,送回主世界
if (player.getWorld().getName().equals(pvpWorldName)) {
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
cleanupPlayerStats(player);
inPVPArena.remove(player.getName());
player.sendMessage("§e[PVP] 已送你回主世界");
@@ -1360,8 +1548,7 @@ public class PVPArenaManager implements Listener {
if (player.getWorld().getName().equals(pvpWorldName)) {
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
if (!player.isOnline()) return;
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
player.teleport(getBedOrSpawnLocation(player));
cleanupPlayerStats(player);
inPVPArena.remove(player.getName());
player.sendMessage("§e[PVP] 检测到你在竞技场非正常断线,已送你回主世界");
@@ -1531,12 +1718,40 @@ public class PVPArenaManager implements Listener {
* 打开装备选择GUI
*/
public void openEquipmentSelection(Player player) {
// ★ 记录GUI打开时间(宽限期机制:此后短时间内内的关闭事件不触发遣返)
guiOpenedMillis.put(player.getName(), System.currentTimeMillis());
graceReopenCount.put(player.getName(), 0);
String name = player.getName();
// ★ 永久确认:选装一次即锁定,退出再进入也不能重新选装
if (permanentlyConfirmed.contains(name)) {
// 恢复之前保存的选装状态到临时map(供 equipFullSet 和 openAdjustmentGUI 读取)
SavedSelection saved = savedSelections.get(name);
if (saved != null) {
selWeaponTier.put(name, saved.weaponTier);
selArmorTier.put(name, saved.armorTier);
selRangedWeapon.put(name, saved.rangedWeapon);
selEnchant.put(name, saved.enchant);
selShield.put(name, saved.shield);
selFood.put(name, saved.food);
}
// ★ 关键:背包已在 onPlayerEnterPVPWorld 中清空,必须重新发放锁定装备
equipFullSet(player);
// 标记本次进入已确认(超时遣返不触发)
equipmentConfirmed.add(name);
cancelKickTimeout(name);
openAdjustmentGUI(player);
player.sendMessage("§a§l装备已就绪(沿用上次选择)");
return;
}
// ★ 已确认装备的玩家(同一次进入内)→ 打开调整GUI(仅食物/盾牌/弓弩,其他锁定)
if (equipmentConfirmed.contains(name)) {
openAdjustmentGUI(player);
return;
}
// ★ 记录GUI打开时间(宽限期机制:此后短时间内内的关闭事件不触发遣返)
guiOpenedMillis.put(name, System.currentTimeMillis());
graceReopenCount.put(name, 0);
// ★ 公平锁定:本局首位进入PVP世界的玩家定死"武器材质/护甲材质/附魔"三档,后续加入者不可更改
boolean isFirst = (lockedWeaponOwner == null);
if (isFirst) {
@@ -1692,6 +1907,146 @@ public class PVPArenaManager implements Listener {
player.openInventory(gui);
}
/**
* 已确认装备玩家的调整GUI:武器/护甲/附魔只读锁定,仅可调食物/盾牌/弓弩
*/
private void openAdjustmentGUI(Player player) {
String name = player.getName();
int myWeapon = selWeaponTier.getOrDefault(name, 0);
int myArmor = selArmorTier.getOrDefault(name, 0);
boolean myEnchant = selEnchant.getOrDefault(name, false);
boolean myShield = selShield.getOrDefault(name, true);
int myFood = selFood.getOrDefault(name, FOOD_DEFAULT);
int myRanged = selRangedWeapon.getOrDefault(name, 0);
Inventory gui = Bukkit.createInventory(null, 54, EQUIPMENT_GUI_TITLE);
// 顶部预览(0-7):锁定状态的装备显示
gui.setItem(0, previewItem(SWORD_TIERS[myWeapon], "§7§l" + WEAPON_TIER_NAMES[myWeapon] + "§l剑 §c🔒", "§7已锁定·不可更改"));
gui.setItem(1, previewItem(AXE_TIERS[myWeapon], "§7§l" + WEAPON_TIER_NAMES[myWeapon] + "§l斧 §c🔒", "§7已锁定·不可更改"));
gui.setItem(2, previewItem(ARMOR_SETS[myArmor][0], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l盔 §c🔒", "§7已锁定·不可更改"));
gui.setItem(3, previewItem(ARMOR_SETS[myArmor][1], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l胸甲 §c🔒", "§7已锁定·不可更改"));
gui.setItem(4, previewItem(ARMOR_SETS[myArmor][2], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l腿甲 §c🔒", "§7已锁定·不可更改"));
gui.setItem(5, previewItem(ARMOR_SETS[myArmor][3], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l靴 §c🔒", "§7已锁定·不可更改"));
if (myShield) gui.setItem(6, previewItem(Material.SHIELD, "§a§l盾牌 ✓", "§7已选·点击可停用"));
else gui.setItem(6, grayPreview("§7盾牌 ✗", "§7未选·点击可启用"));
gui.setItem(7, previewItem(Material.COOKED_BEEF, "§c§l熟牛肉 x" + myFood, "§7可调整数量"));
// 信息面板(8)
ItemStack info = new ItemStack(Material.PAPER);
ItemMeta infoM = info.getItemMeta();
if (infoM != null) {
infoM.setDisplayName("§6§lPVP装备调整");
List<String> lore = new ArrayList<>();
lore.add("§7装备已锁定,仅可调整以下选项:");
lore.add("");
lore.add("§e⚡ 当前装备状态");
lore.add("§7 主武器: " + WEAPON_TIER_NAMES[myWeapon] + "§7剑 + " + WEAPON_TIER_NAMES[myWeapon] + "§7斧");
lore.add("§7 护甲: " + ARMOR_TIER_NAMES[myArmor] + "§7全套4件");
lore.add("§7 附魔: " + (myEnchant ? "§a已开启" : "§c未开启"));
lore.add("");
lore.add("§a↕ 可调整项:");
lore.add("§7 - 食物补给数量");
lore.add("§7 - 盾牌启用/停用");
lore.add("§7 - 弓/弩切换");
infoM.setLore(lore);
info.setItemMeta(infoM);
}
gui.setItem(8, info);
// 分隔线(36-44,跳过40)
for (int i = 36; i < 45; i++) {
if (i == 40) continue;
gui.setItem(i, new ItemStack(Material.GRAY_STAINED_GLASS_PANE));
}
// 分组标签:武器/护甲标为已锁定
gui.setItem(9, labelItem("§7§l主武器材质 §c(已锁定)", "§7首次选装后不可更改"));
gui.setItem(18, labelItem("§7§l护甲材质 §c(已锁定)", "§7首次选装后不可更改"));
gui.setItem(27, labelItem("§6§l可调整选项", "§7点击直接切换/选择"));
// 武器材质(10-13):全部只读灰显
for (int t = 0; t < 4; t++) {
ItemStack btn;
if (t == myWeapon) {
btn = new ItemStack(SWORD_TIERS[t]);
ItemMeta bm = btn.getItemMeta();
if (bm != null) {
bm.setDisplayName("§7" + WEAPON_TIER_NAMES[t] + "剑 §a✔");
bm.setLore(Arrays.asList("§7已锁定"));
btn.setItemMeta(bm);
}
} else {
btn = grayPreview("§7" + WEAPON_TIER_NAMES[t] + "剑", "§7已锁定");
}
gui.setItem(10 + t, btn);
}
// 护甲材质(19-22):全部只读灰显
for (int t = 0; t < 4; t++) {
ItemStack btn;
if (t == myArmor) {
btn = new ItemStack(ARMOR_SETS[t][0]);
ItemMeta bm = btn.getItemMeta();
if (bm != null) {
bm.setDisplayName("§7" + ARMOR_TIER_NAMES[t] + "盔 §a✔");
bm.setLore(Arrays.asList("§7已锁定"));
btn.setItemMeta(bm);
}
} else {
btn = grayPreview("§7" + ARMOR_TIER_NAMES[t] + "盔", "§7已锁定");
}
gui.setItem(19 + t, btn);
}
// 附魔(28):只读灰显
ItemStack enchBtn = grayPreview(
myEnchant ? "§7附魔: 开 §c(已锁定)" : "§7附魔: 关 §c(已锁定)",
"§7首次选装后不可更改");
gui.setItem(28, enchBtn);
// 盾牌开关(30)— 可调整
ItemStack shBtn = new ItemStack(myShield ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE);
ItemMeta sm = shBtn.getItemMeta();
if (sm != null) {
sm.setDisplayName(myShield ? "§a§l盾牌: 装备" : "§c§l盾牌: 不装备");
sm.setLore(Arrays.asList("§7副手装备", myShield ? "§a点击取消" : "§a点击装备"));
shBtn.setItemMeta(sm);
}
gui.setItem(30, shBtn);
// 远程武器切换(29)— 可调整
ItemStack rwBtn = new ItemStack(myRanged == 1 ? Material.CROSSBOW : Material.BOW);
ItemMeta rwm = rwBtn.getItemMeta();
if (rwm != null) {
rwm.setDisplayName(myRanged == 1 ? "§b§l远程武器: 弩" : "§a§l远程武器: 弓");
rwm.setLore(Arrays.asList("§7点击在 弓 / 弩 之间切换",
"§e当前: " + (myRanged == 1 ? "弩" : "弓"),
"§7调整后立即生效"));
rwBtn.setItemMeta(rwm);
}
gui.setItem(29, rwBtn);
// 熟牛肉快捷数量(32-35 = 8/16/32/64)
int[] foodOpts = {8, 16, 32, 64};
for (int i = 0; i < 4; i++) {
gui.setItem(32 + i, makeFoodButton(foodOpts[i], myFood));
}
// 关闭按钮(49)
ItemStack close = new ItemStack(Material.BARRIER);
ItemMeta cm = close.getItemMeta();
if (cm != null) {
cm.setDisplayName("§c§l关闭");
cm.setLore(Arrays.asList("§7关闭装备调整界面"));
close.setItemMeta(cm);
}
gui.setItem(49, close);
player.openInventory(gui);
}
private ItemStack previewItem(Material mat, String name, String desc) {
ItemStack it = new ItemStack(mat);
ItemMeta m = it.getItemMeta();
@@ -1762,6 +2117,20 @@ public class PVPArenaManager implements Listener {
// ★ 旧5档护甲系统已废弃(2026-07-09 改版为铁剑+铁斧+可选盾牌+可选附魔+可调熟牛肉)
// 原 buildTierEquipment(EquipmentTier) 与 makePotion 整体移除,装备由 equipFullSet 按新模型直接构建。
/**
* 创建带PVP标记的箭矢堆
*/
private ItemStack makeArrowStack(int amount) {
ItemStack arrows = new ItemStack(Material.ARROW, amount);
ItemMeta m = arrows.getItemMeta();
if (m != null) {
m.setDisplayName("§b§lPVP箭矢");
m.setLore(Arrays.asList("§7远程武器弹药", PVP_ITEM_MARKER));
arrows.setItemMeta(m);
}
return arrows;
}
private ItemStack makeWeapon(Material mat, String name, boolean enchant) {
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
@@ -1798,6 +2167,75 @@ public class PVPArenaManager implements Listener {
String name = player.getName();
// ★ 已确认装备 → 调整模式:仅允许调整食物/盾牌/弓弩,其余全部锁定
boolean confirmed = equipmentConfirmed.contains(name);
if (confirmed) {
// 武器材质(10-13)— 锁定
if (slot >= 10 && slot <= 13) {
player.sendMessage("§e[PVP] §7装备已锁定,不可更改武器材质");
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
return true;
}
// 护甲材质(19-22)— 锁定
if (slot >= 19 && slot <= 22) {
player.sendMessage("§e[PVP] §7装备已锁定,不可更改护甲材质");
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
return true;
}
// 附魔(28)— 锁定
if (slot == 28) {
player.sendMessage("§e[PVP] §7附魔已锁定,不可更改");
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
return true;
}
// 盾牌(30)— 可调整
if (slot == 30) {
boolean cur = selShield.getOrDefault(name, true);
selShield.put(name, !cur);
reEquipAdjustments(player);
guiReopening.add(name);
openAdjustmentGUI(player);
player.sendMessage("§a[PVP] 盾牌已" + (!cur ? "§a装备" : "§c取消"));
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.7f, 1.2f);
return true;
}
// 远程武器(29)— 可调整
if (slot == 29) {
int cur = selRangedWeapon.getOrDefault(name, 0);
selRangedWeapon.put(name, cur == 0 ? 1 : 0);
reEquipAdjustments(player);
guiReopening.add(name);
openAdjustmentGUI(player);
player.sendMessage("§a[PVP] 远程武器已切换为 " + (cur == 0 ? "§b弩" : "§b弓"));
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.7f, 1.2f);
return true;
}
// 食物数量(32-35)— 可调整
if (slot >= 32 && slot <= 35) {
int[] foodOpts = {8, 16, 32, 64};
int food = foodOpts[slot - 32];
selFood.put(name, food);
reEquipAdjustments(player);
guiReopening.add(name);
openAdjustmentGUI(player);
player.sendMessage("§a[PVP] 熟牛肉数量已设为 " + food);
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
return true;
}
// 关闭(49)
if (slot == 49) {
player.closeInventory();
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
return true;
}
// 预览区/标签(0-8, 9, 18, 27):仅音效
if ((slot >= 0 && slot <= 8) || slot == 9 || slot == 18 || slot == 27) {
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
return true;
}
return false;
}
boolean weaponLocked = (lockedWeaponOwner != null && !lockedWeaponOwner.equals(name));
boolean armorLocked = (lockedArmorOwner != null && !lockedArmorOwner.equals(name));
boolean enchantLocked = (lockedEnchantOwner != null && !lockedEnchantOwner.equals(name));
@@ -1976,7 +2414,7 @@ public class PVPArenaManager implements Listener {
crossbow.setItemMeta(cm);
}
player.getInventory().addItem(crossbow);
player.getInventory().addItem(new ItemStack(Material.ARROW, 64));
player.getInventory().addItem(makeArrowStack(64));
} else {
ItemStack bow = new ItemStack(Material.BOW);
ItemMeta bm = bow.getItemMeta();
@@ -1986,7 +2424,7 @@ public class PVPArenaManager implements Listener {
bow.setItemMeta(bm);
}
player.getInventory().addItem(bow);
player.getInventory().addItem(new ItemStack(Material.ARROW, 64));
player.getInventory().addItem(makeArrowStack(64));
}
// 补血普通食物:熟牛肉(数量可调)
@@ -2007,6 +2445,94 @@ public class PVPArenaManager implements Listener {
+ " 远程:" + (rw == 1 ? "弩" : "弓") + " 熟牛肉x" + food);
}
/**
* 移除可调整类的PVP物品(食物/弓/弩/箭/盾牌),保留武器+护甲不动
*/
private void removePVPAdjustables(Player player) {
// 副手盾牌
ItemStack off = player.getInventory().getItemInOffHand();
if (off != null && off.getType() == Material.SHIELD) {
player.getInventory().setItemInOffHand(null);
}
// 背包中带PVP标记的食物/弓/弩/箭
for (int i = 0; i < player.getInventory().getSize(); i++) {
ItemStack item = player.getInventory().getItem(i);
if (item == null || !item.hasItemMeta()) continue;
ItemMeta m = item.getItemMeta();
if (m == null || !m.hasLore()) continue;
List<String> lore = m.getLore();
if (lore == null || !lore.contains(PVP_ITEM_MARKER)) continue;
Material t = item.getType();
if (t == Material.COOKED_BEEF || t == Material.BOW || t == Material.CROSSBOW || t == Material.ARROW) {
player.getInventory().setItem(i, null);
}
}
}
/**
* 为已确认装备的玩家重新应用食物/盾牌/远程武器调整
* (武器材质/护甲材质/附魔已锁定,不做变动)
*/
private void reEquipAdjustments(Player player) {
String name = player.getName();
if (!equipmentConfirmed.contains(name)) return;
removePVPAdjustables(player);
boolean shield = selShield.getOrDefault(name, true);
int food = selFood.getOrDefault(name, FOOD_DEFAULT);
int rw = selRangedWeapon.getOrDefault(name, 0);
// 盾牌(副手)
if (shield) {
ItemStack sh = new ItemStack(Material.SHIELD);
ItemMeta sm = sh.getItemMeta();
if (sm != null) {
sm.setDisplayName("§a§lPVP盾牌");
sm.setLore(Arrays.asList("§7格挡近战攻击", PVP_ITEM_MARKER));
sh.setItemMeta(sm);
}
player.getInventory().setItemInOffHand(sh);
}
// 远程武器 + 箭矢
if (rw == 1) {
ItemStack crossbow = new ItemStack(Material.CROSSBOW);
ItemMeta cm = crossbow.getItemMeta();
if (cm != null) {
cm.setDisplayName("§b§lPVP弩");
cm.setLore(Arrays.asList("§7远程武器(默认装备)", PVP_ITEM_MARKER));
crossbow.setItemMeta(cm);
}
player.getInventory().addItem(crossbow);
} else {
ItemStack bow = new ItemStack(Material.BOW);
ItemMeta bm = bow.getItemMeta();
if (bm != null) {
bm.setDisplayName("§b§lPVP弓");
bm.setLore(Arrays.asList("§7远程武器(默认装备)", PVP_ITEM_MARKER));
bow.setItemMeta(bm);
}
player.getInventory().addItem(bow);
}
player.getInventory().addItem(makeArrowStack(64));
// 食物
if (food > 0) {
ItemStack beef = new ItemStack(Material.COOKED_BEEF, food);
ItemMeta bm = beef.getItemMeta();
if (bm != null) {
bm.setDisplayName("§c§lPVP熟牛肉");
bm.setLore(Arrays.asList("§7补血普通食物 x" + food, PVP_ITEM_MARKER));
beef.setItemMeta(bm);
}
player.getInventory().addItem(beef);
}
player.sendMessage("§a§l[PVP] §7装备调整已应用");
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.6f, 1.2f);
}
// ==================== 命令入口 ====================
/**
@@ -2118,9 +2644,8 @@ public class PVPArenaManager implements Listener {
player.sendMessage("§e你不在PVP竞技场中");
return;
}
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena)
World main = Bukkit.getWorlds().get(0);
player.teleport(main.getSpawnLocation());
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena),优先床
player.teleport(getBedOrSpawnLocation(player));
player.sendMessage("§a§l正在离开PVP竞技场...");
}
@@ -2153,6 +2678,33 @@ public class PVPArenaManager implements Listener {
saveConfig();
}
/** 获取当前会话ID */
public int getCurrentSessionId() {
return currentSessionId;
}
/** 获取某玩家所有会话的总战绩(从DB) */
public Map<String, Object> getTotalArenaStats(String uuid) {
return db.getArenaTotalStats(uuid);
}
/** 获取指定会话编号的信息 */
public Map<String, Object> getArenaSessionByNumber(int number) {
return db.getArenaSessionByNumber(number);
}
/** 获取某玩家在指定会话中的战绩 */
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
return db.getArenaSessionStats(uuid, sessionId);
}
/** 优先传送玩家到床/重生点,兜底世界出生点 */
public Location getBedOrSpawnLocation(Player player) {
Location bed = player.getRespawnLocation();
if (bed != null) return bed;
return Bukkit.getWorlds().get(0).getSpawnLocation();
}
/** 重置本局公平锁定状态(世界销毁/全量重置时调用,下一局首位玩家重新定死装备) */
private void resetMatchLock() {
lockedEnchantOwner = null;
@@ -2168,6 +2720,8 @@ public class PVPArenaManager implements Listener {
selRangedWeapon.clear();
selArmorTier.clear();
equipInteracted.clear();
permanentlyConfirmed.clear();
savedSelections.clear();
}
public void clearAllPVPArenaStates() {
+57 -2
View File
@@ -1076,8 +1076,15 @@ public class PVPManager implements Listener {
public boolean onCommand(Player p,
String[] args) {
if (args.length == 0) {
return showStats(p,
p.getName());
showStats(p, p.getName());
// ★ 同时展示竞技场总战绩
showArenaTotalStats(p, p.getName());
return true;
}
// ★ 数字参数 → 查询指定会话编号的竞技场战绩
if (args.length == 1 && args[0].matches("\\d+")) {
showArenaSessionStats(p, p.getName(), Integer.parseInt(args[0]));
return true;
}
switch (args[0].toLowerCase()) {
case "create":
@@ -1118,6 +1125,14 @@ public class PVPManager implements Listener {
case "arena":
plugin.getPVPArenaManager().joinArena(p);
return true;
case "equip":
case "adjust":
if (!plugin.getPVPArenaManager().isInPVPArena(p.getName())) {
p.sendMessage("§c你不在PVP竞技场中");
return true;
}
plugin.getPVPArenaManager().openEquipmentSelection(p);
return true;
case "leave":
case "exit":
plugin.leavePVP(p);
@@ -1126,6 +1141,7 @@ public class PVPManager implements Listener {
showStats(p, p.getName());
p.sendMessage("§7——— PVP指令 ———");
p.sendMessage("§e/pvp join §7进入PVP竞技场");
p.sendMessage("§e/pvp equip §7调整装备(食物/盾牌/弓弩)");
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
p.sendMessage("§e/pvp list §7查看PVP区域列表");
@@ -1426,4 +1442,43 @@ public class PVPManager implements Listener {
}
return true;
}
// ★ 展示PVP竞技场总战绩
private void showArenaTotalStats(Player p, String target) {
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
String uuid = off.getUniqueId().toString();
Map<String, Object> stats = plugin.getPVPArenaManager().getTotalArenaStats(uuid);
int kills = (int) stats.get("kills");
int deaths = (int) stats.get("deaths");
if (kills == 0 && deaths == 0) return;
double kd = deaths > 0 ? (double) kills / deaths : kills;
p.sendMessage("§6§l[PVP竞技场] " + target);
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", kd));
}
// ★ 展示指定会话编号的竞技场战绩
private void showArenaSessionStats(Player p, String target, int sessionNumber) {
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
String uuid = off.getUniqueId().toString();
// 先查会话信息(含3天有效期检查)
Map<String, Object> session = plugin.getPVPArenaManager().getArenaSessionByNumber(sessionNumber);
if (session == null) {
p.sendMessage("§c未找到编号 " + sessionNumber + " 的竞技场记录,或已超出3天查询范围");
return;
}
long createdAt = (Long) session.get("created_at");
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
if (System.currentTimeMillis() - createdAt > threeDaysMs) {
p.sendMessage("§c该记录已超出3天查询范围,不再支持查询");
return;
}
int dbSessionId = (int) session.get("id");
Map<String, Object> stats = plugin.getPVPArenaManager().getArenaSessionStats(uuid, dbSessionId);
int kills = (int) stats.get("kills");
int deaths = (int) stats.get("deaths");
String worldName = (String) session.get("world_name");
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(createdAt));
p.sendMessage("§6§l[PVP竞技场#" + sessionNumber + "] " + target + " §7(" + dateStr + " " + worldName + ")");
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", deaths > 0 ? (double) kills / deaths : kills));
}
}
+223 -189
View File
@@ -1,13 +1,18 @@
package Sdf1_login;
import org.bukkit.BanList;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
* 风控管理器:密码错误阈值封禁 + 报警邮件
*
* 功能1:密码错误达到阈值 → 自动封禁账号N分钟 + 推送报警邮件
* 功能2:封禁期间拒绝所有登录尝试
*/
public class RiskControlManager {
@@ -17,232 +22,261 @@ public class RiskControlManager {
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
// 每个玩家名 -> 封禁到期时间戳(毫秒)
private final Map<String, Long> banUntil =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
* 获取当前密码错误阈值(从配置读取)
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
private int getMaxFailAttempts() {
return plugin.getConfig2().passwordFailThreshold;
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
* 获取封禁时长(毫秒)(从配置读取)
*/
public void resetFailAttempts(String playerName) {
private long getBanDurationMs() {
return (long) plugin.getConfig2().passwordBanDuration * 1000L;
}
// ==================== 封禁检查 ====================
/**
* 检查玩家是否已被封禁
* @return true=已被封禁(应拒绝登录)
*/
public boolean isBanned(String playerName) {
Long until = banUntil.get(playerName);
if (until == null) return false;
if (System.currentTimeMillis() < until) {
return true; // 仍在封禁期内
}
// 封禁已过期,自动解除
banUntil.remove(playerName);
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 获取玩家剩余封禁时间(秒),0=未封禁
*/
public int getBanRemainingSeconds(String playerName) {
Long until = banUntil.get(playerName);
if (until == null) return 0;
long remaining = until - System.currentTimeMillis();
if (remaining <= 0) {
banUntil.remove(playerName);
failAttempts.remove(playerName);
return 0;
}
return (int) (remaining / 1000) + 1; // 向上取整
}
// ==================== 失败记录 ====================
/**
* 记录一次失败的登录尝试,判断是否触发封禁
*
* @param player 登录失败的玩家
* @return 剩余尝试次数(-1=已被封禁)
*/
public int recordFailAttempt(Player player) {
String name = player.getName();
int maxAttempts = getMaxFailAttempts();
// 已被封禁,直接返回
if (isBanned(name)) {
return -1;
}
int current = failAttempts
.getOrDefault(name, 0) + 1;
failAttempts.put(name, current);
int remaining = Math.max(0, maxAttempts - current);
// 达到阈值 → 触发封禁
if (current >= maxAttempts) {
triggerBan(player);
}
return remaining;
}
/**
* 触发封禁:Bukkit真实封禁 + 记录时间 + 发邮件 + 踢出
* 双层封禁:Bukkit ban阻止连接加入,内存计时器到期自动解封
*/
private void triggerBan(Player player) {
String name = player.getName();
long durationMs = getBanDurationMs();
long until = System.currentTimeMillis() + durationMs;
banUntil.put(name, until);
String ip = plugin.getPlayerIP(player);
int banMinutes = (int) (durationMs / 60000);
plugin.getLogger().warning(
"[风控] 玩家 " + name
+ " 密码错误达到阈值("
+ getMaxFailAttempts() + "次)"
+ ",封禁 " + banMinutes + " 分钟");
// ★ 真实封禁:调用Bukkit BanList API阻止玩家重新加入服务器
String banReason = "§c§l[安全风控] 密码连续错误"
+ getMaxFailAttempts() + "次,"
+ "账号临时封禁" + banMinutes + "分钟";
try {
// 使用 BanList API 设置带过期时间的封禁
Date expiry = new Date(System.currentTimeMillis() + durationMs);
Bukkit.getBanList(BanList.Type.NAME).addBan(
name, banReason, expiry, "[Sdf1_login] 风控");
plugin.getLogger().info(
"[风控] 已通过Bukkit BanList封禁玩家 "
+ name + " (" + banMinutes + "分钟,到期: " + expiry + ")");
} catch (Exception e) {
plugin.getLogger().warning(
"[风控] BanList封禁失败: " + e.getMessage());
}
// 踢出玩家(无论BanList是否成功,立即踢出)
player.kickPlayer(banReason);
// 异步发送报警邮件
sendBanAlertEmail(name, ip, banMinutes);
}
/**
* 发送封禁报警邮件到玩家绑定的邮箱
* 未绑定邮箱则跳过
*/
private void sendBanAlertEmail(String playerName,
String ip, int banMinutes) {
String emailAddr = (String) plugin.getDb()
.getField(playerName, "email");
if (emailAddr == null || emailAddr.isEmpty()) {
plugin.getLogger().info(
"[风控] 玩家 " + playerName
+ " 未绑定邮箱,跳过报警邮件");
return;
}
String subject =
"[安全警告] 您的账号已被临时封禁";
String body = "尊敬的玩家 " + playerName + ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 因密码连续错误 "
+ getMaxFailAttempts() + " 次"
+ ",已被自动封禁 " + banMinutes + " 分钟。\n\n"
+ "来源IP: " + (ip != null ? ip : "未知") + "\n"
+ "封禁到期: "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date(
System.currentTimeMillis()
+ (long) banMinutes * 60000L))
+ "\n\n"
+ "如果这是您本人操作,请等待封禁到期后重试。\n"
+ "如果非本人操作,建议您立即修改密码"
+ "(使用 /sdf1_login pw)。\n\n"
+ "--Sdf1_login 安全系统";
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.getEmail().sendBody(
emailAddr, subject, body);
plugin.getLogger().info(
"[风控] 封禁报警邮件已发送到 "
+ emailAddr);
} catch (Exception e) {
plugin.getLogger().warning(
"[风控] 报警邮件发送失败: "
+ e.getMessage());
}
});
}
// ==================== 成功重置 ====================
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
failAttempts.remove(playerName);
banUntil.remove(playerName);
}
// ==================== 清理 ====================
/**
* 清理过期的冷却记录(可由定时任务调用)
* 清理过期的封禁记录(可由定时任务调用)
* 同步解除过期玩家的Bukkit封禁
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
// 收集过期的玩家名
List<String> expired = new ArrayList<>();
banUntil.entrySet().removeIf(e -> {
if (e.getValue() < now) {
expired.add(e.getKey());
return true;
}
return false;
});
// 同步解除Bukkit封禁
for (String name : expired) {
try {
Bukkit.getBanList(BanList.Type.NAME)
.pardon(name);
plugin.getLogger().info(
"[风控] 封禁到期,已解除Bukkit封禁: "
+ name);
} catch (Exception ignored) {}
failAttempts.remove(name);
}
}
/**
* 获取所有玩家的失败尝试记录(调试用)
* 获取所有封禁记录(调试/管理用)
*/
public Map<String, Long> getAllBans() {
return Collections.unmodifiableMap(banUntil);
}
/**
* 获取所有失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 手动解除某玩家的封禁(管理员用)
*/
public void unban(String playerName) {
banUntil.remove(playerName);
failAttempts.remove(playerName);
// 同步解除Bukkit封禁
try {
Bukkit.getBanList(BanList.Type.NAME)
.pardon(playerName);
plugin.getLogger().info("[风控] 已解除Bukkit封禁: "
+ playerName);
} catch (Exception ignored) {}
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
banUntil.clear();
}
}
+138 -3
View File
@@ -310,6 +310,7 @@ public class WebManager {
dbTaskQueue.offer(task);
if (dbTaskQueue.size() > 50) {
plugin.getLogger().warning("[DB队列] 队列积压: " + dbTaskQueue.size() + " 任务等待");
discardOldestIfOverflow();
}
}
@@ -323,6 +324,50 @@ public class WebManager {
}
}
/**
* ★ 清空DB队列中所有未回应的web同步任务
* (清空队列/cleartake 命令调用)
*/
public void clearQueue() {
int count = 0;
Iterator<DbTask> it = dbTaskQueue.iterator();
while (it.hasNext()) {
DbTask t = it.next();
// 只保留登录/商店等关键任务,丢弃所有同步类web任务
if (t.name.contains("TimerA") || t.name.contains("首次")
|| t.name.contains("周期") || t.name.contains("sync")
|| t.name.contains("poll") || t.name.contains("push")
|| t.name.contains("pull") || t.name.contains("cdk")) {
it.remove();
count++;
}
}
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务");
}
/**
* ★ 队列积压时丢弃最早的任务(优先丢弃同步类低优先级任务)
*/
public void discardOldestIfOverflow() {
if (dbTaskQueue.size() <= 50) return;
int before = dbTaskQueue.size();
// 收集所有普通同步任务(priority >= NORMAL_PRIORITY),按创建时间排序
List<DbTask> syncTasks = new ArrayList<>();
for (DbTask t : dbTaskQueue) {
if (t != null && t.priority >= NORMAL_PRIORITY) {
syncTasks.add(t);
}
}
syncTasks.sort(Comparator.comparingLong(t -> t.createdAt));
// 丢弃最早的一半同步任务(至少丢弃1个)
int toDrop = Math.max(1, syncTasks.size() / 2);
for (int i = 0; i < toDrop && i < syncTasks.size(); i++) {
dbTaskQueue.remove(syncTasks.get(i));
}
int after = dbTaskQueue.size();
plugin.getLogger().warning("[DB队列] 队列积压(" + before + "),自动丢弃" + (before - after) + "个最早的同步请求");
}
// ==================== SSL断路器 + 降级HTTP ====================
/**
@@ -872,13 +917,72 @@ public class WebManager {
// ★ 注册交易监听器(需在BondManager初始化后调用)
public void registerTransactionListener(BondManager bondMgr) {
if (bondMgr == null) return;
bondMgr.addTransactionListener((playerName, type, amount) -> {
plugin.getLogger().info("[Web交易即时] 检测到交易: " + playerName + " " + type + " " + amount + ",立即推送");
requestImmediateTransactionSync();
bondMgr.addTransactionListener((playerName, type, amount, targetPlayer) -> {
// 高频出售商品缓冲逻辑:shop_sell类型商品在1分钟内≥10次时暂缓推送
if ("shop_sell".equals(type) && targetPlayer != null && !targetPlayer.isEmpty()) {
handleHighFreqSell(playerName, targetPlayer, amount);
} else {
requestImmediateTransactionSync();
}
});
plugin.getLogger().info("[Web通信] 交易即时推送监听器已注册");
}
/**
* 高频出售缓冲处理:追踪每种商品的出售频率
* 1分钟内单种商品售卖≥10次时进入缓冲模式,暂缓推送到PHP,60秒后批量推送
*/
private void handleHighFreqSell(String playerName, String itemId, int amount) {
long now = System.currentTimeMillis();
SellItemBuffer buf = sellBuffers.computeIfAbsent(itemId, k -> new SellItemBuffer(itemId));
// 如果窗口已过期(>60秒),重置计数器
if (now - buf.windowStart > 60000) {
buf.count = 0;
buf.windowStart = now;
}
buf.count++;
// 已经在缓冲模式中,不做任何推送
if (buf.buffering && now < buf.bufferEnd) {
return;
}
// 达到阈值,进入缓冲模式
if (buf.count >= HIGH_FREQ_SELL_THRESHOLD && !buf.buffering) {
buf.buffering = true;
buf.bufferEnd = now + HIGH_FREQ_BUFFER_MS;
inHighFreqBufferMode = true;
bufferModeEndTime = Math.max(bufferModeEndTime, buf.bufferEnd);
plugin.getLogger().info("[Web高频] 触发缓冲: " + itemId + " " + buf.count + "次/分钟,暂缓推送60秒");
// 调度60秒后的批量推送
new BukkitRunnable() {
@Override
public void run() {
buf.buffering = false;
buf.count = 0;
buf.windowStart = System.currentTimeMillis();
// 检查是否所有缓冲都结束了
boolean anyBuffering = false;
for (SellItemBuffer b : sellBuffers.values()) {
if (b.buffering) { anyBuffering = true; break; }
}
if (!anyBuffering) inHighFreqBufferMode = false;
plugin.getLogger().info("[Web高频] 缓冲到期,批量推送: " + itemId);
requestImmediateTransactionSync();
}
}.runTaskLater(plugin, 20L * 60); // 60秒
return;
}
// 未达阈值或非shop_sell,立即推送
requestImmediateTransactionSync();
}
public void start() {
if (!enabled) {
plugin.getLogger().info("[Web通信] 未启用,在插件设置.txt中设置 web通信-启用=true");
@@ -1318,6 +1422,26 @@ public class WebManager {
private static final long MIN_IMMEDIATE_TX_SYNC_MS = 3000; // 最小间隔3秒,防止频繁推送
private volatile boolean pendingImmediateTxSync = false;
// ★ 高频出售商品缓冲机制:1分钟内单种商品售卖≥10次时暂缓推送
private final ConcurrentHashMap<String, SellItemBuffer> sellBuffers = new ConcurrentHashMap<>();
private static final int HIGH_FREQ_SELL_THRESHOLD = 10; // 10次触发缓冲
private static final long HIGH_FREQ_BUFFER_MS = 60000; // 缓冲60秒
private volatile boolean inHighFreqBufferMode = false; // 全局缓冲模式标志
private volatile long bufferModeEndTime = 0; // 缓冲模式结束时间
private static class SellItemBuffer {
final String itemId;
volatile int count = 0;
volatile long windowStart = 0;
volatile boolean buffering = false;
volatile long bufferEnd = 0;
SellItemBuffer(String itemId) {
this.itemId = itemId;
this.windowStart = System.currentTimeMillis();
}
}
private void requestImmediateTransactionSync() {
long now = System.currentTimeMillis();
if (now - lastImmediateTxSyncTime < MIN_IMMEDIATE_TX_SYNC_MS) {
@@ -4674,6 +4798,11 @@ public class WebManager {
public void syncBondTransactions() {
if (!enabled) return;
// ★ 高频出售缓冲模式:跳过周期推送,等待缓冲期满后由监听器统一推送
if (inHighFreqBufferMode) {
return;
}
try {
BondManager bondMgr = plugin.getBonds();
if (bondMgr == null) return;
@@ -5100,6 +5229,12 @@ public class WebManager {
// 记录为已验证正版玩家
plugin.addVerifiedPremiumPlayer(playerName, mcUuid, mcUsername);
// ★ 记录OAuth登录IP(用于异地登录风控)
String oauthIP = plugin.getPlayerIP(player);
if (oauthIP != null && !oauthIP.isEmpty()) {
plugin.getDb().setField(playerName, "last_oauth_ip", oauthIP);
}
// 在主线程执行自动登录
Bukkit.getScheduler().runTask(plugin, () -> {
if (player.isOnline() && !plugin.getLoggedIn().contains(playerName)) {
+7 -1
View File
@@ -1,5 +1,5 @@
name: Sdf1_login
version: 2.8
version: 2.81
main: Sdf1_login.Main
api-version: '1.21'
commands:
@@ -178,3 +178,9 @@ commands:
who:
description: 查看在线玩家列表
usage: /who
清空队列:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /清空队列
cleartake:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /cleartake