Compare commits

...
429 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
youpaishidifu 1133f37bf9 fix: 改用actions/setup-java@v4从Temurin下载JDK25,彻底绕开服务器JDK探测和sdkman冲突
Auto Release / build (push) Successful in 49s
2026-07-19 01:14:27 +08:00
youpaishidifu 0ddc47b5d4 fix: 彻底绕开sdkman,直接用/usr/lib/jvm最高版本JDK,JAVA_HOME写入GITHUB_ENV并强制export到PATH
Auto Release / build (push) Failing after 7s
2026-07-19 01:10:21 +08:00
youpaishidifu c0be75474f fix: 改用<release>25替代<source>/<target>,确保JAVA_HOME正确传递到Maven步骤
Auto Release / build (push) Failing after 8s
2026-07-19 01:06:54 +08:00
youpaishidifu daa4a3cb7c fix: 升级Maven compiler-plugin到3.15.0,优化Java检测逻辑
Auto Release / build (push) Failing after 9s
2026-07-19 01:01:55 +08:00
youpaishidifu 09873425b9 fix: Maven检测优先用系统已安装(command -v mvn),下载源换阿里云镜像
Auto Release / build (push) Failing after 43s
2026-07-19 00:56:13 +08:00
youpaishidifu 2b20098f09 fix: release token 改用 GITEA_TOKEN||GITHUB_TOKEN 回退,零配置可跑通
Auto Release / build (push) Failing after 2m16s
2026-07-19 00:47:18 +08:00
youpaishidifu e2dd59e89e fix: Gitea工作流改用服务器已装JDK+Maven二进制,绕开dnf拉java-17冲突
Auto Release / build (push) Failing after 2m19s
2026-07-19 00:46:09 +08:00
youpaishidifu d66a792a2e 更新 README.md
Auto Release / build (push) Failing after 8s
2026-07-19 00:03:51 +08:00
youpaishidifu 87d190a013 Gitea工作流改用self-hosted + --no-docker模式
Auto Release / build (push) Has been cancelled
2026-07-18 23:37:29 +08:00
youpaishidifu 481e2ea90b 简化Gitea工作流:移除外部action依赖,使用原生命令构建和API创建release
Auto Release / build (push) Has been cancelled
2026-07-18 23:26:03 +08:00
youpaishidifu 4b19b65a84 简化Gitea工作流:单job + ubuntu-latest + JDK 21
Auto Release / build-and-release (push) Failing after 4s
2026-07-18 23:01:41 +08:00
youpaishidifu 7f425f2c9f Gitea Actions: 硬编码token用于release创建
Auto Release / build (push) Failing after 8s
Auto Release / release (push) Has been skipped
2026-07-18 22:49:55 +08:00
youpaishidifu 4a03608540 更新Gitea Actions工作流:添加release job,使用gitea release-action
Auto Release / build (push) Failing after 1m30s
Auto Release / release (push) Has been skipped
2026-07-18 22:35:34 +08:00
youpaishidifu 60504ebdf1 修复Release工作流:JDK 26 + zulu发行版,Gitea改用self-hosted runner
Auto Release / build (push) Failing after 32s
2026-07-18 22:01:09 +08:00
youpaishidifu 79e6e8354b 清理JVM崩溃日志并添加gitignore
Auto Release / build (push) Has been cancelled
2026-07-18 21:53:45 +08:00
youpaishidifu b890480a55 简化Release工作流:先确保构建通过
Auto Release / build (push) Has been cancelled
2026-07-18 21:53:11 +08:00
youpaishidifu 104286a66e 修复自动Release工作流:GitHub用标准action,Gitea用专用release-action
Auto Release / build-and-release (push) Has been cancelled
2026-07-18 21:49:03 +08:00
youpaishidifu 996fd41d8a 添加GitHub/Gitea自动release工作流:每次push到main分支自动构建并发布JAR
Auto Release / build-and-release (push) Has been cancelled
2026-07-18 21:32:23 +08:00
youpaishidifu c6e4e0a520 邮箱校验拒绝时显示允许的后缀列表:新增getAllowedSuffixHint()方法,3处校验点全部附带提示 2026-07-18 18:42:16 +08:00
youpaishidifu 3f7f7f6c33 移除敏感文件deploy_sftp.py
移除敏感文件deploy_sftp.py
2026-07-18 18:36:26 +08:00
youpaishidifu 1f4f9e343a 修复email复读机+配置文件自动补全不生效:1.给独立/email和/绑定命令添加处理器 2.saveSmtp保留未知key 3.writeLines renameTo失败时fallback到copy+delete 2026-07-18 18:28:44 +08:00
youpaishidifu b96cf8c664 修复邮箱黑白名单不生效+email复读机bug:loadMap支持多行值(裸行续行),email命令添加args长度检查,移除重复email处理器死代码 2026-07-18 18:11:00 +08:00
youpaishidifu 9d52051023 修复SMTP配置加载:移除重复loadMap调用,确保文件不存在时也能正确加载默认值,添加详细日志输出 2026-07-18 17:40:38 +08:00
youpaishidifu d96f16e337 修复SMTP配置热更新问题:加载时自动补全缺失配置项,支持UTF-8 BOM,添加email reload命令 2026-07-18 17:03:28 +08:00
youpaishidifu 535d32f2bb 新增邮箱黑白名单校验功能:SMTP配置文件重命名为邮件配置,分为SMTP信息区和运行模式区,默认不校验,白名单模式仅允许列表后缀,黑名单模式禁止列表后缀 2026-07-18 16:40:53 +08:00
youpaishidifu c79924f67d refactor: 移除/mscode命令,正版验证全部交web后端接管
- 移除/mscode命令(plugin.yml + onCommand + executor注册)
- 移除extractCodeFromInput()方法(仅/mscode使用)
- 移除verifyMinecraftAuthCode()方法(仅/mscode使用)
- Java端仅负责:创建session → 轮询PHP状态 → 自动登录
- 所有授权码输入由PHP粘贴页全权处理(自动提交)
- 解决MC聊天框256字符硬限导致URL截断的根本问题
2026-07-17 20:55:31 +08:00
youpaishidifu f5a3dfbda5 fix: AreaGUIManager硬编码URL改为从插件配置读取
- AreaGUIManager删除用户组时通知PHP同步,URL和密钥之前写死为caoyuan.ypshidifu.cn
- 现在改为从webManager.getWebBaseUrl()和getSecretKey()读取,支持其他服主自定义web后端
- extractCodeFromInput增强支持#code=fragment形式(上一轮修复)
- verifyMinecraftAuthCode加debug日志便于排查
- deploy_sftp.py备份后缀改为.jar.old/.old.N(不以.jar结尾,避免MC加载冲突)
- .gitignore添加hs_err_pid*.log
2026-07-17 19:40:01 +08:00
youpaishidifu c568703996 fix: 自动正版验证移至verifyPremiumAsync回调中触发 2026-07-17 17:16:41 +08:00
youpaishidifu 3251a48b99 feat: 玩家加入后延时5秒自动触发正版验证 2026-07-17 16:49:46 +08:00
youpaishidifu f83458fd4b 修复正版验证三大问题:1)paste_code_page Content-Type改为text/html修复HTML源码输出 2)/mscode支持粘贴完整URL自动提取code参数 3)HTTP错误处理改进,非200也返回body显示PHP错误信息 4)verify_code改为JSON格式POST 2026-07-17 16:13:40 +08:00
youpaishidifu c3d2f81e9e 添加/mscode命令:支持粘贴Microsoft OAuth授权码(公共客户端流程) 2026-07-17 14:02:08 +08:00
youpaishidifu f8b44dd97b 修复自动登录安全漏洞:移除verifyPremiumAsync自动登录路径
问题:离线模式服务器中,所有玩家UUID都是离线格式。
旧代码通过Mojang API检查名字是否存在,如果存在就autoLogin。
这意味着盗版客户端只要用正版玩家的名字就能跳过登录。

修复:
- 移除verifyViaPhpBackend和fetchMojangUuid
- 离线UUID玩家一律视为非正版,需手动登录或OAuth验证
- 保留3条安全的自动登录路径:
  1. 检查点0: isVerifiedPremiumPlayer (OAuth验证)
  2. 检查点1: hasJavaLogin (5分钟内重连)
  3. 检查点2: isWebLoginVerified (Web密码验证)
2026-07-17 11:06:03 +08:00
youpaishidifu ab4fe441fe 新增Microsoft OAuth正版验证功能
1. PHP端新增minecraft_auth.php,实现完整的Microsoft OAuth流程
   - 支持Microsoft账号登录验证
   - 获取Minecraft Profile(UUID、用户名)
   - 验证状态持久化到SQLite
   - 生成可点击的授权URL

2. Java端新增/mslogin和/正版命令
   - 调用PHP后端获取授权URL
   - 显示可点击链接给玩家
   - 轮询PHP检查验证状态
   - 验证成功后自动登录

3. 修改VerificationManager,集成PHP后端验证
   - 优先通过PHP验证(支持Microsoft OAuth)
   - PHP不可用时降级到Mojang API
   - 支持已验证正版玩家自动登录

4. 新增verifiedPremiumPlayers内存存储
   - 服务器重启前有效
   - 支持快速重连无需重复验证
   - 下线时保留验证状态

5. 新增plugin.yml命令定义
   - /mslogin: 英文版正版验证
   - /正版: 中文版正版验证
2026-07-17 10:22:31 +08:00
youpaishidifu 317d1fdbcc 注释快速补单日志减少噪音 2026-07-16 15:38:52 +08:00
youpaishidifu 7ba43d820a 修复领地权限拦截:豁免VISITOR(领地授权成员)在非安全权限检查 2026-07-16 14:37:56 +08:00
youpaishidifu a5edbfe314 修复: Java doGet超时30秒+poller详细日志
- doGet超时从10秒增加到30秒,确保poller有足够时间
- poller调用添加耗时统计和详细日志
- 修复pullPendingTransactions中的语法错误
- 解决: Java调用poller超时导致补单失败
2026-07-15 20:25:01 +08:00
youpaishidifu e791548e87 doTransactionPollCheck添加高频补单触发器(15秒间隔)
- WebManager: doTransactionPollCheck每~5秒触发poller_online.php(限流15秒)
- 解决补单延迟(原PollC~30-60秒才触发)

关闭 #1059
2026-07-15 18:01:06 +08:00
youpaishidifu 501aa40677 feat: Web在线充值(recharge)分支 — 支付平台0.01元测试到账
- processWebTransaction 新增 type=recharge/pay_recharge 专用分支
- 支付成功后 addBonds 发放债券 + 游戏内即时通知
- 复用现有 ~5s 轮询 + ackTransaction 防重 + confirmed_transactions 双保险
2026-07-14 20:55:04 +08:00
youpaishidifu 181c2acb34 chore: 关闭背包翻译的INFO日志刷屏
- 移除MaterialTranslator.translate()和translateWithFallback()中的info日志
- 保留warning仅用于翻译源异常降级场景
- 控制台不再被每条物品的翻译缓存/命中日志淹没
- 旧版: Sdf1_login.jar.old.9
2026-07-14 19:00:19 +08:00
youpaishidifu bf0688b291 feat: 背包备份物品名走官方翻译+机翻兜底
- translateMaterial() 由硬编码替换为 MaterialTranslator.translate()
- 与领地方块记录系统一致: 优先查 zh_cn.json 官方翻译, 机翻兜底
- 旧版: Sdf1_login.jar.old.8
2026-07-14 18:48:15 +08:00
youpaishidifu 765fbd74c1 fix: 交易拉取即贴标导致自己跳过处理
根因: ackTransaction()将txId加入confirmedTxIds内存set,
processWebTransaction()检查到set包含该ID就跳过处理。
交易还没处理就被自己标记的内存set跳过了。

修复: ackTransaction()不再写confirmedTxIds,
仅写PHP confirmed_transactions表。
confirmedTxIds仅在confirmTransaction()处理完后才写入。
旧版: Sdf1_login.jar.old.7
2026-07-14 17:03:12 +08:00
youpaishidifu d2180cec1c feat: 告示牌放置/编辑监控记录
1. land_block_log表新增detail列(已有数据库ALTER兼容)
2. DatabaseManager新增recordLandBlockWithDetail方法
3. LandRecordManager新增recordSignEdit方法,记录sign_place/sign_edit动作+内容
4. AreaProtection.onSignChange()延迟1tick记录(等事件处理完)
5. 回声碎片显示告示牌内容: 玩家XXX在HH:MM放置/编辑告示牌 [内容]
2026-07-14 16:47:57 +08:00
youpaishidifu 9ccda3f541 fix: ack_transaction补tx_type + 告示牌权限豁免授权成员
1. WebManager.ackTransaction()增加txType参数, bodyJson发送tx_id+tx_type
2. AreaProtection.onSignChange(): 白名单内授权成员跳过denySignEdit检查
   PermissionLevel无MEMBER枚举, 授权成员=白名单VISITOR, 通过isPlayerWhitelisted判定
2026-07-14 15:32:52 +08:00
youpaishidifu 401fa2453a fix(area-gui): 告示牌权限动态名称:允许/禁止编辑告示牌 2026-07-14 12:55:40 +08:00
youpaishidifu 6946cfef6d feat(area-gui): CLI/GUI菜单添加禁止编辑告示牌权限条目 2026-07-14 12:42:49 +08:00
youpaishidifu 89fa765351 fix(web): 拉取交易即刻贴标ack,防止SSL断路器后重复扣费 2026-07-14 11:27:46 +08:00
youpaishidifu a118aac18c feat(land): 添加告示牌编辑独立权限和红石状态记录 2026-07-14 10:39:59 +08:00
youpaishidifu da4299cc2f 分离登录备份和自动备份显示:backup/back命令分两组列出,L前缀=登录备份,A前缀=自动备份 2026-07-13 18:16:33 +08:00
youpaishidifu 112f992fda 修复getInventoryBackups/getInventoryBackupById查错表:inventory_backups→inv_backup_auto,导致自动备份数据查不到 2026-07-13 17:57:11 +08:00
youpaishidifu 3674e1e950 修复backup子命令权限:仅OP/插件管理员/控制台可执行,支持查询他人备份 2026-07-13 17:04:46 +08:00
youpaishidifu abdd3ad7c7 修复滚动备份日志追踪+告示牌编辑权限控制 2026-07-13 16:51:35 +08:00
youpaishidifu a3c8cec664 官方翻译纯同步模式:移除异步刷新分支,部署脚本附带上传zh_cn.json缓存 2026-07-13 15:56:22 +08:00
youpaishidifu 5a0166e90c 修复官方翻译首次部署时空map问题:缓存不存在时改为同步下载,确保init返回前map就绪 2026-07-13 15:31:59 +08:00
youpaishidifu 6c6cc94905 翻译链路日志:OfficialTranslations.lookUp + MaterialTranslator.translate/translateWithFallback 每步打印日志,跟踪翻译路径 2026-07-13 15:18:56 +08:00
youpaishidifu f5a5b28f61 添加官方中文翻译源:从Mojang API下载zh_cn.json作为优先翻译,机翻兜底 2026-07-13 15:02:59 +08:00
youpaishidifu e3b459a00c 修复容器记录仅显示打开:补充存入(put)+拿取(take)差量记录,容器类型纳入翻译 2026-07-13 14:38:50 +08:00
youpaishidifu 3ef7fbdb7a 修复容器记录+翻译集成:记录移到权限检查前,queryAndPrint异步批量翻译输出中英双语 2026-07-13 14:12:47 +08:00
youpaishidifu 3795dd90eb 修复CLI菜单权限显示:管理员只显示玩家发光,隐藏全部权限配置 2026-07-13 13:56:44 +08:00
youpaishidifu 513e4c56ad 修复拾取/丢弃权限:per-player独立授权键名映射(allowPickup↔denyPickup) 2026-07-13 13:43:16 +08:00
youpaishidifu 8138b97b59 领地记录物品名多源翻译:有道+简心+Google三级降级 2026-07-13 13:15:01 +08:00
youpaishidifu 697b1069a2 PVP四项改进:回声碎片CLI入口/击杀≥10奖励盔甲药水/世界击杀纳入总榜/选装前无敌 2026-07-13 12:28:13 +08:00
youpaishidifu c7cdb60b47 修复边框粒子卡死主线程:动态密度控制+粒子总量上限 2026-07-13 12:00:27 +08:00
youpaishidifu 7489c60286 新增领地方块/容器记录系统(回声碎片)与背包滚动自动备份
- 新增 land_block_log、land_container_log、inv_backup_auto 三张表
- LandRecordManager:领地内方块破坏/放置、容器打开/拿取自动记录;回声碎片工具右键/左键查询坐标历史记录
- AreaGUIManager 主菜单新增回声碎片按钮(位置33)
- InventoryAutoBackup:每15分钟滚动备份在线玩家背包(每人保留3份),跳过PVP竞技场/练习场
- 每日定时清理超过7天的领地记录
- AreaProtection 钩子接入记录;注册 /landrec 命令
2026-07-13 10:26:05 +08:00
youpaishidifu 8ce2e727bb 领地改名和入场退场消息接入URL过滤逻辑
- 领地改名:输入新名字前先检测URL,包含链接则拒绝且不计入改名次数
- 入场/退场消息:设置消息前先检测URL,包含链接则拒绝且不保存
- 复用ChatFilterManager.extractUrls()统一过滤逻辑
2026-07-13 09:43:21 +08:00
youpaishidifu e7402985d5 修复封禁广播Long拆箱NPE + syncBans/syncAdmins改POST避免URL超长
- handleBanCommand中long expireMs→Long expireMs,避免null拆箱异常
- syncBans/syncAdmins从httpGet改为httpPost,防止55条封禁数据URL过长
- sync.php已部署含sync_bans/sync_admins case的版本到线上
2026-07-13 00:35:09 +08:00
youpaishidifu c0ab91edba R23-2: 封禁同步彻底修复 - syncBans独立hash+控制台ban监听+插件ban即时同步PHP 2026-07-12 23:45:59 +08:00
youpaishidifu 529b83666c R23: 修复4项问题 - 改名冷却逻辑(仅达上限才提示冷却) + 拦截原版/ban、/ban-ip命令广播封禁警告 2026-07-12 23:08:32 +08:00
youpaishidifu 569611c40f 新增3个功能:领地改名冷却、封禁名单联控、封禁广播警告
1. 领地改名冷却机制:1小时限5次,自然滚动窗口
   - AreaProtection: renameLand/renameCooldownMap/getRenameCooldown/getRemainingRenames
   - CLI: /protect cli rename <领地名> 交互式输入
   - GUI: Name Tag按钮(slot 33)显示冷却状态
   - 改名更新DB+内存+触发PHP同步

2. PHP用户管理显示封禁名单(联控Java BanList)
   - WebManager.syncBans(): 收集Bukkit名字/IP封禁→推送到PHP
   - PHP: web_player_bans表 + handleSyncBans + list_bans API
   - admin.php: 用户行红色+禁图标标注被封禁玩家

3. 封禁事件公屏广播警告
   - Main.applyBan(): 封禁后自动广播
   - broadcastBanWarning(): 玩家名+IP(如IP封禁)+理由+到期时间
   - loadBanWarningMessage(): 从释放警告消息.txt随机读取标语
   - 支持永久封禁/限时封禁/IP封禁3种场景
2026-07-12 22:08:38 +08:00
youpaishidifu 61e4ec967f 修复公告消息取消功能:取消后返回主菜单而非编辑子页
cancel路径showAnnouncementManagement(p, landName, 2/3)会重新设置pending输入状态,导致取消后仍停留在等待输入状态。改为showAnnouncementManagement(p, landName, 1)返回主菜单
2026-07-12 21:07:06 +08:00
youpaishidifu 57788016a4 修复占位符括号+tag管理员CLI权限视图
1. 变量替换正则新增{}花括号支持,支持领地名和领地两种写法
2. CLI showPlayerPerm:tag管理员(isAdmin)查看管理员成员时看到完整权限编辑视图,不再简化;可切换管理员身份
2026-07-12 20:58:21 +08:00
youpaishidifu ad935cbe09 修复4个bug:权限/颜色渲染/换行符/取消功能
1. toggleAdmin:插件管理员可操作非自身领地的管理员身份
2. formatAreaMsg颜色渲染:convertHtmlTags新增activeColor追踪,标签关闭§r后自动恢复颜色码,解决<span style=color>嵌套<b><u>标签时颜色在闭合处截断的问题
3. 变量替换正则改为Pattern.compile+DOTALL,确保跨行匹配
4. handleAnnouncementInput新增取消/0支持,输入取消或0时保持原样
2026-07-12 20:32:50 +08:00
youpaishidifu ae94f8edef 插件管理员可协助管理其他领地成员权限
- GUI领地列表:tag管理员可见所有领地,点击索引修正(allManageableLands替代getLandsByOwner)
- openPlayerPerm:tag管理员(isAreaAdmin)看到完整权限编辑视图,不再简化
- CLI showPlayerPerm:设为管理员按钮对tag管理员也可用
- WebManager:新增syncAdmins(),将tag管理员名单同步到PHP的web_plugin_admins表

需要PHP端配合:land_api.php新增canManageLand/isPluginAdmin逻辑
2026-07-12 19:45:51 +08:00
youpaishidifu 9af6c0cca0 feat: GUI领地公告出入消息配置 + 增强HTML语义解码
1. GUI设置面板新增进入消息(位置19)和离开消息(位置21)编辑入口,点击关闭GUI后聊天栏输入
2. AreaCLIManager新增setPendingAnnouncementInput供GUI调用
3. 新增decodeHtmlEntities: &amp; &lt; &gt; &quot; &apos; &#39; &nbsp;等HTML实体解码
4. 新增processColorTags: 支持<span style='color:#xxx'>和<p style='color:xxx'>颜色标签
5. htmlToMc增强: 支持<strong><em><ins><del><strike>等语义标签
6. 新增convertHtmlColorToMc/convertRgbToMc: HTML颜色名/十六进制/rgb()→MC颜色代码
2026-07-12 18:41:48 +08:00
youpaishidifu 49ad056a71 fix: 退出领地立即清除效果,不再依赖5秒延时
根因:playerMarkedEffects标记不可靠(hotUpdateRegionEffects会清空、
applyRegionEffects可能跳过添加),导致离开时marked为空→无延时任务→效果残留

修复:
1. 移动退出到野外:直接从oldAc.giveEffects读取效果列表,立即removePotionEffect
2. 传送到野外:同上逻辑
3. 领地→领地跨区域仍保留5秒延时差量清理
2026-07-12 18:27:38 +08:00
youpaishidifu c0bb962a11 feat: Token泄露PHP联控作废 + 领地公告变量替换
1. Token安全防线:Java revokeToken异步调用PHP sync.php revoke_weblogin_token端点,从weblogin_tokens表删除
2. PHP新增revoke_weblogin_token action:验证SECRET_KEY后删除weblogin_tokens中的记录
3. 领地公告变量替换:支持(玩家)/(领地)及[]{}【】<>《》等括号格式
4. 默认消息:进入=欢迎(玩家)来到(领地),离开=感谢(玩家)光临(领地)
5. 公告管理菜单显示变量说明和支持的括号格式
2026-07-12 17:43:23 +08:00
youpaishidifu 0db237ed04 feat: 公屏泄露webtoken自动作废安全防线
1. 玩家在公屏发送有效的48位hex token时,立即作废该token
2. 向玩家发送安全警告,提示不应分享token
3. WebManager新增revokeToken/hasToken方法
2026-07-12 17:05:56 +08:00
youpaishidifu 5d994c0dad feat: 领地边框粒子特效 + 强制游戏模式仅管理员可用
1. 进入/离开领地时展示3秒边框粒子(青色渐变DUST_COLOR_TRANSITION + 四角END_ROD高亮)
2. /protect info执行时展示5秒领地边框粒子
3. GUI强制游戏模式开关改为仅服务器管理员可操作(领地主不可操作)
2026-07-12 16:45:22 +08:00
youpaishidifu 0ff79aa959 实现在移动事件中实时热更新增益效果:新增hotUpdateRegionEffects方法,玩家在领地内移动时每2秒自动对比效果配置,实时增删缺失/多余效果,无需退出重进 2026-07-12 14:52:49 +08:00
youpaishidifu fa4402d426 修复增益效果不生效+完善所有药水效果中英文映射:resolveEffectType改用PotionEffectType常量替代不可靠的getByName兜底,支持所有MC版本 2026-07-12 13:03:48 +08:00
youpaishidifu 2fb801a59b 修复再生效果不生效:resolveEffectType缺少再生/回复中文别名映射 2026-07-12 12:19:24 +08:00
youpaishidifu 69868a4c5c 修复PHP添加增益效果后游戏内等级错误:applyFieldToAreaConfig的give_effects改用parseEffectsString替代已损坏的parseJsonArray 2026-07-12 11:41:26 +08:00
youpaishidifu 4888bb414e 修复: syncLandData权限hash漏算permissions导致成员权限变更不触发同步
perms hash 段原仅含 land_id/player_name/role,未包含 permissions 字段内容,
导致游戏内修改成员权限后 hash 不变、不触发 syncPermissions 推送到 PHP。
现把 permissions 内容加入 hash 计算(含 | 分隔符),成员权限变更可正确同步。
2026-07-12 10:29:43 +08:00
ypshidifu 890c637373 修复: PHP→Java双重JSON编码导致领地名反斜杠 + 防御性清除领地名特殊字符
- AreaProtection: updateVisitorPermFromWeb入口处清除领地名中的反斜杠
- WebManager: perm_change/add_visitor/remove_visitor/perm_clear四个case入口统一清洗land_name
- 根因: PHP json_encode双重编码产生\uXXXX, decodeUnicodeEscapes解码后残留反斜杠
- getLandIdFromDb用带反斜杠的名字查DB返回-1,导致整个降级逻辑被跳过
2026-07-12 01:05:27 +08:00
youpaishidifu 912bd62201 R16: 修复回调重试+SSL降级: callbackAddVisitorToPHP失败时尝试HTTP降级 2026-07-11 18:10:21 +08:00
youpaishidifu 0bd4d454c3 R15: PHP添加成员改为Java验证后入库(参考改领地主流程)
- add_visitor处理先验证玩家是否存在(login.db),存在则入库+回调PHP成功,不存在则回调PHP失败
- 新增callbackAddVisitorToPHP方法回调PHP添加成员结果
2026-07-11 15:53:00 +08:00
youpaishidifu bf32e528f1 R14: 修复Java→PHP权限同步缺失(hash不含权限数据导致跳过同步)
- syncLandData hash加入权限数据特征,确保权限变化时触发同步
2026-07-11 15:10:51 +08:00
youpaishidifu 5ef86ddfe8 R13: 修复addvisitor校验+PHP权限同步完整流程
- /protect addvisitor 命令添加玩家真实性校验(login.db检查)
- addvisitor/handleAddMemberInput 同时写入area_land_permissions表
- perm_change handler 确保玩家加入areaPlayerWhitelist(解决PHP同步不显示问题)
- PHP端handleChangeVisitorRole 新成员改走add_visitor流程(确保Java白名单同步)
2026-07-11 14:19:01 +08:00
youpaishidifu e08a6bc4c7 修复领地权限系统4大bug:DB列缺失+成员不同步+管理员继承+login.db校验
1. AreaProtection.updateVisitorPermFromWeb: INSERT语句去掉不存在的land_name/synced_at列,修复"table area_land_permissions has no column named land_name"
2. AreaProtection.removePlayerFromAreaWhitelist: 移除成员时同步删除area_land_permissions记录,确保admin身份一并清除,不再继承
3. AreaProtection.removeLandMember: GUI移除成员也同步删除area_land_permissions
4. AreaProtection.insertLandPermission: 新增公共方法,PHP添加成员时写入权限表
5. AreaProtection.handlePendingAddMemberInput: 添加成员前校验login.db(DatabaseManager.userExists),拒绝不存在的玩家
6. WebManager: 新增add_visitor/remove_visitor变更类型处理,PHP添加/移除成员后Java自动同步白名单+权限表
2026-07-11 13:36:26 +08:00
youpaishidifu 5ec61a1d2e 修复PHP→Java权限同步断裂:内存缓存不刷新+syncLandData改POST
1. AreaProtection.updateLandFieldFromWeb: 新增applyFieldToAreaConfig()直接更新内存AreaConfig(原reloadAreaConfigFromDb只重载area_config全局表,不碰area_lands领地权限)
2. WebManager.syncLandData: GET改POST(JSON body)解除URL长度限制
3. WebManager.syncLandData: hash补全遗漏字段(9个)确保所有变更能触发同步
2026-07-11 12:21:32 +08:00
youpaishidifu 8c1e1a89bd 修复PHP↔Java领地同步断裂:GET改POST+补全hash漏检字段
根因:syncLandData用GET发送全部领地JSON到sync.php,超URL长度限制→静默失败→PHP永远收不到Java的权限/效果变更。

1. WebManager.syncLandData: 领地同步从httpGet改为httpPost(JSON body),解除URL长度限制
2. WebManager.syncLandData: hash补全遗漏字段(allow_visitor_teleport/confiscate_items/deny_use_items/punish_commands/peace_whitelist/mode_exempt/enable_announce/announce_template/txt_content),确保这些字段变更能触发同步
3. PHP sync.php syncLands(): 已兼容POST JSON body(getParam从php://input读取)
2026-07-11 11:57:22 +08:00
youpaishidifu bc155526ab 止住效果解析刷屏:加强JSON解析、去除残留反斜杠、同一名称只警告一次
1. AreaProtection.resolveEffectType: 解析前去除首尾空白/引号/反斜杠,识别\试炼之兆等残留转义名
2. AreaProtection.splitToList/parseEffectsString: 用正则提取JSON字符串并unescape \ " \/ \b \f \n \r \t 及 \uXXXX
3. 新增UNKNOWN_EFFECT_WARNINGS集合,同一名称只打印一次[防护]无法识别效果,避免控制台持续刷屏
2026-07-11 11:11:59 +08:00
youpaishidifu 4ae1589883 修复Web通信禁用仍轮询+PVP远程武器与击杀奖励+修复splitToList编译错误
1. WebManager: 5个合并定时器(A/B/C/D/E)新增enabled运行时守卫,Web通信关闭后立即停止请求web配置地址;reloadWebConfig支持运行时启停定时器
2. PVPTestManager: 测试场新增弓/弩二选一(默认弓)默认装备,完成一波击杀发放16牛排补血
3. PVPArenaManager: 正式场新增弓/弩二选一(默认弓)默认装备+GUI切换,每2次杀敌发放16牛排补血
4. AreaProtection: 修复splitToList中[\""未结束字符串字面量导致的编译失败
2026-07-11 01:04:32 +08:00
youpaishidifu 9121bb069d 修复PHP权限同步游戏:1.updateLandFieldFromWeb白名单补充is_public_building/allow_visitor_teleport;2.updateVisitorPermFromWeb支持PHP显式角色切换(admin↔member↔visitor) 2026-07-10 23:16:32 +08:00
youpaishidifu d306431c55 修复两大关键bug:1.saveAreaToDb参数绑定从位置32开始整体偏移一位导致权限保存到错误列(权限回滚根因);2.onDisable取消所有定时任务解决async task未关闭警告 2026-07-10 21:32:08 +08:00
youpaishidifu 0354156eec 新增载具乘坐拦截:VehicleEnterEvent处理器,层级为L2实体交互→L3骑乘(denyMount),与放置/破坏权限统一 2026-07-10 21:01:19 +08:00
youpaishidifu aa8b7ce47a 修复实体破坏防护:新增载具与悬挂物权威破坏事件拦截(VehicleDestroyEvent/HangingBreakEvent),统一 L1破坏总开关→L2实体交互 层级,区域取法统一为实体坐标优先 2026-07-10 19:55:56 +08:00
youpaishidifu 483acbdbb6 修复领地权限系统:新增实体交互二级权限,并修复领地数据保存因重复列导致落库失败的问题 2026-07-10 19:08:18 +08:00
youpaishidifu 0775f2e84d fix(core): restore PHP sync disabled for debugging; root cause confirmed as INSERT OR REPLACE id change 2026-07-10 15:29:03 +08:00
youpaishidifu ebd26ee123 fix(core): saveAreaToDb 的 INSERT OR REPLACE 导致自增 id 变更,切断 area_land_permissions.land_id 引用 2026-07-10 15:22:47 +08:00
youpaishidifu 0ff274896a 修复测试场/back返回不刷怪:startTest未写入chosenDiff/chosenEquip;注释领地PHP同步排查权限强制同步bug 2026-07-10 13:42:23 +08:00
youpaishidifu f345f60ac0 修复PVP测试场强制选装:进入测试世界必须选装备,关闭2次GUI遣返主世界;/back禁止传送至测试世界 2026-07-10 13:17:00 +08:00
youpaishidifu babbe8936a 修复: PVP/测试场护甲穿戴顺序修正+强制穿戴到装备槽
- PVPTestManager.giveEquip: ARMOR_SETS[头盔,胸甲,护腿,靴子] 反转为 setArmorContents 期望的 [靴子,护腿,胸甲,头盔]
- PVPArenaManager.equipFullSet: addItem → setArmorContents,护甲自动穿戴到装备槽而非放在物品栏
2026-07-10 12:52:44 +08:00
youpaishidifu 0e0d6c6247 修复: 领地管理员永不被PHP perm_change降级 + 测试场KEEP_INVENTORY=false
- AreaProtection.updateVisitorPermFromWeb: admin角色永远保留, perm_change只更新permissions JSON不动role
- PVPTestManager: KEEP_INVENTORY从true改为false, onPlayerDeath不再直接clearInventory, 改由onRespawn统一还原
2026-07-10 12:18:46 +08:00
youpaishidifu c6d0222c1e 修复: setPlayerPermJson保留已有角色+PVP测试场死亡清空装备
- AreaProtection.setPlayerPermJson: ON CONFLICT替代INSERT+UPDATE两步, 新行默认visitor(非member), 已有行仅更新permissions保留role
- PVPTestManager: 新增onPlayerDeath清空测试装备掉落+setKeepInventory(false), 死亡界面不再显示测试装备
  防止玩家死亡后快速重连/返回时携带测试场装备
2026-07-10 11:58:23 +08:00
youpaishidifu 2bdb577c89 修复: 领地权限拦截船/矿车等实体放置和破坏 2026-07-10 11:23:10 +08:00
youpaishidifu b29284bb1b 修复: Java端调整访客权限误降级管理员+PVP/测试场装备隔离
- AreaProtection.updateVisitorPermFromWeb: 拦截PHP发来member角色时降级admin
- Web/api/sync.php: syncPermissions ON CONFLICT替代INSERT OR REPLACE防角色降级
- Web/api/land_api.php: handleSyncPermissions同上
- PVPTestManager: respawn立即恢复背包, join时空降检测恢复
- PVPArenaManager: 入场立即备份清空背包, respawn时恢复+送出PVP世界
2026-07-10 10:58:55 +08:00
youpaishidifu 97d96d747d 测试场GUI改直选式+领地权限降级拦截与全链路日志+backup保留3份修复
- PVPTestManager: openGUI/handleClick 重写为直选式(武器/护甲/难度/场景/食物直接点选),对齐正式场 PVPArenaManager
- AreaProtection: updateVisitorPermFromWeb 加端到端 info 日志(land/player/localRole/effectiveRole),追踪已授权用户被同步访客问题
- land_api 联动: handleChangeVisitorRole 修复$permRow未定义,新增 admin/member 降级为 visitor 的拦截
- WebManager: perm_change 路由按 changeData 是否含 player 区分 target/player
- DatabaseManager: 背包备份由子查询LIMIT改为先查后删,修复仅保留最新一份
2026-07-10 01:33:38 +08:00
youpaishidifu 2c9aa6bd8e 修复领地主调整访客权限时管理员身份丢失:Web端perm_change现同步role到本地,且updateVisitorPermFromWeb不降级已授权管理员 2026-07-10 00:15:53 +08:00
youpaishidifu 89b3167d67 修复附魔书显示名罗马数字V解析失败:ROMAN_PAT无法匹配单独V,导致锋利V等五级附魔书被发成一级 2026-07-09 23:21:27 +08:00
youpaishidifu 2e869a1c94 修复测试场和平模式不刷怪:测试场难度提为非和平改为权威helper(创建/开始/离场三处调用),不再依赖startTest入口 2026-07-09 23:15:08 +08:00
youpaishidifu 9d8d70f63f 修复竞技场断线重进绕过公平限制(上线遣返出生点);backup命令放开控制台执行(支持指定玩家);测试场装备自由选武器/护甲档位;食物shift点击±16 2026-07-09 22:59:48 +08:00
youpaishidifu e03db026b9 修复PVP竞技场/测试场掉线覆盖主背包:主备份改保留最近3份且竞技场内跳过写入,新增/sdf1_login backup玩家自助还原(最近3份任选) 2026-07-09 22:29:02 +08:00
youpaishidifu 36b8192dc9 测试场装备对齐主场(可调熟牛肉/去金苹果)并禁止玩家互殴,只能打练习生物 2026-07-09 22:28:56 +08:00
youpaishidifu 2c12bbde8b PVP装备系统改版:铁剑+铁斧+可选盾牌+可选附魔(锋利V+击退II),移除护甲;PVP竞技场首位玩家定死附魔后续仅可调盾牌与熟牛肉数量;测试场全员自由选 2026-07-09 21:59:11 +08:00
youpaishidifu 9b725791aa 封堵容器管理禁用时访客越权开箱
onInventoryOpen 在 denyContainer=false 时直接放行所有人,
导致完全无领地权限的外部玩家也能开箱。
现于 denyContainer 判定前拦截 getPermissionLevel 返回 null 的未授权玩家,
一律禁止访问容器;VISITOR 成员行为不变。
2026-07-09 20:58:02 +08:00
youpaishidifu 0b8feab3e5 修复领地管理员越权:setadmin改为限领地+OP认全局
- 根因:/protect setadmin|unsetadmin 给玩家加全局 ScoreboardTag(areaProtectAdminTag),使其在所有领地拥有管理员权限(跨领地越权);而 isAreaAdmin 又不认 OP,导致 OP 反而无全局权限
- 修复1: isAreaAdmin(Player) 增加 player.isOp() 判定,OP 即真正的全局领地管理员
- 修复2: setadmin/unsetadmin 改为限领地: /protect setadmin <领地名> <玩家> 仅在该领地写 role='admin'(DB),不再授予全局标签;unsetadmin 同理并兼容清除旧版遗留的全局标签
- getPermissionLevel 第3步 isLandAdmin(ac.name,...) 本就限领地,无需改动;GUI toggleAdmin 路径(归属校验+限领地)本就干净
- 注释更新:领地管理员严格按领地限定,绝不跨领地生效
2026-07-09 20:45:27 +08:00
youpaishidifu 4c100a0c9f PVP测试场:改为全清后才间隔10秒刷,全清给可点击回床链接
- 撤掉每10秒无脑刷的全局定时器,改为事件驱动:监听 EntityDeathEvent 反查练习生物所属玩家
- 玩家清空当前波次(无存活练习生物)后:发送提示 + 可点击超链接[/pvp back 返回床],并排程10秒后刷下一波
- 新增 mobOwner 反查表 + pendingRespawn 防重复排程;退场(离开/死亡/退出/跨世界)均清理 pendingRespawn
- /pvp back 子命令:在测试场先清理还原背包,再传送至床(出生点),无床则回主世界出生点
- checkAllLeft 改为无人时还原测试场难度(和平模式临时提困难语义保持)
2026-07-09 20:30:38 +08:00
youpaishidifu 5fa12cddbf PVP测试场:修复和平模式刷怪 + 死循环刷怪 + 回血食物
- 和平模式修复:主世界为和平时,有人在场则临时把测试场难度提为困难(HARD),保证亡灵生物不被移除;主世界非和平则跳过
- 死循环刷怪:Bukkit runTaskTimer 每10秒一波,遍历所有在场玩家各补一波
  - 单场景(近战/远程)每组3只;混合场景每组6只(3僵尸+3小白)
  - 全部退场自动停刷怪并还原测试场难度为当前主世界难度
  - 加单玩家存活上限(30)安全阀,防止无限堆积卡服
- 回血食物:进入即发放金苹果x16 + 熟牛肉x32
- AttributeModifier 改用非过时3参构造(String,double,Operation)
2026-07-09 20:15:29 +08:00
youpaishidifu 0ae5c8d16e 改用无限耐久帽子免疫太阳灼烧(替换防火效果)
- 撤销练习生物 FIRE_RESISTANCE 效果(实测仍灼烧)
- 改为给每只练习生物(僵尸/小白)戴一顶 Unbreakable 皮革帽:
  原版机制下亡灵生物有帽子时太阳灼烧先耗帽耐久,帽子永不复破即永久免灼烧
- 用 Attribute.ARMOR 属性修正(-1)抵消皮革帽自带1点护甲,纯挡太阳不增加伤害减免
- 帽子 setHelmetDropChance(0) 不会被打掉
2026-07-09 19:56:55 +08:00
youpaishidifu d7afdd2a47 修复PVP测试场亡灵生物白天被太阳灼烧(SUNBURN)的bug
- 生成练习生物(僵尸/小白)时挂载长时效防火(PotionEffectType.FIRE_RESISTANCE)效果
- 防火直接免疫太阳灼烧的着火伤害,亡灵生物不再白天自燃
- 仅作用于测试场练习生物,不影响玩家与其他世界
2026-07-09 19:46:09 +08:00
youpaishidifu ac92cdb31e 新增PVP练习测试场(pvp_test独立世界)与完善/pvp命令补全
- 新增 PVPTestManager:首次加载预生成30x30露天FLAT世界(pvp_test),worldborder边界+玻璃围墙+关闭自然刷怪
- /pvp test 打开GUI面板选择难度(简单/中等/噩梦)与装备(近战/远程/混合)
- 开始:备份背包、发放装备、传送、生成40血僵尸/小白(近战=僵尸/远程=小白/混合=两者)
- 难度固定伤害:简单(僵尸4/小白2)、中等(僵尸4/小白3)、噩梦(僵尸8/小白7)
- 噩梦难度被玩家击中随机走位;测试场敌对生物互不攻击(取消mob→mob目标与伤害、小白箭不伤友军)
- /pvp命令补全补齐 join/leave/exit/test 一级子命令
- /pvp leave|exit 统一经 Main.leavePVP 路由(测试场优先,否则竞技场)
2026-07-09 19:36:29 +08:00
youpaishidifu 4812c093ea 将 /ban-ip 命令重命名为 /banip,避免与原版 ban-ip 冲突
- plugin.yml 命令名 ban-ip → banip,usage 同步更新
- Main.onCommand 分发与 doBanIp 用法提示同步改为 /banip
- ChatFilterManager 内转发原版 ban-ip 自动封禁的逻辑保持不变(本就调用原版命令)
2026-07-09 18:09:18 +08:00
youpaishidifu 9916968152 新增 ban-ip/tempbanip 封禁命令与 whois/who 查询命令
- 新增 /tempbanip:临时封禁IP或玩家,与 /tempban 共用实现
- 新增 /ban-ip:永久封禁IP或玩家(支持 永久/forever/perm 标识)
- /tempban 与 /tempbanip 统一支持名字或IP,无需专门区分地址类型
- 新增 /whois:玩家自查(注册时间/最近登录/登录IP/禁言状态/理由/效期),
  普通玩家仅可查自己,OP或插件tag管理员或控制台可查指定玩家,
  唯独控制台禁止不带参数
- 新增 /who:查看在线玩家列表,限 OP/插件管理员/控制台
- ChatFilterManager 新增 getMuteExpire 取当前仍有效的禁言到期时间戳
- 封禁与查询权限统一采用 isPrivileged(OP + 插件tag + 控制台)
2026-07-09 17:51:12 +08:00
youpaishidifu 41d8ffce3d 新增/tempban临时封禁、/chat mute禁言、异地登录检测与账号冻结解冻
- DurationParser: 通用时长解析(时间戳/日期/相对时长)
- /tempban: Bukkit原生封禁(1秒~10年)
- ChatFilterManager: 持久化禁言(muted_until)+loadMutes启动加载
- 异地登录检测: onJoin异步通知PHP比对IP归属(ip9.com.cn)
- freeze/unfreeze: web_admin_changes通道执行/解除Bukkit原生封禁
- PVPArenaManager: 移植PVP全服连杀/终结播报逻辑
- DatabaseManager/plugin.yml: 风控列 + tempban命令注册
2026-07-09 17:09:35 +08:00
youpaishidifu 2e417c67b3 调整商店出售限额为部分出售,并修复单件附魔书NBT重建
- 出售限额改为部分出售:超出限额只卖限额内部分,背包剩余不动
- sellItem/sellAllOf 引入 getMaxSellableUnits 计算可卖单位数
- 新增 removeItemsOfMaterial 按材质精确移除指定数量,保留背包剩余
- 修复单件PHP购买附魔书(itemStack为null)按商品ID权威重建NBT,避免整单退款
2026-07-09 13:59:11 +08:00
youpaishidifu 626fbcd591 附魔书NBT:移除空壳兜底,解析失败则整单取消并退款(含单笔购买退款) 2026-07-09 13:32:50 +08:00
youpaishidifu e4e74f1cb8 修复三处Bug:附魔书NBT空壳(ID权威重建)、PVP死亡榜世界未销毁不清战机、TPA processedRequests永不清导致秒过期 2026-07-09 13:15:57 +08:00
youpaishidifu 8fd5b06960 修复雪球菜单可被丢弃导致永久丢失的问题
- 新增 PlayerDropItemEvent 拦截:玩家主动丢弃菜单雪球时取消丢弃并提示,雪球留在手中
- 调整 EntityPickupItemEvent:仅阻止生物(如恶魂)拾取菜单雪球,允许玩家自身拾取,避免任何掉落路径导致永久丢失
2026-07-09 11:17:06 +08:00
youpaishidifu 70bbdba16d 修复附魔书材料匹配失败+购物车发货失败自动退款
- ShopManager: .md加载时对附魔书ID格式(material无效但id像MENDING_I)容错默认ENCHANTED_BOOK,不再静默跳过
- WebManager dispatchItemByMaterialOrId: 增加方式3附魔书兜底(名称含附魔关键词时直接getShopStackByName)
- WebManager shop_cart: 发货跟踪failedItems列表,任一商品失败→不confirm交易+writeRefundTransaction写回PHP退款
- WebManager: 新增writeRefundTransaction方法通过sync.php写退款记录
- PHP sync.php: 新增write_shop_refund action(接收Java退款请求,恢复bond_cache+写refund交易+标记原交易refunded)
- PHP shop.php buy_cart: 扣款前验证每件商品material有效性(防止扣款后无法发货)
2026-07-09 10:54:35 +08:00
youpaishidifu dfd8fee381 优化PVP世界创建:关闭结构生成+简化安全出生点,消除主线程长时卡服
- createPVPWorld: 加 c.generateStructures(false),去除村庄/神庙/矿洞等结构生成(出生点搜索时单区块十几秒卡死的主因);hill/water/cave/tree 自然地形特征不受影响

- forceSafeSpawn: 删除半径8广域扫描(289列同步区块生成),改为仅检查Minecraft已生成的(0,0)出生列,不安全才原地搭3x3小平台(O(1)零额外生成)

- 预生成半径 4->2(25区块),进一步缩短主线程占用
2026-07-09 00:37:12 +08:00
youpaishidifu 7a14457713 修复PVP装备确认后被误遣返及榜单残留
- onInventoryClose: 关闭事件判定同时检查 equipmentConfirmed,避免确认装备后 selectedTier 已移除导致误判未选择而遣返

- 遣返路径补 cleanupPlayerStats(player),修复回到主世界仍携带PVP榜单

- joinArena: createPVPWorld 移入 runTask 下一tick执行,/pvp join 命令即时返回,不再冻结命令线程
2026-07-09 00:18:49 +08:00
youpaishidifu a8451a4ccf 修复PVP chunk预生成:异步改同步分批,消除AsyncCatcher全拦截
- preGenerateSpawnChunksAsync: runTaskAsynchronous → runTaskTimer 同步分批(每tick一行chunk)
- 原因:Paper AsyncCatcher 强制要求 chunk 操作在主线程,原异步方案导致81个区块全部失败
- 新方案:radius=4 时 9 tick 完成(每行 9 chunk),均摊不冻服,自取消用 BukkitTask[] 闭包
- 日志清理:SFTP /logs 删除50个6月旧日志,保留59个7月日志
2026-07-09 00:05:10 +08:00
youpaishidifu 2df8c44cda 传送请求改为纯内存存储并改用JVM运行时长计时
- TeleportManager: 移除 saveTeleportRequest 及全部 DB 读写回退,传送请求仅驻留内存
- 计时改用 ManagementFactory.getRuntimeMXBean().getUptime() 单调钟,不再依赖墙钟时间戳
- 新增 teleportRequestTypes 内存映射记录 tpa/tpahere/tpaall 类型,移除时同步清理
- DatabaseManager: 删除不再使用的 teleport_requests 表
2026-07-08 23:49:47 +08:00
youpaishidifu 46a3dc2515 修复传送请求过期key不对称:incoming循环/清理统一为sender:receiver,确保接收方请求能被定时器正常清理 2026-07-08 23:17:13 +08:00
youpaishidifu 9fab58afe8 修复:回退PVP异步创建世界(改用主线程NORMAL避免IllegalStateException崩溃),保留真实自然地形 2026-07-08 23:00:50 +08:00
youpaishidifu 33ea4292b7 修复PVP地形与日志:改回NORMAL真实自然地形(异步创建避免主线程卡服),ConfigManager改用Bukkit.getLogger()消除System.out警告 2026-07-08 22:50:58 +08:00
youpaishidifu f4c5c0049a 修复PVP加入卡服(13s看门狗)并新增公共设施连控权限
- PVP世界改由 WorldType.FLAT 空世界+手动地形生成:createWorld 毫秒级返回,
  彻底杜绝主线程13s卡服与 Chunk wait 看门狗报错;随机种子仍保证每次地形不同(防背图);
  新增 forceSafeSpawn 固化安全出生点,绕开原'水→删除重建换种子'卡服链路
- 公共设施连控:领地 isPublicBuilding 启用时,所有访客自动获得 传送/容器交互/攻击敌对生物
  3项权限(传送在 canTeleportToLand,容器交互在两处 denyContainer 检查,攻击敌对生物复用既有逻辑)
2026-07-08 20:58:41 +08:00
youpaishidifu ee22930efd 第20轮修复:①移除WorldCreator.keepSpawnInMemory(false)(Paper26.x已移除此API,导致/pvp join NoSuchMethodError崩溃)②访客授权GUI/CLI补全传送权限(openVisitorPerm/getPermList/toggle三处均缺传送项,导致4页翻完无传送选项);AreaConfig新增allowVisitorTeleport字段+DB自动迁移ALTER TABLE+canTeleportToLand检查该字段 2026-07-08 20:41:09 +08:00
youpaishidifu 612ec7a66b 第19轮PVP修复:彻底移除模板世界方案(unloadWorld删目录导致无法加入),改为每次随机种子createWorld+出生点实体方块校验(水/空气等非实体则删除重来换种子),预生成区块改为异步不卡服;防背图逻辑恢复为玩家离开删世界、加入时非实体出生点自动换种子 2026-07-08 20:14:36 +08:00
youpaishidifu b6fb3c3ab2 第17轮修复4问题:①PVP模板世界改为懒加载(从onEnable移除,首次/pvp join时按需生成,消除启动卡50秒)②修复模板不存在导致无法加入PVP(ensurePVPTemplates先创建成功再加列表+复制前校验目录存在+递归重试)③confirmTransaction改同步调用+本地confirmedTxIds去重+PHP超时60s→3600s三管齐下修复重启重复发货bug;PHP端pull_pending_transactions不再选中processing状态交易 2026-07-08 19:43:48 +08:00
youpaishidifu 5ed006538e 修复PVP战绩榜与卡服、领地传送权限:记分板改用1.21.4三参registerNewObjective,击杀事件改用DamageSource.getCausingEntity()替代已移除的getKiller();PVP世界改为模板复制避免实时生成地形卡服;领地新增传送权限控制(领地主/插件管理员/领地管理员无条件,未授权玩家需allowTeleport授权,CLI/GUI可授权);药水效果改用setBasePotionType 2026-07-08 19:27:11 +08:00
youpaishidifu dec41c7b67 修复PVP加入崩溃 + 合并小票潜影盒(兼容中文标签)
1. /pvp join 崩溃(NoSuchMethodError: WorldCreator.keepSpawnInMemory):
   运行时 Paper API 已移除 WorldCreator.keepSpawnInMemory(boolean),
   移除该 Creator 调用,改由 reapplyWorldRules 的 World.setKeepSpawnInMemory(false)
   (稳定方法)控制出生点区块不常驻,避免同步加载卡主线程,且不再抛 NoSuchMethodError。

2. 收银台不打包(塞背包)仍给小票潜影盒:
   PHP(api/shop.php)推送小票时 settlement 误传中文标签(塞背包(环保单)/潜影盒打包),
   导致 Java give_receipt_book 的 equals('backpack') 守卫不匹配而仍然单独发书。
   修复:PHP 改传原始代码($settlement);Java 守卫同时兼容原始代码与中文标签
   (含'潜影盒'/'背包'即拦截),双保险——两种结算模式均不再单独发放小票潜影盒
   (打包:小票已在商品盒内;不打包:跳过发书)。
2026-07-08 17:34:04 +08:00
youpaishidifu 8b03ff4dcb 修复PVP崩溃/卡死 + 合并小票潜影盒 + ShopManager类缺失防护
1. PVP加入世界 WorldInitEvent 异步崩溃:createWorld 必须在主线程执行
   (WorldInitEvent/WorldLoadEvent 仅允许同步触发)。改为主线程同步创建,
   并通过 WorldCreator.keepSpawnInMemory(false) + 取消 reapplyWorldRules 的
   setKeepSpawnInMemory(true),使 createWorld 不再同步加载出生点区块,
   主线程毫秒级返回,彻底消除 Watchdog 卡死(上一版误用异步线程导致崩溃)。
   移除无用的 pvpWorldCreating 异步互斥锁。
2. 收银台合并发货:打包(潜影盒)模式不再单独发放'小票潜影盒',
   小票已塞入商品潜影盒内;不打包(塞背包)模式跳过发放小票书。
   解决玩家拿到两个潜影盒(商品+小票 / 仅小票)的问题。
3. 构建时确保 ShopManager$1 等匿名内部类完整打入 JAR,避免 NoClassDefFoundError。
2026-07-08 17:06:14 +08:00
youpaishidifu 2f29f3beb1 修复PVP:选装备被误遣返 + 加入世界主线程卡死
1. 装备确认/一键装备分支关闭GUI前加 guiReopening 标记,
   避免 Paper 延迟关闭事件在 selectedTier 已移除后误判为未选装备而遣返。
2. joinArena 的世界创建(Bukkit.createWorld 阻塞~8s)移到异步线程执行,
   主线程仅负责传送与打开装备GUI,修复 Watchdog 卡死;
   新增 pvpWorldCreating 互斥锁防止并发 /pvp join 重复创建世界。
2026-07-08 16:30:38 +08:00
youpaishidifu 2d84153571 订正NBT退款:移除散装退货退款路径中残留的旧签名调用,统一走OrderItem精确匹配收回实物 2026-07-08 15:25:57 +08:00
youpaishidifu ebd35f5c1c 修复三处问题:1.PVP创建看门狗超时(预生成区块改异步非阻塞) 2.NBT商品快速退款仅退款(OrderItem携带NBT快照并精确匹配收回实物) 3.订单退款校验支持NBT精确计数 2026-07-08 15:19:00 +08:00
youpaishidifu 0eca13f5a1 修复PVP种子忽略/退款仅退钱/潜影盒小票丢失三问题
PVPArenaManager:
- PVP世界改为每次随机世界名(pvp_arena_<随机数>)+随机种子,彻底绕开Windows文件锁导致旧世界目录删不掉、复用旧小岛地形的问题
- 删除deleteExistingPVPWorldFolder,改用deleteWorldFolderIfExists(String) best-effort移入trash;15处PVP_WORLD_NAME引用改为动态pvpWorldName
- cleanupOldTrashWorlds前缀匹配改用pvp_arena_前缀

OrderManager:
- 修复管理员审批全额退款'两段处理'bug:原逻辑status提前变2导致退货分支永不可达,表现为只退钱不退货
- 改为status==0时同步收回物品(潜影盒按lore标签#orderId定位清除,普通物品按mat/qty移除)后再addBonds

WebManager:
- 修复打包发货小票落入被丢弃盒子副本:改为先addBookToShulker塞入首个潜影盒,再整体addItem进背包
2026-07-08 13:53:52 +08:00
youpaishidifu e658aa3274 修复6项问题:#804菜单[玩家名]方括号占位符、#805 PVP装备选择宽限期防误遣返、#808收银台附魔书NBT重建、#809商店退货精确扣货 2026-07-08 12:34:44 +08:00
youpaishidifu 121c12c71d 修复3个问题:#801菜单玩家名中文占位符 #802 PVP种子被MC忽略 #803 .web.md同步PHP
#801 菜单控制台指令+玩家名变量(Main.java 两处处理器)
- 原代码只匹配英文 {player} 等模式,实际菜单用中文 {玩家名}
- 新增5种中文占位符支持:{玩家名} %玩家名% <玩家名> [玩家名] (玩家名)
- 控制台权限检测本身正常(日志确认走了console路径)

#802 PVP世界种子固定问题(PVPArenaManager.java createPVPWorld)
- creator.createWorld() 改为 Bukkit.createWorld(creator)(完整Bukkit管线更可靠处理seed)
- 新增种子验证:创建后比对期望种子 vs MC实际种子,日志明确报告是否匹配
- 种子不匹配时输出警告(Paper 26.x 已知行为,需观察 Bukkit.createWorld 是否解决)

#803 .web.md PHP专属商品同步(ShopManager.java buildCatalogJson)
- loadCategories() 继续跳过 .web.md(游戏内不加载,行为不变)
- buildCatalogJson() 新增第二遍扫描:单独读取 .web.md 文件并纳入同步JSON
- PHP 收银台现在能展示 .web.md 中的专属商品
2026-07-08 11:42:15 +08:00
youpaishidifu 7631e30e2a 修复: PVP显式随机种子(根除固定-5092种子) + 菜单控制台指令+玩家名变量 + 商店跳过.web.md(PHP专属商品)
- PVPArenaManager: createPVPWorld改用worldSeedRandom.nextLong()显式设定随机种子(用户实测连续多次createWorld返回相同种子-5092048402667366605);增加删除后验证日志(确认目录消失或告警)
- Main.java(两处菜单点击): 新增permType提取→控制台/console身份执行(Bukkit.getConsoleSender);玩家名变量替换{player}/%player%/<player>/[player]/(player)等所有括号形式
- ShopManager: loadCategories文件过滤器增加!.web.md条件,游戏内不加载.web.md文件(PHP端专属商品)
2026-07-08 10:57:46 +08:00
youpaishidifu 3c15e6b060 修复PVP地形被切小岛/出生点干预、药水NBT缺失,以及商店附魔书NBT解析
- PVPArenaManager: 删除forceTerraformSpawnArea等184行小岛死代码;新增deleteExistingPVPWorldFolder确保每次进入都是自然生成的纯净主世界(彻底消除旧版setSpawnLocation/小岛误加载);pvpEquipment与makePotion改用PotionMeta写入INSTANT_HEALTH真实治疗NBT(1.21.4 PotionType.HEALING)
- ShopManager: 新增ENCHANT_MAP与inferEnchantedBook,按中文名解析附魔等级并写入EnchantmentStorageMeta,商店附魔书现在带正确附魔NBT
2026-07-08 01:37:46 +08:00
youpaishidifu 3c3d7cdbf2 修复PVP世界逻辑:不再锁定出生点、去除删世界重建死循环、有玩家直接加入无人即删 2026-07-08 01:05:51 +08:00
youpaishidifu caa58977a4 修复: green_discount语义从减免百分比改为折扣率(折数)——10不打折/9.9=9.9折(支付99%),命令/计算/显示文案全链路修正 2026-07-08 00:18:26 +08:00
youpaishidifu e18587c432 修复: packmoney和green_discount全面支持小数(int→double,去掉Math.floor截断),PHP侧packmoney强转int改为float 2026-07-07 23:54:20 +08:00
youpaishidifu 6bf3fceed6 修复: PVP世界生成改为每次强制删除现有世界后重新生成全新主世界(彻底杜绝复用小平台/环形岛),移除无用的正版标记逻辑 2026-07-07 23:39:37 +08:00
youpaishidifu ebece189e1 修复: PVP世界生成加诊断日志+遗留世界识别强制重置(环形岛/小平台不再复用) + 正版世界标记 2026-07-07 22:43:51 +08:00
youpaishidifu a5a63dc5d8 修复: PVP世界生成改为仅检测出生点非实体方块则删除重建(插件放手不干预地形); shop子命令新增packmoney/green参数; 彻底移除商店.md文件运行时复写(onDisable与定时任务不再saveAll, saveCategory改为空操作) 2026-07-07 22:14:24 +08:00
youpaishidifu 4297e0e02d 修复:PVP世界改为纯NORMAL主世界(插件零干预地形,仅只读定位出生点);商店保存改为合并感知,避免服务器关闭时覆盖管理员手工新增的商品/分类 2026-07-07 21:25:57 +08:00
youpaishidifu 3ebd8f0d5f 修复: PVP世界改FLAT+噪声圆形岛根除方形切割区块丢失 + 商城目录定时全量同步给PHP + 装备GUI响应优化(延迟20->5tick) 2026-07-07 19:40:47 +08:00
youpaishidifu 7c8c7f7818 修复: PVP世界种子重试机制+强制造陆兜底,根除小平台地形 2026-07-07 16:53:44 +08:00
youpaishidifu 49461ac042 修复: PVP世界旧虚空世界Windows删除锁致地形失效(重命名回收站强制刷新NORMAL地形) + /weblogin命令主线程同步HTTP卡服改为异步 2026-07-07 14:26:45 +08:00
youpaishidifu dd7166d73e 修复: PVP加入即被遣返(跨世界强制关背包误触遣返) + 打包小票书改为潜影盒包装并改用黑色文字 2026-07-07 13:37:50 +08:00
youpaishidifu 6d655787ce 功能+修复: PVP地形陆地优先搜索+打包小票 Written Book
PVP地形 (PVPArenaManager.java):
1. findSafeSpawn 大幅改进: 搜索半径32→64格, 新增陆地质量评分
   系统(草地3>沙子2>其它1), 海洋biome自动远距离扫掠找陆地
2. 区块预生成半径4→8(256格), 减少虚空可见范围
3. 保底平台扩大20x20 + 围栏 + 四角萤石标记

小票书 (WebManager.java):
4. 新增 give_receipt_book 变更类型处理(pollAdminChanges拾取)
5. 新增 giveReceiptBook() 方法生成 Written Book(§颜色热敏小票风格)
6. 书本含订单号/时间/玩家/操作员/结算方式/商品明细/合计多页
2026-07-07 12:28:40 +08:00
youpaishidifu d18884a3ee 修复: PVP装备选择彻底改判(已选档位则自动重开GUI不遣返) + 60秒超时安全网 2026-07-07 11:37:57 +08:00
youpaishidifu 751eeecfd5 修复: PVP装备选择误遣返(新增GUI打开宽限期3秒忽略地形加载期间的InventoryClose) 2026-07-07 10:39:35 +08:00
youpaishidifu 79b5af66b2 修复PVP装备选择误遣返并改用自然主世界地形生成 2026-07-07 00:33:24 +08:00
youpaishidifu 164267208a 商店打包费/环保减免配置化与收银台双模式收款
- ConfigManager 新增 packingFee/greenDiscount 公共配置字段
- ShopManager 颜色选择框对齐原色(SHULKER_BOX),打包费改读配置;新增 shop setgreen 命令
- WebManager 购物车结算支持现金模式(跳过扣债券),小票书塞入首个潜影盒;新增 pullShopConfig/pushShopConfig 与定时器拉取
- Main 新增 set packmoney 命令,修复 getConfigMgr()/webManager 访问方式
- 修复 openColorSelect 名字错乱(索引错位)与硬编码打包费显示
2026-07-06 23:51:01 +08:00
youpaishidifu 4cdd32913e 修复PVP背包被覆盖问题(独立备份表+未退场不覆盖)、优化随机地形生成、TPA传送请求有效期下限保护 2026-07-06 21:48:43 +08:00
youpaishidifu 122850f6f4 修复PVP竞技场三项问题:世界重建、进入流程与背包序列化
- 加入世界版本检测机制,磁盘残留的旧版小平台世界自动删除并重建为完整主世界地形
- 补全 PlayerChangedWorldEvent 事件处理器,进入/离开PVP世界时正确触发装备选择GUI与背包还原
- joinArena 传送后立即直接触发进入流程,杜绝跳过选装备清背包阶段
- 实现背包物品序列化(BukkitObjectOutputStream+Base64),真正支持清背包后还原玩家物品
2026-07-06 20:01:49 +08:00
youpaishidifu 035dd418fc 修复PVP世界生成小平台问题:先强制生成出生点区块再寻找安全点,避免误判虚空铺设漂浮平台(#735) 2026-07-06 19:27:03 +08:00
youpaishidifu 24e5248cec 新增maxhome全局传送点上限参数,并联动用户组home_limit(#726)
- AreaProtection新增max_home_per_player全局配置,复用现有area_config/web_area_config通路
- updateAreaConfig同步刷新内存字段,Web后台修改后立即生效
- HomeManager.getGlobalHomeLimit改为读取area_config,解析失败或<=0回退默认5
- 用户组home_limit(>0覆盖/0跟随/-1无限)逻辑不变,形成完整联动
2026-07-06 18:10:06 +08:00
youpaishidifu 28198114e6 修复购物车结算潜影盒颜色变量作用域;重构PVP竞技场为独立主世界(装备选择+背包备份还原);新增商品价格同步接口 2026-07-06 17:33:15 +08:00
youpaishidifu f0b0f23827 修复PVP竞技场无法进入:新增/pvp join进入与/pvp leave离开指令,世界生成时铺设出生平台防止掉虚空 2026-07-06 16:48:33 +08:00
youpaishidifu d62b61ef51 新增购物车结算shop_cart类型处理:Gson解析多商品detail,支持塞背包和潜影盒打包两种发货模式,含CartEntry内部类及辅助方法 2026-07-06 16:39:47 +08:00
youpaishidifu 273a462cd3 商品价格同步API:PHP admin改价→Java拉取应用
PHP端:
- core.php: shop_items表新增admin_buy_price/admin_sell_price列
- admin.php: adminShopUpdate()支持修改buy_price/sell_price,写入admin覆盖字段
- sync.php: 新增pull_shop_prices/clear_admin_prices/check_shop_prices_changed接口

Java端:
- ShopItem: buyPrice/sellPrice改为非final,添加setter
- WebManager: 新增pullShopPrices()/clearAdminPrices()/parseJsonObject()
- 所有定时器集成pullShopPrices调用

附带修复:
- Main: handlePVPArenaCommand重定向到pvpManager.onCommand
- PVPArenaManager: 适配Paper 1.21.4 ChunkGenerator API
- DatabaseManager: 新增getInventoryBackup(String)方法
- ShopItem: 添加setBuyPrice/setSellPrice
2026-07-06 15:49:13 +08:00
youpaishidifu 893b431757 修复:非法URL检测误踢含下划线玩家名(如Wings_of_Ashes)
- Main.java: 移除用户名的extractUrls检查,仅保留containsIllegalDomain
- URL_PATTERN的下划线分隔域名模式会误匹配合法MC用户名
- containsIllegalDomain已能正确检测非法域名后缀(baidu_com等)
2026-07-06 13:20:55 +08:00
youpaishidifu e847a72079 修正门禁交互分类:按钮/压力板移至红石电路,门禁只控制门/栅栏门 2026-07-06 13:03:33 +08:00
youpaishidifu 18000efb62 修复门禁交互和容器交互权限bug:DoubleChest容器检测+移除无权限级别拦截 2026-07-06 12:41:29 +08:00
youpaishidifu b403e4298d 修复<br>标签导致颜色变白和交易同步缺少id字段 2026-07-06 10:57:43 +08:00
youpaishidifu 6d40243fd2 修复HTML标签下划线切断:<u>/<b>/<i>标签内使用§r切断,防止样式溢出 2026-07-06 10:42:07 +08:00
youpaishidifu 8a54123d3a 修复HTML标签处理:移除<u>标签的下划线效果,只保留文本内容 2026-07-06 10:35:38 +08:00
youpaishidifu a306a00908 修复<b><u><i>标签处理:移除§r结尾,让<p>color统一控制整段文字颜色 2026-07-06 00:01:38 +08:00
youpaishidifu b901a72140 修复HTML颜色标签正则表达式:允许引号和尖括号之间有空格 2026-07-05 23:17:34 +08:00
youpaishidifu cc8727a2f9 扩展颜色名称映射:支持chartreuse、orange、pink、brown等CSS颜色名称,修复正则表达式匹配分号 2026-07-05 22:58:55 +08:00
youpaishidifu 57e9ba46a0 修复HTML颜色标签解析:支持rgb/rgba/hsl/hsla颜色函数 2026-07-05 22:48:04 +08:00
youpaishidifu 0c3478d1ff 修复出售限额提示关闭后仍打印消息bug,新增颜色代码和HTML标签支持 2026-07-05 22:24:46 +08:00
youpaishidifu ba0b7fb879 修改快速返回冷却时间+移除广告机挂机消息 2026-07-05 18:05:40 +08:00
youpaishidifu 3118f9bd1a @2026-07-05 00:40 Java修复: 1) 验证码核心修复-回答错误不再永久放行玩家,保持PENDING状态重试 2) URL正则扩展支持_.混合格式 3) 过期验证码不自动放行 4) 编译成功 2026-07-05 00:44:49 +08:00
youpaishidifu f18e7485c7 @2026-07-05 00:30 Java修复: 1) N+1验证码核心修复-回答错误不再永久放行,玩家需持续答对才能验证通过 2) 验证码过期后仍需重新验证(不自动放行) 3) URL正则扩展支持_.混合格式如123_ypshidifu.cn 4) GUI验证码跳过聊天答案 5) 编译成功 2026-07-05 00:35:11 +08:00
youpaishidifu 8b147b2531 @2026-07-04 23:40 Java修复: 1) illegal_domains.txt 热更新恢复-每次玩家加入时重读配置文件 2) N+1验证码机制-pendingMessages改为List缓存列表,答对第N题释放N条消息 3) 非法域名Kick抑制join/quit消息 4) onEnable自动释放illegal_domains.txt到插件目录 5) 编译打包通过 2026-07-04 23:43:55 +08:00
youpaishidifu b1ea407f3f 修复: 验证码GUI关闭逻辑 + Inner Class NoClassDefFoundError + 重复注册问题 + deploy脚本增强 2026-07-04 21:46:03 +08:00
youpaishidifu c9e740d4c9 fix: 非法域名支持underscore格式+GUI关闭拦截+验证码关闭后重抽题+修复内联类可见性 2026-07-04 19:45:27 +08:00
youpaishidifu a716424dd5 fix: 验证失败丢弃消息不发出 + 非法域名后缀拦截 + 验证码随机化 + 配置文件热更新 2026-07-04 18:33:48 +08:00
youpaishidifu cb37eceb9b feat: 玩家加入时拦截含URL格式的名称 + 修复Main.java冗余代码 2026-07-04 18:02:46 +08:00
youpaishidifu 2618cc5d80 fix: GUI验证码中文提示+标题显示物品名+失败后重新抽题+防止拖拽 2026-07-04 17:18:27 +08:00
youpaishidifu bca19bf3d0 fix: GUI验证码标题中显示目标物品名(如点击绿宝石完成验证) 2026-07-04 16:49:30 +08:00
youpaishidifu 7f01f75343 fix: GUI验证码改为精确物品匹配+5~10个物品+30秒超时 2026-07-04 16:26:19 +08:00
youpaishidifu 5edbcb57c3 fix: 修复验证码逻辑、URL过滤拦截链、传送无限次接受问题 2026-07-04 16:07:38 +08:00
youpaishidifu d5cebbf6ad feat: 完善GUI验证码点击验证逻辑 + 修复ChatFilterManager checkAnswer路由 2026-07-04 14:52:30 +08:00
youpaishidifu afa53876ad feat: 实现新玩家验证码系统(数学题)+全场景链接过滤+私信URL拦截 2026-07-04 13:34:16 +08:00
youpaishidifu c2cf7ff220 fix: 1.TPA请求过期误报:tpaccept/tpdeny双重修复——内存路径先查有效性再处理,DB fallback路径加时间戳校验+恢复双方向key 2.赠送用户组:送礼人付费,收礼人免费获得 2026-07-04 00:21:42 +08:00
youpaishidifu 91b03fd127 fix: 赠送用户组扣钱逻辑修正,送礼人付费而非收礼人付费。新增giftGroupToPlayer方法,送礼人扣费,收礼人免费获得用户组(购买/续费) 2026-07-03 23:56:23 +08:00
youpaishidifu 8da0ac68c8 fix: 1.用户组同步保留expiry_time,PHP→Java同步时不丢掉到期时间 2.Java/PHP到期解析:数字=分钟,中文单位秒数计算修正为秒级 2026-07-03 23:36:50 +08:00
youpaishidifu 99d83f1b07 fix: 修复tpaccept路由被isValidRequest提前拦截导致4秒也报过期。tpaccept现在先确定targetName再查数据库时间戳校验,不再依赖teleportRequestTimes内存 2026-07-03 23:11:24 +08:00
youpaishidifu 5e10cbf420 fix: 修复过期请求key不匹配bug。tpa/tpahere/tpaall全部统一用 sender:receiver 键名,清理定时器/key查找/isValidRequest/key对比统一去掉-out后缀 2026-07-03 23:07:04 +08:00
youpaishidifu 729e1383c1 修复tpa请求误报过期bug + PHP到期时间智能解析 2026-07-03 22:40:24 +08:00
youpaishidifu 2cc8490016 修复传送请求永不过期bug:新增每秒定时检查过期请求的定时器,自动清理超时/已处理的请求,防止重复接受/拒绝 2026-07-03 21:49:31 +08:00
youpaishidifu 41b50f7ee8 fix: QuickBack新增shutdown()方法,插件停用时取消定时任务避免异步泄漏 2026-07-03 20:25:52 +08:00
youpaishidifu fc495802f2 feat: 新增死亡报告与快速返回功能 - DeathReport记录死亡坐标并发送消息 - QuickBack每分钟记录坐标支持/back传送 - 更新plugin.yml添加back命令 2026-07-03 19:24:57 +08:00
youpaishidifu abaf54470c fix: 1.出售限速记录时机提前到扣货发钱之前 2.用户组删除/添加成员分别处理 3.sellAllOf也加限速 2026-07-03 18:11:03 +08:00
youpaishidifu 53a7430184 feat: 商店出售全局IP段限速(每小时1.5万债券) 2026-07-03 17:01:55 +08:00
youpaishidifu 56e53a1b07 fix: PVP全局生效修复 + 用户组DB同步删除 2026-07-03 16:23:42 +08:00
youpaishidifu 7eb350a8da fix: PVP/伤害权限双重检查修复 + PHP删除用户组同步
1. PVP/伤害不生效: onEntityDamage中对PVP玩家先检查denyAllDamage再检查denyPVP,避免重复getArea调
2. PHP删除用户组: handleDeleteUserGroup先删成员表再删组表,确保同步完整
2026-07-03 14:28:02 +08:00
youpaishidifu 3d9475ccec fix: PVP/伤害权限修复 + 到期自动清理
1. getEffectiveDeny(): denyPVP和denyAllDamage不再豁免ADMIN/OWNER
2. 移除重复的onEntityDamageByEntity PVP逻辑
3. Timer E新增checkAndRemoveExpired()自动清理过期成员
2026-07-03 13:41:11 +08:00
youpaishidifu 4a28287dec feat: 用户组GUI管理界面 + validation_callback自动执行 + 自动续费偏好
- 用户组GUI: /group无参打开GUI, 可购买/续费/赠予/切换自动续费
- PHP validation_callback: 验证通过后自动执行add_group_member/add_visitor
- DatabaseManager: 新增player_auto_renew表(get/setAutoRenew)
- CDKManager: 新增gift_target输入处理
- GUIManager: 新增openUserGroup/handleUserGroupClick/handleGiftTargetInput
2026-07-03 11:43:12 +08:00
youpaishidifu 7e1dbf26c8 fix: GarbageManager TypeToken NPE + Vault依赖移除 + sync_login secret修复 + AreaProtection空值检查 2026-07-03 11:09:47 +08:00
youpaishidifu e9724fd47e fix: 购买用户组后立即pushMemberToPHP写入PHP后端 2026-07-03 01:21:56 +08:00
youpaishidifu dcd1170851 fix: pushMemberToPHP改用_java后缀action,确保PHP端能识别Java推送 2026-07-03 01:16:23 +08:00
youpaishidifu 40cafa4b74 fix: 点燃权限全面重构——统一使用getEffectiveDeny做豁免+去重方法
1. 所有点燃拦截统一使用getEffectiveDeny()进行OWNER/ADMIN自动豁免
2. 移除了重复的方法定义(onIgnite/onPlayerInteractFire/BlockIgniteEvent各两个)
3. onPlayerInteractFire: 拦截打火石/火焰弹/岩浆桶/火床
4. onPlaceCampfire: 单独拦截火床/灵魂火床放置
5. onIgniteBlock: 拦截玩家放置火把等非直接用物品点燃
6. onFireSpreadNonPlayer: 拦截非玩家触发的火蔓延(如TNT红石)
7. onEntityChangeBlock: 拦截末影龙/末影螨改变方块为火/岩浆
2026-07-03 00:58:30 +08:00
youpaishidifu 3ece4c8369 fix: 领地点燃权限修复OWNER/ADMIN豁免+补充岩浆桶/打火石拦截
1. onIgnite(onBlockIgnite): 加入OWNER/ADMIN豁免检查,避免误伤领地主
2. onPlayerInteractFire: 加入岩浆桶(MAGMA_BUCKET→LAVA_BUCKET)拦截
3. onPlayerInteractFire: 加入OWNER/ADMIN豁免
4. 清理冗余的TNT材质检查逻辑
2026-07-02 23:49:45 +08:00
youpaishidifu 106211564b 修复用户组购买反馈:发送在线玩家消息+同步扣费到PHP+推送成员变更到PHP数据库 2026-07-02 22:29:57 +08:00
youpaishidifu c9a3d24e64 修复领地禁止点燃:新增PlayerInteractEvent拦截打火石/火球/火焰弹,ProjectileHitEvent拦截烈焰球落地,彻底关闭TNT/篝火点燃 2026-07-02 21:58:19 +08:00
youpaishidifu 6f004f257f 修复领地禁止点燃:支持BlockIgniteEvent+EntityExplodeEvent+BlockExplodeEvent多层拦截,覆盖TNT/火床/烈焰弹等所有来源 2026-07-02 21:21:30 +08:00
youpaishidifu 46897f06af 修复领地禁止点燃:支持检查源/目标双领地,覆盖非玩家来源(TNT/闪电) 2026-07-02 20:01:23 +08:00
youpaishidifu fb871e886a 修复领地拾取/丢弃权限:OWNER/Admin不应被allowDrop=false限制,改用getEffectiveDeny路径 2026-07-02 19:35:27 +08:00
youpaishidifu 925017e21a 领地拾取/丢弃权限重构: denyPickup/denyDrop → allowPickup/allowDrop(正向命名),修复访客权限检查 2026-07-02 19:17:17 +08:00
youpaishidifu 2f35b945fc 修复传送系统和时间解析多个Bug
1. 修复tpaall/tpahere传送方向:tpahere改为B→A(executeTeleport(player,sender))
2. 自动接受传送仅对tpa生效:tpahere/tpaall不触发autoAccept,避免误自动传送
3. 自动接受时精简消息:开启自动传送后仅显示「已自动通过」,不再打印等待确认消息
4. 修复roman数字解析:parseRoman加toLowerCase()支持大写罗马数字
5. parseChineseTime支持罗马数字识别(如II分X秒→130秒)
6. 修复配置文件writeLines:删除错误的file.renameTo(tmp),防止Linux上配置丢失
7. 新增teleport_requests表建表语句:修复SQL no such table错误
8. handleTPAccept新增DB fallback:内存找不到请求时查数据库恢复
9. HomeManager清理废弃代码
2026-07-02 16:53:00 +08:00
youpaishidifu addc5563c6 ConfigManager: 恢复奖励发放方式默认值为债券;修复中文/英文数字解析叠加bug
- rewardChannel默认值从economy改回债券
- normalizeRewardChannel支持中英文全兼容
- 中文数字解析:修复current在total!=0时错误拼接
- 英文解析:修复currentNum在单位后未重置导致叠加
- loadSettings/saveSettings/createDefaultSettings全部同步恢复为债券
2026-07-01 19:36:30 +08:00
youpaishidifu 1e6d2419db 修复时间解析器叠加bug+移除债券支持改用经济+去重loadSettings 2026-07-01 19:01:30 +08:00
youpaishidifu 17b7513276 重构配置文件:loadSettings中追加+saveSettings原子写入+保留全量旧配置 2026-07-01 16:41:03 +08:00
youpaishidifu 698a6445ed 重构配置文件保存机制:原子写入+保留全量旧配置 2026-07-01 16:18:07 +08:00
youpaishidifu 36568cb31a 重写时间解析器:支持中文大写/罗马数字/英文复合格式+自动推断单位 2026-07-01 15:38:40 +08:00
youpaishidifu 5574749351 修复saveSettings()覆盖Web通信配置+新安装默认包含Web通信配置 2026-07-01 15:12:32 +08:00
youpaishidifu 6a6685bcec 修复用户组永久时长+中文数字时间解析 2026-07-01 14:30:13 +08:00
youpaishidifu 18da80c799 修复传送系统配置:1.管理员/tpa显示传送面板+配置区 2.配置按钮直接输入值(聊天拦截) 3.tpauto仅切换自动接受 4.PHP部署到/plugin/目录 2026-07-01 13:13:56 +08:00
youpaishidifu 6a7d3bd635 feat: 修复管理员tpa CLI交互式配置 + 修复home持久化问题(不再删除已有数据) 2026-07-01 12:00:21 +08:00
youpaishidifu 0bab938784 feat: 管理员tpa CLI交互式配置菜单 + 状态机管理 + 用户组权限修复 2026-07-01 11:21:19 +08:00
youpaishidifu c437851470 feat: 管理员tpa CLI面板添加全局配置显示和快捷命令 2026-07-01 10:59:12 +08:00
youpaishidifu 265503be38 feat: 传送系统增强 - 全局配置化有效期/冷却时间 + 灵活时间输入解析 + 请求过期清理 + HomeManager + UserGroupManager8个新方法 + WebManager/EconomyManager扩展 + 基岩版GUI三级导航 2026-07-01 00:37:11 +08:00
youpaishidifu c8e8fe88e4 基岩版GUI三级导航: 主面板→玩家列表(25人/页翻页)→玩家操作(tpa/tpahere/接受/拒绝)
1. Level1主面板(27格): 玩家列表入口、incoming数量、自动接收开关、全服传送、取消请求
2. Level2玩家列表(27格): 25个玩家头颅+左右翻页(slot 18/26)
3. Level3玩家操作(9格): 请求传他/请求他来/接受/拒绝
4. 点击事件按面板大小自动路由到对应层级处理
2026-06-30 13:16:01 +08:00
youpaishidifu 4f99b6af05 tpaccept/tpdeny无参直接通过/拒绝,不再弹出面板 2026-06-30 12:42:01 +08:00
youpaishidifu 6b78165019 修复传送系统3个bug: tpaall自动接受冲突/基岩版GUI拦截/autoAccept持久化
1. tpaall全服传送: autoAccept玩家直接传送不发通知,非autoAccept正常发请求
2. 基岩版GUI无响应: Main.java二级菜单白名单排除传送系统面板
3. autoAccept持久化: 新增teleport_auto_accept表,登录从DB加载,开关同步写入
2026-06-30 12:26:22 +08:00
youpaishidifu 24aa623f7a 传送系统v5:TeleportManager独立Listener+撤回超链接+基岩版仅tpa无参走GUI+调试日志 2026-06-30 11:19:36 +08:00
youpaishidifu 7c42f60709 修复基岩版GUI按钮无效:传送面板不受isFrozen登录冻结限制 2026-06-30 11:03:16 +08:00
youpaishidifu 45dbc01eae 传送系统v4:重写CLI完整操作面板(在线玩家列表+发起传送)、基岩版玩家跳过登录自动接收、基岩版GUI增加传送发起功能 2026-06-30 10:53:18 +08:00
youpaishidifu 3d3c830a6e 重写传送系统:修复tpacancel/tpauto无参路由、CLI完整面板(incoming+outgoing)、Java版可点击按钮、Tab补全区分命令类型 2026-06-30 10:37:13 +08:00
youpaishidifu 77c49a1fa5 添加传送系统调试日志:记录消息发送过程,帮助排查目标玩家未收到提醒问题 2026-06-30 00:56:31 +08:00
youpaishidifu 2cd56d637b 修复Geyser API检测:使用正确的GeyserApi.api().isBedrockPlayer(UUID)方法,添加Floodgate降级检测 2026-06-30 00:52:07 +08:00
youpaishidifu 90365bb3eb 优化Geyser检测:精简反射调用链,修复GeysonAPI拼写错误,清理冗余嵌套try-catch 2026-06-30 00:40:30 +08:00
youpaishidifu 2706c0aa2a 重构传送系统:Java版CLI交互+基岩版GUI面板、A传B时通知等待同意、Geyser自动检测、tpa支持序号快捷接受 2026-06-30 00:32:30 +08:00
youpaishidifu 141a29a39c 修复传送命令:注册plugin.yml命令到Executor和TabCompleter,解决指令无响应和Tab补全缺失问题 2026-06-29 23:47:48 +08:00
youpaishidifu 60ce93617b 修复传送系统:补全plugin.yml命令注册、添加Tab补全、修复GUI面板判断逻辑、登录联动自动接受传送 2026-06-29 23:32:39 +08:00
youpaishidifu c303849335 feat: 所有转让操作增加注册时间≥5分钟验证
- WebManager: 管理面板改主和玩家过户都验证register_time
- AreaProtection: 游戏内转让也验证register_time
- 防止注册秒退玩家接收领地,确保玩家是真实有效
2026-06-29 21:29:40 +08:00
youpaishidifu c41ef2eacc fix: 管理面板改主验证改为仅查login.db(不再用Bukkit缓存) 2026-06-29 21:05:40 +08:00
youpaishidifu ac8b23faff feat: 添加admin面板删除领地走pending队列(land_delete)
- WebManager: pollAdminChanges增加land_delete分支
- 验证领地存在→删除→回调PHP确认
- 取消过户按钮加粗显示
2026-06-29 20:44:03 +08:00
youpaishidifu bbbbf54b3f 添加改主心跳延时机制: PHP前端轮询1分钟,Java回调通知原主人,支持撤回 2026-06-29 20:06:17 +08:00
youpaishidifu fe4bee53de 修复CLI创建领地跳转 + 无参数快速创建
1. CLI主菜单和领地列表中的创建领地按钮从/protect cli create改为/protect 创建(CLI没有create子页面,旧指令无响应)
2. /protect 创建无参数时自动快速创建3x3领地,不要求先选点
3. /protect 创建有参数时自动选名然后走confirm_create流程
2026-06-29 18:42:30 +08:00
youpaishidifu fa9be74e1f 修复异步打开GUI报错 + 创建领地快速创建
1. 修复InventoryOpenEvent异步报错: handleConfigInput/handleAddMemberInput/handleEffectInput/handleGroupEditInput中所有areaGUIManager.open调用改为Bukkit.getScheduler().runTask切主线程
2. 首页创建领地: 无选点时自动以玩家为中心3×3快速创建领地,自动命名+冲突数字0~999
3. AreaProtection新增quickCreateLand/hasPos1/hasPos2方法
2026-06-29 15:48:31 +08:00
youpaishidifu c94fb60a86 修复过户GUI面板无作用、CLI用户组交互式编辑、过户结果超链接取消、PHP改主校验流程 2026-06-29 15:22:56 +08:00
youpaishidifu 0223ece81d refactor: 过户流程重构 - 验证+追踪+回调
1. activeLandTransfers Map追踪进行中过户
2. TransferInfo: landName/oldOwner/newOwner/expiresAt/permissionsSnapshot
3. pollAdminChanges: 区分source(player_transfer/transfer_cancelled/admin)
4. handlePlayerTransfer: 验证新所有者存在性→改DB→回调PHP→追踪cooldown
5. notifyTransferCallback: POST回调PHP transfer_callback接口
6. handlePendingTransferCancellations: 权限变更自动取消过户
7. /protect transfer: 加验证+cooldown追踪+通知PHP
8. /protect canceltransfer: 新增游戏内取消命令
9. getLandPermissionsSnapshot: 权限快照用于检测cooldown期间变更
10. Timer A: 新增transferCancellations调用
2026-06-29 12:13:38 +08:00
youpaishidifu 0949ae18b0 聊天前缀: 用户组成员发言自动加[组名]前缀(颜色由组配置决定)\n\n- AsyncPlayerChatEvent优先级NORMAL,在所有拦截之后、聊天过滤之前添加\n- 使用e.setFormat()注入§颜色前缀\n- 同步日志优化: pullUserGroupsFromPHP对比现有配置,仅在有变化时才打印+reload\n- UserGroupManager.loadGroupConfigs日志降级为fine(不再每次拉取都打印) 2026-06-29 11:05:21 +08:00
youpaishidifu a33757a465 修复: 密码验证统一使用checkPasswordWithFallback,消除重复checkPassword定义;LoginManager/main/web使用新方法 2026-06-28 23:12:20 +08:00
youpaishidifu f3362380a3 彻底移除Vault Economy支持 + 3个UI Bug修复
Vault移除:
- Main: 移除Economy字段/import/setupEconomy/签到经济奖励
- GiftManager: stage2/3/4 游戏币改为债券
- PointsManager: 积分兑换游戏币改为积分换债券
- QuestTracker: useEconomy分支移除,全部走债券
- CommissionManager/CdkManager/GUIManager: 所有Vault调用清除
- WebManager: autoGenerateWebLoginToken改为异步执行(修复10秒阻塞)

Bug修复:
1. 垃圾箱打不开: GUIManager.handleSlot对垃圾回收站点击的处理路径正确,无需额外修复
2. 余额操作返回->去管理员面板: slot 49改为plugin.openMyWallet(p)
3. 签到/WebLogin线程阻塞: WebManager异步化,不再阻塞Server Thread
2026-06-28 22:15:37 +08:00
youpaishidifu 47b21f31fc fix: addPlayer返回具体错误消息(替代笼统操作失败)
- UserGroupManager.addPlayer: 返回值从boolean改为String(null=成功)
  格式错误/玩家未注册/数据库错误 均返回具体中文提示
- AreaProtection groupadd: 显示addPlayer返回的具体错误
- AreaGUIManager: 同步修改addPlayer调用方式
2026-06-28 16:24:42 +08:00
youpaishidifu 3555f26583 feat: PHP玩家验证改为异步模式(PHP→pending表→Java轮询验证→推回结果)
- land_api.php: 新增 pending_player_validations 表
- validatePlayerViaJava() 改为异步:先查缓存→无缓存写入pending→返回null
- 新增 handleGetPendingValidations: Java拉取待验证列表
- 新增 handleValidationCallback: Java推送验证结果
- handleAddGroupMember/UpdateLandOwner/AddVisitor: 适配异步验证
- 改善错误提示文案(更友好的中文提示)
- WebManager.java: Timer A新增 pullPendingPlayerValidations 轮询
2026-06-28 15:59:43 +08:00
youpaishidifu 4fa9808ddc feat: PHP回调Java验证玩家存在性(login.db)
- WebManager.java: 回调服务器新增 /validate_player 路由
  接收POST {player, secret},查login.db返回 {exists: true/false}
  重构handleCallback为路径分发模式(notify_sync/validate_player/register_callback)
- land_api.php: 新增 validatePlayerViaJava() 函数
  PHP通过HTTP调Java回调服务器验证玩家,替代直接读login.db
- handleAddGroupMember: 替换直接读login.db为Java验证
- handleAddVisitor: 替换playerExists()为Java验证
- handleUpdateLandOwner: 替换playerExists()为Java验证
- config.php: 新增 GAME_SERVER_HOST 配置项
2026-06-28 15:29:58 +08:00
youpaishidifu 82242ed428 fix: pushMemberToPHP改用GET+secret + 玩家名格式校验(3-16位字母数字下划线) 2026-06-28 15:15:46 +08:00
youpaishidifu 8aa20c4d39 feat: Java↔PHP用户组成员双向同步 + 玩家存在性校验(login.db) 2026-06-28 14:27:04 +08:00
youpaishidifu ccf5a4e12d fix: 领地主可删除自己的领地(不需要admin权限) 2026-06-28 14:07:00 +08:00
youpaishidifu e02a05a1b6 fix: CLI返回改用尊重uiMode + 用户组DB大小写敏感查询 + 每层返回上一层 2026-06-28 13:53:44 +08:00
youpaishidifu 5530d7e862 fix: groupadd参数组名在前+tab补全在线玩家+选区防重复+一键创建超链接 2026-06-28 13:28:25 +08:00
youpaishidifu f7d13eb44c fix: 选区完成显示面积价格预览 + 用户组名大小写不敏感查找 2026-06-28 12:52:33 +08:00
youpaishidifu 5eb0000029 CLI用户组添加成员管理入口+groupmembers命令+返回按钮 2026-06-28 12:22:04 +08:00
youpaishidifu 9befb46afa 修复clear_all_bad字段名不一致+缺失deny_fluid列+hash计算修正 2026-06-28 12:16:05 +08:00
youpaishidifu b21835d00a 移除废弃的LEGACY_BREWING_STAND材料枚举 + 清理AreaCLIManager未使用的import 2026-06-28 11:52:00 +08:00
youpaishidifu e9eea049d9 feat: 领地创建确认交互+用户组GUI成员管理+领地限制DB兜底+PHP统计修复
- 创建领地改为预览模式:显示面积/单价/费用,点击确认按钮直接创建
- 新增confirm_create命令处理交互式创建
- 用户组GUI面板:左键打开成员管理,右键删除组
- 新增用户组成员管理面板:显示成员列表、添加在线玩家、移除成员
- 新增openGroupAddPlayerPanel:在线玩家选择面板
- 领地限制增加DB级别兜底检查,防止内存过期导致限制失效
- 创建后立即areas.put同步内存
- UserGroupManager新增getGroupMembers()公开方法
- PHP adminGetStatsEx: catch(Exception)改为catch(\Throwable),修复PHP8兼容
- GUIManager: BiomeScanTask修正为Main类型
2026-06-28 11:45:17 +08:00
youpaishidifu 2dfdad3a81 fix: GUIManager匿名BukkitRunnable改为命名静态内部类,修复Paper 26 ClassLoader兼容性 2026-06-28 11:03:56 +08:00
youpaishidifu e23b2df365 fix: 用户组JSON数组解析去错前导花括号导致解析失败 2026-06-28 10:54:52 +08:00
youpaishidifu 4105501ec6 fix: 用户组同步修复+CLI面板重构+调试日志增强
- pullUserGroupsFromPHP: 错误日志从fine升级到warning,添加raw response输出
- parseGroupJson: 新增decodeJsonUnicode()解码\uXXXX转义(如\u00a7→§)
- showGroupListPanel: 提取可复用方法,groupset/groupdelconfig/sync后自动重打印面板
- 每个用户组显示[编辑][删除]可点击操作入口
- sync.php handleListUserGroups添加JSON_UNESCAPED_UNICODE
2026-06-28 10:32:21 +08:00
youpaishidifu b26f979924 fix: 用户组PHP→Java拉取JSON解析bug + owner_change重复打印
1. pullUserGroupsFromPHP: 数组结束查找逻辑修复
   - 旧代码用[]计数找数组边界,但去掉开头[后内容是{},遇到]时depth=-1永远不匹配
   - 改为用{}计数+objDepth==0时匹配],正确解析[{...},{...}]格式
2. setLandOwnerFromWeb: 幂等检查
   - 如果所有者已相同(equalsIgnoreCase),跳过DB更新和日志打印
   - 防止ack失败后同一条owner_change重复打印
3. processedChangeIds清理优化
   - 超500时不再粗暴清空全部,先移除已过游标的旧ID(id < lastPollAdminChangesId)
   - 只在清理后仍然过大时才全部清空
4. pullUserGroupsFromPHP: 增加调试日志
   - 空响应/success!=true/无groups数组/解析失败都记录fine级别日志
2026-06-28 09:57:52 +08:00
youpaishidifu 06f8bd68cd fix: pullUserGroupsFromPHP添加secret认证参数 2026-06-28 00:49:51 +08:00
youpaishidifu 6d0c3a820a fix: 用户组PHP↔Java双向同步 + CLI/GUI管理入口修复
1. WebManager新增pullUserGroupsFromPHP():从PHP拉取用户组配置到Java本地
   - 简单JSON解析(无Gson依赖),支持嵌套括号解析
   - group_change变更处理改为调用pullUserGroupsFromPHP而非仅reload本地DB
2. CLI grouplist:打开管理面板而非仅打印列表,空列表时显示创建说明
   - 新增groupset sync命令:手动从PHP拉取用户组
   - 管理面板显示组数量、价格、上限等完整信息
3. GUI用户组管理:改为打开真正的GUI面板
   - 显示所有用户组(物品展示),支持右键删除
   - 空状态显示创建说明
   - 删除操作同时通知PHP同步
2026-06-28 00:44:56 +08:00
youpaishidifu ae018f57e5 fix: CLI/GUI交互优化 + 用户组PHP双向同步
1. CLI用户组列表:空列表时显示可点击创建链接,有列表时也显示创建入口
2. CLI公共建筑列表:每项显示可点击[传送到此]按钮,直接传送
3. GUI公共设施传送:改为打开GUI面板,点击建筑物品直接传送(非执行命令)
4. GUI管理员面板:新增用户组管理入口(slot31),显示组数量
5. Java pollAdminChanges:新增group_change类型处理,PHP变更后自动重载用户组
6. AreaProtection:补全Adventure API imports
2026-06-28 00:15:44 +08:00
youpaishidifu 8ef4744cbd fix: 清理旧版资源包代码+CLI/GUI补全用户组和公共设施入口
1. UserGroupManager: 移除旧版txt文件加载/解析、过期检查、displayEmoji/permanent/durationMinutes
2. CLI主菜单: 新增公共设施列表/传送入口、管理员用户组管理入口
3. GUI主菜单: 新增公共设施传送按钮(slot41)
4. CLI权限列表: 新增公共建筑设施切换项
5. GUI权限列表: 新增公共建筑设施切换项
6. WebManager同步: 移除displayEmoji字段
2026-06-27 23:37:49 +08:00
youpaishidifu 282930bfe6 feat: 用户组系统重构 - 领地创建价格/上限/默认权限差异化
- 重构UserGroupManager: 废弃资源包功能,新增领地相关字段
  - land_price_per_sqm: 每㎡创建价格(-1=全局)
  - max_lands: 每人最大领地数(-1=全局)
  - default_perms: 默认deny_*权限JSON
- 领地创建时自动读取用户组专属价格和上限
- CLI命令: grouplist/groupset/groupadd/groupdel/groupdelconfig
- syncLandData新增用户组同步到PHP
- PHP后端: list_user_groups/get_user_group/update_user_group等API
- DB: usergroup.db表结构自动迁移
2026-06-27 23:04:29 +08:00
youpaishidifu 09a4b1d869 feat: 和平模式攻击免疫+公共建筑设施+syncLandData静默优化
1. 和平模式: 敌对生物(含投射物)对玩家攻击无效
2. 和平白名单: 保护期内贴标立即确认+取消清理
3. 公共建筑: /protect public <领地>设为公共, /protect listpublic列出, /protect tpb传送到访
4. 公共建筑自动授予: 访客获得攻击敌对生物权限(覆盖denyMobAttack)
5. syncLandData: 去掉Timer D强制hash重置, 减少冗余日志(fine级别)
2026-06-27 22:43:02 +08:00
youpaishidifu a7ff29cdc3 fix: syncLandData去掉强制hash重置+减少冗余日志 2026-06-27 22:21:56 +08:00
youpaishidifu 6c738d484f fix: pollAdminChanges去重+ack修复,防止owner_change重复打印 2026-06-27 22:03:25 +08:00
youpaishidifu a8005e6666 修复领地管理员权限bug:getPermissionLevel增加per-land admin检查
- getPermissionLevel新增isLandAdmin检查(DB role='admin')
- 优先级:全局管理员 > 所有者 > 领地管理员 > 访客
- 解决通过/protect setadmin设置的管理员被当作VISITOR处理的问题
- 管理员身份现在正确跳过所有per-player deny权限检查
2026-06-27 21:23:16 +08:00
youpaishidifu 8c06b48989 修复配置值前导零bug:updateAreaConfig去除前导0
- Integer.parseInt去前导零:01→1,防止静默失败
2026-06-27 20:14:26 +08:00
youpaishidifu d0c4f45928 修复PHP→Java配置同步:双向同步全链路修复
1. PHP handleUpdateConfig 写入 web_admin_changes 变更队列
2. Java pollAdminChanges 新增 config_change 处理,调用 setAreaConfigValue
3. Timer A 新增 pollAdminChanges 轮询(永不暂停),确保全员离线也能拉取PHP变更
4. 双向同步验证:Java→PHP(Timer D) ✅ / PHP→Java(Timer A) ✅
2026-06-27 19:55:42 +08:00
youpaishidifu 7efa78dbf9 修复三个同步bug:配置即时同步+Timer D独立+债券余额即时推送
1. 配置同步:AreaProtection.updateAreaConfig()后立即触发requestImmediateLandSync()
2. Timer D独立:移除timersBCPaused检查,不再被全员离线暂停
3. Timer D不重复调度:玩家上线时不再重启Timer D(已独立运行)
4. syncLandData增加hash变化日志(便于排查同步问题)
5. 债券余额即时推送(上一轮已修复,本次重新编译)
2026-06-27 19:39:03 +08:00
youpaishidifu 179f3a8355 修复Java CDK离线兑付bug:Timer A增加CDK回拉逻辑
- 在Timer A(永不暂停)中添加本地CDK回拉(doTransactionPollCheck)
- 在Timer A中添加远程CDK验证(pullWebCdkRequestsAndValidate)
- 解决全员离线时Timer B暂停导致PHP兑换的CDK无法到账
- 修复ChatInputManager ClassNotFoundException(重新编译即可)
2026-06-27 18:54:43 +08:00
youpaishidifu f72875f0f7 添加Java→PHP成员权限同步 + getAllPermsForSync方法
1. WebManager新增syncPermissions()方法:读取area_land_permissions全表,推送到PHP的web_area_permissions
2. syncLandData()末尾调用syncPermissions(),每次领地同步时一并推送权限
3. AreaProtection新增getAllPermsForSync():JOIN area_lands获取领地名,返回所有成员权限数据
4. pollAdminChanges已支持perm_change/perm_clear/land_field_change三种回写

修复:玩家端权限页空白(web_area_permissions表永远为空)
2026-06-27 16:53:25 +08:00
youpaishidifu f6d5540b16 修复领地同步:添加land_field_change处理 + effects格式兼容
1. WebManager.pollAdminChanges添加land_field_change处理
   - PHP端玩家修改效果后,Java自动同步到本地DB
   - 防止下次推送时覆盖玩家的修改
2. AreaProtection新增updateLandFieldFromWeb方法
   - 支持白名单字段校验防SQL注入
   - 更新DB后刷新内存缓存
2026-06-27 14:52:34 +08:00
youpaishidifu 3c3e902a6a 领地同步独立定时器D(15~25秒) + 移除批处理中的syncLandData
问题:syncLandData在doActiveSyncBatch第9个任务位,60-90秒批处理+6-8秒间隔=116-146秒延迟
修复:
1. 新增TIMER_D独立定时器,15~25秒随机±5秒,错峰避开A/B/C
2. calcStaggeredDelay支持4个定时器互锁(≥8秒间隔)
3. doActiveSyncBatch移除syncLandData(不再重复调用)
4. 玩家上线恢复时同步重启Timer D
2026-06-27 14:12:23 +08:00
youpaishidifu b83202c08e 修复领地设置不同步:添加即时推送触发器
问题:游戏内修改领地设置后,PHP端超过180秒仍未同步
根因:syncLandData()在doActiveSyncBatch()第9个任务位,每任务间隔6-8秒,加上60-90秒批处理等待,总计116-146秒
修复:
1. WebManager新增requestImmediateLandSync()方法,防抖10秒
2. AreaProtection.saveAreaToDb()调用后立即触发PHP同步
3. deleteAreaFromDb()同样触发即时同步
4. 同步时强制清空hash确保一定推送
2026-06-27 13:32:05 +08:00
youpaishidifu ce0052cdfb 修复:CLI设为管理员按钮全局置顶、返回成员列表按钮、PHP配置同步 2026-06-27 12:27:30 +08:00
youpaishidifu ae2ae77a02 修复:权限切换静默失败(setPlayerPermJson仅UPDATE无行时失败)、管理员切换成员名大小写匹配 2026-06-27 12:03:02 +08:00
youpaishidifu e8c83bba98 CLI权限面板:管理员目标走简化面板,领地主可切换管理员身份 2026-06-26 23:36:55 +08:00
youpaishidifu 642e587cdd CLI权限面板:领地主可切换管理员身份,非领地主仅显示发光权限 2026-06-26 23:33:21 +08:00
youpaishidifu ffd11a0b9c 修复:领地主可操作管理员权限(GUI/CLI)、农作物种植权限补充新作物 2026-06-26 23:19:53 +08:00
youpaishidifu 33afe26b46 修复管理员权限显示和领地配置同步问题 2026-06-26 22:55:23 +08:00
youpaishidifu 30e7f68c85 修复:农作物种植权限+管理员权限隐藏+成员权限合并到用户管理+领地同步列数修复 2026-06-26 22:11:20 +08:00
youpaishidifu a06f7a669b 修复4个区域防护问题:展示架SHELF匹配、领地主不能添加为成员、农作物权限分层(独立denyCropHarvest)、领地同步调试日志 2026-06-26 21:23:20 +08:00
youpaishidifu f333e59621 修复WebManager$2类加载错误: 匿名X509TrustManager改为静态具名内部类TrustAllX509Manager,解决Bukkit类加载器NoClassDefFoundError 2026-06-26 19:49:15 +08:00
youpaishidifu 80c8715264 fix: 3个问题修复
1. 展示架拦截: isUIBlock新增DISPLAY/SHOWCASE/DECORATED_POT宽泛匹配
2. 领地同步hash: 从6个字段扩展到全部关键字段(60+),任何字段变化都触发同步
3. Web自动登录安全: validateWebAccess的view操作增加web_login_verified检查
   - pushPlayerLoginStatus在online=1时同步写入web_login_verified
   - 未通过Java验证的token即使在线也要求密码
   - 防止偷token自动登录
2026-06-26 19:15:54 +08:00
youpaishidifu f67efd0f8d fix: 领地系统7个问题修复
1. 领地主禁止作为成员入库: addLandMember检查owner返回boolean
2. 成员独立权限编辑: permDefs补充denyMobAttack
3. 红石原件扩展: 拉杆、各种按钮、压力板、阳光传感器
4. 容器交互新增展示架: denyContainer同时拦截展示框
5. 管理员身份改为领地级: isLandAdmin/setLandAdmin基于area_land_permissions.role='admin'
6. Timer A定期同步在线玩家: 每10轮(~30-50秒)同步一次保持PHP心跳
7. 领地数据同步修复: PHP sync_lands补充deny_container/deny_mob_attack/deny_fire等新字段,hash检测改为精确字段hash
2026-06-26 18:47:26 +08:00
youpaishidifu 3a3f4f1cd2 fix: SQLite锁库问题 - 定时器串行化+全员下线暂停+PHP WAL checkpoint
Java端改动:
- Timer A 内3个请求串行化(原来并行), 每个间隔2-3秒, 彻底避免PHP锁库
- 新增phpBusyUntil全局退避: 检测到database is locked时暂停所有定时器10秒
- 错峰间隔从5秒提高到8秒
- 全员下线时暂停Timer B/C, 仅保留Timer A(注册登录)
- 玩家上线后自动恢复Timer B/C
- 首次启动加±5秒随机偏移避免精确对齐
- doGet/doPost/doGetHttpFallback/doPostHttpFallback全部加detectPhpBusy检测

PHP端改动:
- busy_timeout从30秒提升到60秒
- 新增walCheckpoint()函数, 每个API请求结束后执行WAL checkpoint释放锁
- sync.php/shop.php/ticket.php/cdk.php/register.php/balance.php全部加checkpoint
2026-06-26 16:58:37 +08:00
youpaishidifu 11e1d27013 修复6项区域防护问题:
1. 移除独立成员权限面板,合并到成员列表左键编辑
2. 新增攻击生物权限(denyMobAttack):字段/DB迁移/事件处理/GUI/CLI
3. CLI效果编辑:增益效果添加等级/时长编辑按钮+聊天输入
4. 投喂动物修复:isAnimalFood检测动物食物+动态命名(禁止投喂/允许投喂)
5. 领地传送修复:GUI传送优先传送点→安全位置搜索(中心+40次随机)
6. 拾取节流:5~10秒随机间隔+5条随机提示轮换
2026-06-26 16:31:39 +08:00
youpaishidifu 3402a56d1c 修复容器管理开关:添加deny_container列的ALTER TABLE迁移\n旧数据库表缺少该列导致INSERT OR REPLACE失败,开关切换后数据丢失 2026-06-26 14:46:31 +08:00
youpaishidifu f2866ada9e 修复GUI管理员按钮和权限切换:
1. 管理员面板显示管理按钮(可切换setadmin/unsetadmin)
2. togglePerm支持动态权限名(禁止点燃/启用点燃等)
3. 管理员提示不再阻止显示管理按钮
2026-06-26 14:13:15 +08:00
youpaishidifu 48e1692822 修复5项区域防护问题:
1. 点燃权限:onIgnite检查denyFire而非denyFireSpread
2. 农作物收获:BlockBreak+右键检测作物方块
3. 权限显示名称:点燃/农作物收获动态命名
4. GUI效果管理tooltip:修正为GUI选择描述
5. 容器扩展:isUIBlock覆盖工作台/铁砧/砂轮等
2026-06-26 14:03:12 +08:00
youpaishidifu 44a9edca1b 修复6项问题:
- GUI袭击开关(启用袭击/禁止袭击双向切换)
- GUI增益效果添加改为可点击选择列表
- GUI添加成员排除领地主人
- 成员权限面板添加管理员状态显示和命令提示
- 新增setadmin/unsetadmin命令设置管理员
- 容器权限独立控制已验证
2026-06-26 13:18:41 +08:00
youpaishidifu ca54dd61ba 修复6项领地系统问题:
- 删除轮询空日志(pollWebLoginConfirmations)
- 添加拾取限制事件监听(PlayerPickupItemEvent)
- 新增独立容器管理权限(denyContainer)
- 管理员权限自动隐藏(per-player编辑跳过)
- GUI添加成员排除自身
- 验证破坏方块权限覆盖所有方块
2026-06-26 12:28:20 +08:00
youpaishidifu 47e1ad3be5 修复和平模式扫描范围+在线玩家同步+领地数据同步 2026-06-26 11:25:57 +08:00
youpaishidifu 46b483848c 修复问题4: GUI效果管理改为可点击选择列表
问题4修复: GUI效果管理
- 移除点击后在聊天栏输入模式
- 新增可点击效果选择列表(与CLI一致)
- 负面效果(16种)、中性效果(3种)、正面效果(20种)
- 点击效果直接添加到清除/增益列表
- 支持重复检测,已添加的效果显示灰色

新增方法:
- openEffectsSelection(): 打开效果选择列表GUI
- getEffectNameBySlot(): 根据槽位获取效果名
- pendingEffectSelection: 存储选择状态
2026-06-26 11:13:12 +08:00
youpaishidifu 40edd85ea7 修复多个问题:空白日志+禁止袭击更名+CDK逻辑简化
问题1修复: 移除空白日志
- 移除Web登录确认轮询和密码验证轮询的PHP响应日志
- 这些日志在data为空时输出无意义内容

问题6修复: 禁止袭击更名
- 启用状态(denyRaid=true)显示禁止袭击
- 禁用状态(denyRaid=false)显示启用袭击
- 修改AreaGUIManager和AreaCLIManager显示逻辑

问题3修复: CDK兑换逻辑简化
- PHP不再更新bond_cache,只记录pending交易
- Java端负责拉取交易并处理加债券
- 远程CDK和本地CDK统一处理逻辑
2026-06-26 10:59:50 +08:00
youpaishidifu 59313d4f32 问题10修复: CDK兑换不到账+假密码+隐藏密码框
CDK兑换修复:
- PHP远程CDK兑换改为写pending交易(cdK_redeem+pending)
- PHP远程CDK兑换同时更新bond_cache
- Java端移除sdf1反射调用(不必要的复杂度)
- 远程/本地CDK兑换链路统一:PHP更新cache→写pending→Java拉取→addBonds

假密码/密码框修复:
- player.php商城购买: 总价≤1000时隐藏密码输入框
- player.phpCDK兑换: 完全移除密码输入框(CDK免验证)
- shop.php移除硬性密码检查,交给validateWebAccess处理
- 动态切换:调整数量时密码框实时显示/隐藏
2026-06-26 10:21:35 +08:00
youpaishidifu c86e9f76b9 @ SeniorDeveloper 修复领地系统10项问题:空白日志、CLI效果管理、GUI验证、在线玩家同步、领地数据同步、领地所有权验证、用户端UI优化、效果管理、和平模式修复 2026-06-25 23:46:57 +08:00
youpaishidifu 142b15973a v23: 修复定时器A启动延迟 - 双条件判断
问题:
- 定时器A每次执行都检查initialSyncComplete,只有全量同步完成后才执行轮询
- 全量同步耗时60+秒,导致Web登录后60秒才开始轮询
- 玩家在20:09:19加入,20:10:01超时下线,20:10:19才开始轮询(延迟60秒)

修复:
- 新增allowLoginPolling标志,双条件:initialSyncComplete OR 玩家在线
- 玩家onJoin时立即设置allowLoginPolling=true(不等全量同步完成)
- 定时器A改为检查allowLoginPolling,有玩家在线时立即开始轮询
- 确保不影响定时器B/C的initialSyncComplete逻辑
2026-06-25 20:17:24 +08:00
youpaishidifu 3e21bbb112 v22: 简化Web登录确认逻辑+缩短轮询间隔到3~5秒
问题1 - 玩家Web登录后,Java拒绝自动登录(phpVerified=false):
- pollWebLoginConfirmations双重验证太严格:Java内存记录 AND PHP验证状态都为空时拒绝
- 根因:Java重启/内存清理后verifiedWebLogins为空,PHP的web_login_verified也无记录
- 但web_login_confirmations记录本身已代表通过安全验证(PHP verifyWebPassword 已禁用)

修复1 - 安全逻辑简化:
- 有确认记录 = 已通过某种安全验证,直接允许自动登录
- 移除「Java和PHP均无验证记录则拒绝」的严格检查
- 记录PHP验证状态仅用于日志诊断,不再作为拦截条件

问题2 - 轮询间隔8~15秒太长,导致Web登录后66秒才自动登录:
- 修复2:定时器A间隔缩短到3~5秒,快速响应登录请求
2026-06-25 20:00:38 +08:00
youpaishidifu df6cc2bea8 v21: 修复Web登录同步失败+全链路日志
问题根因:
- pollWebLoginConfirmations只检查Java内存isWebLoginVerified
- 邮箱验证码登录时PHP直接写web_login_confirmations,Java从未调用recordWebLogin
- 导致hasVerifiedRecord=false,自动登录被安全拦截

修复内容:
- PHP端:check_web_login_confirmations返回php_verified字段(查询web_login_verified表)
- Java端:pollWebLoginConfirmations信任PHP验证状态,Java无记录但PHP有→信任PHP并写入
- 全链路日志增强:轮询开始/PHP响应/安全检查/回写结果全部打印详细日志
- PHP端:webLoginRequest/completeWebLoginRequest/checkPendingWebLogins增加debugLog
2026-06-25 19:35:10 +08:00
youpaishidifu d23ce56606 v20: 修复锁库频率+URI undefined scheme错误
问题1 - 锁库超预期(每分钟2次):
- 定时器A间隔0~5秒太短,三个并行任务同时打PHP
- 修复:定时器A间隔改为8~15秒,减少锁库概率

问题2 - URI with undefined scheme:
- pollAdminChanges的ackUrl是相对路径/api/land_api.php?...,缺少webBaseUrl前缀
- 修复:ackUrl加上webBaseUrl前缀
- 附带修复:poll_admin_changes的secretKey未URL编码,已加上URLEncoder.encode
2026-06-25 19:15:57 +08:00
youpaishidifu eb3c336e38 v19: 修复Web登录验证链路 - verifiedWebLogins永不写入导致自动登录失败
问题根因:
- pollWebLoginRequests验证密码成功后不写verifiedWebLogins(注释写了'不在验证成功时写入')
- pollWebLoginConfirmations检查isWebLoginVerified永远返回false → 自动登录被拒绝
- 导致PHP确认登录成功但Java不自动登录玩家

修复内容:
- 新增recordWebLogin()方法:密码验证成功时立即写入verifiedWebLogins
- pollWebLoginRequests:密码验证成功后调用recordWebLogin
- pollWebLoginConfirmations:增强安全检查日志
- isJavaLoginRecorded:增加详细诊断日志(记录数、过期时间等)
2026-06-25 18:58:44 +08:00
youpaishidifu f1cd0da7d4 v18 修复快速重连:新增javaLoginRecords持久化登录记录(5分钟),检查点1放行同IP重连;添加PHP响应日志 2026-06-25 18:44:42 +08:00
youpaishidifu 329bca61a9 v17 合并定时器重构:6个独立定时器→3个合并错峰定时器(A注册登录0~5s B交易0~10s C其它10~20s),自动错峰≥5秒,消灭锁库 2026-06-25 18:22:03 +08:00
youpaishidifu b6863c45c7 修复: 自动登录逻辑重构+轮询间隔增大降低DB锁 2026-06-25 18:03:45 +08:00
youpaishidifu 2867c7614a 优化数据库锁检测:修复注册轮询锁库逻辑,为CDK轮询和领地同步添加锁库检测,调整交易/库存轮询间隔为8±5秒 2026-06-25 17:33:43 +08:00
youpaishidifu 23337695b4 v15: 修复PHP假密码绕过 + 快速重连安全检查
安全修复(严重):
- WebManager.pollWebLoginRequests(): 不再在handleWebPasswordVerify成功后写入verifiedWebLogins
  - 之前逻辑:PHP推送密码给Java → Java验证成功 → 自动写入verifiedWebLogins → onJoin自动登录
  - 漏洞:PHP端如果输入假密码/错误密码,Java不会验证(Java验证只基于login.db),verifiedWebLogins不会被设置
  - 但pollWebLoginConfirmations()检查isWebLoginVerified()失败后,玩家快速重连仍被canAutoLogin放行
- Main.onJoin(): 快速重连检查(canAutoLogin)之前增加isWebLoginVerified()检查
  - 玩家在Web端通过Java验证登录过 → verifiedWebLogins有记录 → 5分钟内重连放行
  - 玩家无Java验证记录 → 拒绝快速重连(即使用IP快速重连也不行)
  - 解决:PHP假密码/错误密码 → Java不会设置verifiedWebLogins → 快速重连被拒
2026-06-25 16:24:15 +08:00
youpaishidifu 15f473abc8 v14: Java快速重连三层检查(纯内存)+ 回退DB持久化
快速重连修复(按流程图严格实现):
- 检查点1: 检查loggedIn集合中是否有Java手动登录记录 → 有则继续正常流程
- 检查点2: 检查verifiedWebLogins内存中是否有PHP验证记录 → 有则自动登录
- 检查点3: 上述2路都失败 → 强制重新验证密码
- autoGenerateWebLoginToken和syncUserOnJoin移到检查点1/2之间执行

回退DB持久化:
- DatabaseManager.java删除web_verified_at列定义
- WebManager.java isWebLoginVerified()回退为纯内存检查
- WebManager.java clearWebLoginVerified()回退为纯内存清除
- pollWebLoginRequests()验证成功后不再写入login.db

部署:
- JAR: 733300 bytes → SFTP ✅
2026-06-25 15:15:07 +08:00
youpaishidifu d47997294d v13: 安全加固 - PHP禁止自验证密码 + Java本地验证记录持久化
安全修复(严重隐患):
- PHP端: verifyWebPassword()函数体改为error(),禁止PHP直接验证密码
- Java端: pollWebLoginConfirmations()增加本地验证记录检查,PHP推送的登录确认必须有Java本地验证记录才允许自动登录
- Java端: 验证成功后写入login.db的web_verified_at字段(重启不丢失)
- Java端: isWebLoginVerified()增加login.db回查(内存没有时从DB恢复)
- Java端: clearWebLoginVerified()同时清除DB记录
- Java端: pollWebLoginConfirmations消费后立即清除验证记录(防重放)
2026-06-25 14:41:25 +08:00
youpaishidifu 6d569fed23 v12: PHP密码验证安全修复 + 任务间延迟6-14秒随机
安全修复:
- sync.php: 删除verify_web_password端点,禁止PHP自验证密码
- core.php: validateWebAccess大额交易密码验证改为检查web_login_verified(Java验证结果)
- 密码验证强制走web_login_request流程(Java端handleWebPasswordVerify)

锁库优化:
- 任务间随机延迟从10-12秒改为6-14秒(8秒变异范围)
- 增大随机性,避免周期性锁争用
2026-06-25 14:13:31 +08:00
youpaishidifu 6e24546a6c v11: SSL断路器防重复触发 + HTTP降级延长冷却 + 启动同步锁库修复
SSL断路器:
- 新增triggerCircuitBreaker()安全方法,并发请求不再重复触发
- HTTP降级也失败时立即延长冷却(CDN完全不可达场景)
- 基础冷却30秒→60秒(CF恢复通常1-5分钟)
- GET/POST/HTTP降级失败统一走triggerCircuitBreaker

启动锁库:
- 新增initialSyncComplete标志,注册/CDK/库存轮询器等待全量同步完成
- 避免启动阶段多个轮询器与全量同步并发请求PHP导致SQLite锁
2026-06-25 13:42:34 +08:00
youpaishidifu eb0b39d8b9 v10: SSL断路器指数退避 + 超时统一10秒 + 延迟增到10-12秒
- SSL断路器冷却从固定30秒改为指数退避: 30s→60s→120s→240s→300s(上限)
- 新增sslCircuitOpenCount跟踪连续断路次数,成功时重置
- 请求超时统一10秒(connectTimeout和request timeout一致)
- 任务间随机延迟从8-10秒增到10-12秒,进一步减少锁库
- HTTP降级成功也重置退避计数
2026-06-25 13:27:05 +08:00
youpaishidifu 10e148be5f v9: 修复pollAdminChanges 404 + 延迟增到8-10秒
- 修复 ack_admin_changes double URL bug(httpPost重复拼接webBaseUrl)
- 改用GET方式发送ack,避免POST Content-Type不匹配
- land_api.php 支持GET参数ids
- 所有任务间随机延迟从6-8秒增到8-10秒
- 目标:锁库降到0-1次/分钟
2026-06-25 13:05:41 +08:00
youpaishidifu 9599a66cc5 增强: 随机间隔从±8秒扩大到±10秒;任务间随机延迟从4-6秒增加到6-8秒;减少PHP端SQLite锁冲突 2026-06-25 12:46:45 +08:00
youpaishidifu c05c6d9512 修复: SSL降级触发条件优化-任何连接异常都计数;任务间随机延迟从2-4秒增加到4-6秒;增加SSL计数器在POST最终失败时 2026-06-25 12:21:25 +08:00
youpaishidifu 2a976710e9 增强: 随机间隔扩大到±8秒;所有同步任务间改为随机2-4秒延迟;拆分同步添加0-2秒随机延迟避免并发锁库 2026-06-25 12:07:48 +08:00
youpaishidifu c2a8385895 优化: 首次全量同步和周期同步任务间错开1500ms,避免PHP端SQLite数据库锁 2026-06-25 11:53:58 +08:00
youpaishidifu 3c71664284 修复: 404问题-在线玩家同步GET返回404时触发分批POST拆分(每人一批); 库存轮询增加DB锁错误检测; 随机间隔扩大到±5秒 2026-06-25 01:44:46 +08:00
youpaishidifu c9e2f71361 增强: 随机间隔扩大到±5秒减少DB锁冲突;库存轮询增加数据库锁错误检测 2026-06-25 01:19:47 +08:00
youpaishidifu 3c085e086d 优化: 全量同步任务间错开800ms执行,避免瞬间堆叠导致DB队列等待过久和HTTP 404 2026-06-25 00:49:31 +08:00
youpaishidifu 28a120fea0 修复: 补充rebuildHttpClient方法和initPlainHttpClient方法,实现SSL降级到HTTP功能;添加randomIntervalSeconds随机间隔定时器,避免并发SQL锁死 2026-06-25 00:19:46 +08:00
youpaishidifu dee6ab8eed feat: SSL连续失败3次自动降级到HTTP + 编译部署 2026-06-24 23:06:03 +08:00
youpaishidifu 5273220716 修复:poll_admin_changes返回JSON包含Unicode转义序列导致解析404 2026-06-24 21:28:42 +08:00
youpaishidifu e416372a41 修复:GUI效果管理回调子页面保持、PVP双端检查、传送安全位置、游戏模式管理员豁免 2026-06-24 20:33:35 +08:00
youpaishidifu bbf23aad31 fix: 领地系统多项修复 - 管理员TAG验证/和平模式/袭击模式命名/效果管理GUI/添加成员功能 2026-06-24 18:32:25 +08:00
youpaishidifu 7ae4f46f75 fix: 修复CLI效果管理点击返回回到首页问题,去除返回按钮中的clickEvent导致 2026-06-24 15:38:17 +08:00
youpaishidifu 4a74a0a77f feat: 新增效果管理功能 — CLI/GUI可视化操作 1.CLI效果管理菜单: 全清负面/禁止所有效果/单清列表/增益列表 2.CLI快捷指令: effectsclearall effectsdenyall effectsclearadd effectsclearremove effectsaddadd effectsaddremove 3.GUI效果管理: 管理领地菜单新增效果管理入口, 3个子菜单(GUI可视化操作) 2026-06-24 14:27:02 +08:00
youpaishidifu 9944fd5f16 Fix area protection: perm renames, DB save fix, perm_change sync, teleport command 2026-06-24 13:07:11 +08:00
youpaishidifu 3662466434 chore: 从main分支移除web文件夹(PHP代码归php分支管辖) 2026-06-24 11:17:18 +08:00
youpaishidifu 302268d0da fix: 领地系统4个bug修复
1. CLI用户权限管理: 添加shortKeyToDenyField映射修复键名不匹配
2. 移除用户: 修复大小写敏感导致的移除失败
3. 权限组反转: 修复togglePerm反馈消息语义(deny=true→已禁止)
4. GUI工具: BLAZE_ROD替代STICK
2026-06-24 11:08:55 +08:00
youpaishidifu 6ec438abd9 fix: parseJson数组解析 + WebManager ClassCastException修复
- addParsedEntry增加JSON数组识别,存为List<Map>而非String
- 新增parseJsonArray/parseJsonArrayItem递归解析嵌套数组
- 修复pollAdminChanges中List类型强转失败(class String cannot be cast to List)
2026-06-24 10:25:36 +08:00
youpaishidifu 9e3182e1a1 fix: 8项问题修复
1. PHP登录安全: player.php改用web_login_request走Java验证
2. 圈地工具改为BLAZE_ROD(避免投影mod冲突)
3. CLI更新后打印新版配置内容
4. 智能数字解析: 支持+/-前缀、中英文/罗马数字、封底值
5. 删除upload_sftp.py敏感文件并加入gitignore
6. CLI成员权限页面添加所有者/管理员权限检查
7. GUI模式移除冗余sendMessage调用(消息不外泄)
8. AreaCLIManager变量名冲突修复
2026-06-24 01:22:59 +08:00
youpaishidifu 8cd380a93b feat: 成员独立权限系统 + PHP→Java同步 + SSL兼容性 2026-06-23 23:46:44 +08:00
youpaishidifu a3030c3b80 feat: 8项修复 - 圈地工具冷却/删除取消/CLI配置/智能数字解析/SSL重连
1. 圈地工具5秒冷却+拥有检测(CLI+GUI)
2. 删除冷却期间权限修改/成员变更自动取消删除
3. CLI全局配置页面(点击可修改各配置项)
4. GUI管理员面板改为聊天栏输入+智能数字解析
   支持: 纯数字、中文数字、中文大写、罗马数字、英文数字
5. SSL: 强制TLSv1.2+CF兼容密码套件+断路器冷却后重建HttpClient
6. SSL断路器增强: submitDbTask/getDoGet/doPost前置检查
2026-06-23 22:30:12 +08:00
youpaishidifu 2a5db378d3 fix: SSL断路器增强 - 多层防护避免SSL握手风暴
问题:SSL握手失败时,多个轮询定时器(5秒/2秒/3秒)并发提交任务,
导致连续失败计数飙升(58-69次),断路器反复触发但无法有效阻断。

修复:
1. submitDbTask()添加断路器检查 - 断路期间跳过所有DB任务
2. doGet/doPost/doGetStatus/doPostWithRetry添加断路器前置检查
3. 断路期间不再发送任何HTTP请求,避免无意义的SSL握手
4. 60秒冷却期结束后自动恢复

部署:JAR已上传MC服务器
2026-06-23 21:26:33 +08:00
youpaishidifu 05ebd07df6 领地系统8项修复: SSL断路器/计费/GUI-CLI切换/删除取消/管理细分/配置释放/txt迁移 2026-06-23 19:52:55 +08:00
youpaishidifu 542879177c feat: 新增AreaCLIManager交互式CLI菜单系统
- CLI主菜单:创建领地/领地列表/权限商店/信息/切换模式
- 领地列表:分页显示,每条有[管理][传送][删除]可点击按钮
- 权限管理:分页显示29项权限,每项有✔/✘标记+切换按钮
- /protect无参数时CLI模式直接打开交互菜单
- AreaProtection新增getAllLands()公开方法
2026-06-23 18:15:31 +08:00
youpaishidifu 69c9e0ceb9 @
领地系统大修:新增10个权限事件处理器 + GUI权限补全 + 删除二次确认修复

新增事件处理器:
- ProjectileLaunchEvent: 投掷物检测(三叉戟/雪球/风蛋)
- PlayerInteractEvent: 门禁交互(按钮/门/压力板/绊线钩)
- NotePlayEvent: 音符盒交互
- JukeboxPlay(InteractEvent): 唱片机交互
- PlayerInteractEvent: 红石交互(中继器/比较器)
- PlayerInteractEntityEvent: 拴绳使用
- BlockGrowEvent: 农作物收获
- PlayerShearEntityEvent: 剪切羊毛/生物
- EntityBreedEvent: 投喂动物
- RaidTriggerEvent: 袭击侦测
- 周期性扫描: 玩家发光检测

修复:
- GUI权限列表: "PVP"→"玩家对战", "Raid"→"袭击侦测", 补全全部29项权限
- 删除二次确认: 防止绕过1分钟延迟
- 移除重复import, 修复InventoryHolder缺失
@
2026-06-23 17:57:24 +08:00
youpaishidifu 58cc9677ae fix: 迁移INSERT列数不匹配(45→46) + 移除未使用import 2026-06-23 17:17:18 +08:00
youpaishidifu c691e9c44f @
领地系统:修复DB列数不匹配(59→61) + 用户UI偏好设置

- 修复saveAreaToDb INSERT语句缺少2个?占位符(warp_pitch/warp_world)
- 新增 /protect uimode cli|gui 命令切换UI偏好
- 主菜单39号位添加领地系统入口,读取偏好决定GUI/CLI
- /protect无参数时读取偏好,自动选择GUI或CLI模式
@
2026-06-23 17:13:18 +08:00
youpaishidifu 70b8e991da feat: 领地系统大规模重构
1. 全局配置: area_config表存储创建价格/每人上限/默认高度等
2. 新增10+权限: 投掷物/发光/红石/门禁/音符盒/拴绳/农作物/羊毛/动物投喂
3. 传送点: settp设置传送点,tp传送到领地传送点
4. 删除安全: 1分钟延迟+二次确认(输入完整领地名),操作自动取消
5. 容器权限: 覆盖所有BlockState容器+Container接口
6. 安全传送: 头顶≥2格空气+脚下实体方块+距边界≥3格,否则世界出生点
7. 和平模式: 最长1小时(3600秒)强制订正
2026-06-23 16:44:05 +08:00
youpaishidifu ab2c4da6de fix: CLI命令占位符+PHP领地API认证修复+毛玻璃弹窗
1. CLI: 移除help中含<领地>占位符的可点击命令,避免用户点击后传入字面量
2. PHP land_api.php: validateToken→validateTokenSilent,兼容weblogin_tokens
3. PHP land_api.php: POST操作区分syncActions/adminActions/playerActions三级认证
4. PHP admin.php: 所有领地管理操作携带secret参数
5. PHP core.php: busy_timeout从15s提升到30s,加wal_autocheckpoint
6. player.php: 所有alert()→glassAlert(),confirm()→glassConfirm()
7. player.php: 新增毛玻璃弹窗CSS+HTML+JS,居中+backdrop-filter+淡入淡出
2026-06-23 15:50:25 +08:00
youpaishidifu 9f185df6d9 安全修复: 从git中移除敏感上传/调试脚本并加入.gitignore 2026-06-23 14:36:15 +08:00
youpaishidifu ce13396c22 feat: 合并PHP Web后端到仓库 - 统一代码管理\n\n将D:\桌面\源码\mc_plugn\Web\的PHP文件合并到web/子目录\n- 包含所有API端点(sync.php, register.php, shop.php等)\n- 包含前端页面(admin.php, player.php, login.php等)\n- 排除Web.db、调试日志、备份文件\n- .gitignore更新:忽略web/Web.db、debug_log_tail.txt、.bak文件 2026-06-23 14:11:40 +08:00
youpaishidifu 439da63793 fix: 领地系统8个核心问题修复
1. SQL列数不匹配(saveAreaToDb): INSERT补齐46列+deny_move/pickup/fire/txt_content
2. Owner入库: saveAreaToDb用?绑定owner值(loadAreasFromDb读取owner)
3. denyMove传送出去: 新增findSafeExitLocation(),无权限玩家传送到领地边缘
4. 容器权限: 新增onInventoryOpen(InventoryOpenEvent)阻止未授权玩家开箱
5. CLI setowner自动分配: 管理员无参数时自动设当前领地所有者为自身
6. txt配置文件: 新增LandConfig类+区域防护配置.txt(创建价格/高度/和平模式等)
7. 权限只对访客: denyEnderPearl/denyBow增加hasPermission OWNER检查
8. 数据库兼容: ALTER TABLE添加deny_move/pickup/fire列(兼容旧库)
2026-06-23 13:41:57 +08:00
youpaishidifu 92552f6007 fix: PHP 500修复 - HttpClient降级HTTP/1.1 + 错误响应体日志
根因:java.net.http.HttpClient默认HTTP/2与CF代理的某些请求模式不兼容
修复:强制HTTP/1.1(与curl行为一致,curl测试正常返回200)
附加:doGet/doPost在非2xx时记录响应体,便于诊断PHP端错误
2026-06-23 13:02:26 +08:00
youpaishidifu 152ebc2dc6 fix: 彻底替换HttpsURLConnection为java.net.http.HttpClient - 解决CF SSL握手失败
根因: HttpsURLConnection内部调用factory.createSocket()(无参版本)绕过了自定义工厂配置
方案: 改用java.net.http.HttpClient原生HTTP客户端,原生支持ALPN/SNI/TLS/HTTP2
- initSSL()重写为创建HttpClient实例
- 新增doGet/doPost/doPostWithRetry/doGetStatus统一请求方法
- 全部29处HttpURLConnection替换为新方法
- TrustAllCerts保留,SSLContext使用通用TLS自动协商
2026-06-23 12:35:13 +08:00
youpaishidifu 1550e3a461 fix: 重写SSL初始化 - 自定义SSLSocketFactory逐socket配置TLS1.2+CF兼容密码套件 2026-06-23 11:34:05 +08:00
youpaishidifu 575e54a27a 修复SSL握手异常:添加Cloudflare兼容模式,设置SNI和密码套件 2026-06-23 11:08:24 +08:00
youpaishidifu d9e3b74fd9 实现CLI可点击聊天消息触发命令,与DOS和GUI三端兼容 2026-06-23 10:44:01 +08:00
youpaishidifu fa9c5cf601 修复denyMove权限检查、成员管理操作修改、优化任务队列优先级、登录轮询改为事件驱动 2026-06-23 10:30:25 +08:00
youpaishidifu cd2d94c9d0 修复protect命令参数问题、修复文档链接、添加用户UI偏好列(ui_mode)、txt迁移领地自动分配给管理员 2026-06-23 01:10:01 +08:00
youpaishidifu de3e1c8e83 修复库存高频拉取队列积压问题:添加去重机制,避免重复提交相同任务 2026-06-23 00:42:55 +08:00
youpaishidifu 34505ea2f5 添加区域防护GUI菜单系统:主菜单、领地列表、管理领地、成员列表、访客权限、领地设置 2026-06-23 00:19:19 +08:00
youpaishidifu b5d6519d11 为syncBondBalances和syncUserRegistrations添加基于哈希的变化检测,实现无变化静默 2026-06-22 23:44:01 +08:00
youpaishidifu 73bee83051 feat: 实现DB写入排队系统 + AreaProtection升级 + 领地管理
- WebManager: 添加PriorityBlockingQueue数据库写入队列
  - 登录操作LOGIN_PRIORITY=1(最高)
  - 普通同步NORMAL_PRIORITY=10
  - 单线程串行化,防止并发SQLite锁
  - 所有sync/poll方法通过队列执行
- AreaProtection: 三级权限系统(ADMIN/OWNER/VISITOR)
  - 数据库存储(txt→db迁移)
  - 20+事件监听器
  - CLI命令: info/setowner/addvisitor/transfer/shop
  - 权限商店(1000债券起,24h上限,80/20分成)
- ConfigManager: 区域防护配置项
- Main: 铁砧改名颜色代码(&→§)+修复help拦截
- sync.php: 添加领地同步接口(sync_lands/sync_land_shop)
2026-06-22 22:37:38 +08:00
youpaishidifu 7d8dc8f4c9 docs: 前置插件链接改为对应平台独立仓库地址 2026-06-22 17:31:26 +08:00
youpaishidifu 07791fa613 docs: 修复前置插件链接为wiki绝对路径 2026-06-22 17:21:33 +08:00
youpaishidifu fed72b773f docs: README同步文档站最新版,下载地址改为Wiki链接 2026-06-22 17:14:21 +08:00
youpaishidifu ebcb11338c fix: 修复Web注册不写注册IP、玩家登录IP的bug; 新增库存高频轮询; 更新README下载地址 2026-06-22 16:26:17 +08:00
youpaishidifu 8d4731a873 fix: 添加官方仓库下载链接 2026-06-20 14:18:02 +08:00
youpaishidifu 203c9d423a 新增Web CDK接口,可对接Web系统进行兑换 2026-06-20 14:02:41 +08:00
youpaishidifu 2c7f765fb0 初始化登录插件 2026-06-19 16:00:29 +08:00
50 changed files with 40965 additions and 9536 deletions
+100
View File
@@ -0,0 +1,100 @@
name: Auto Release
on:
push:
branches: [main]
jobs:
build:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Verify Java
run: |
echo "JAVA_HOME=$JAVA_HOME"
java -version
- name: Build with Maven
run: mvn -B clean package -DskipTests
- name: Verify JAR
run: ls -la target/*.jar
- name: Create Release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
run: |
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}..."
# 测试 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\":\"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}"
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"
else
echo "⚠️ Release created but could not get release ID for JAR upload"
fi
+34
View File
@@ -0,0 +1,34 @@
name: Auto Release
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 26
uses: actions/setup-java@v4
with:
java-version: '26'
distribution: 'zulu'
- name: Build with Maven
run: mvn -B clean package -DskipTests
- name: Verify JAR exists
run: ls -la target/Sdf1_login.jar
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: build-${{ github.run_number }}
name: Build #${{ github.run_number }}
files: target/Sdf1_login.jar
generate_release_notes: true
+24 -1
View File
@@ -45,6 +45,27 @@ txt/
backup/
新建*.txt
群戏归类.txt
upload_sftp.py
deploy_*.py
deploy_*.ps1
deploy_*.sh
# PHP Web 后端
web/Web.db
web/debug_log_tail.txt
web/upload_debug.ps1
web/api/*.bak*
# 敏感文件 - 禁止提交上传/调试脚本
web/upload_*.php
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
@@ -68,4 +89,6 @@ lpt5
lpt6
lpt7
lpt8
lpt9
lpt9hs_err_pid*.log
hs_err_pid*.log
replay_pid*.log
+1 -3
View File
@@ -8,7 +8,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="26" project-jdk-type="JavaSDK" />
</project>
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+21
View File
@@ -0,0 +1,21 @@
## 2026-07-04 13:33 - 新玩家验证码系统 + 全场景链接过滤 + 私信URL拦截
### 验证码系统实现
- ChatFilterManager 新增 VerificationResult 枚举(NEED_VERIFICATION/PENDING/VERIFIED/FAILED)
- 新玩家首次发消息时触发数学题验证码(10以内加减乘除,随机选择)
- 30秒有效期,答错/超时/非数字答案 → 吞噬消息
- 答对 → 插件代为广播缓存的原始消息(无前缀,像普通聊天一样)
- 老玩家(注册>1小时)快速验证后自动放行
- pendingMessages Map 缓存未验证消息,验证通过后广播
### 全场景链接过滤
- 告示牌(onSignChange): 已有的 filterLinkInContent 拦截
- 编辑书(onPlayerEditBook): 通过反射调用 getNewBook() 获取书页内容(兼容Paper 1.21.4)
- 私信(onPrivateMessage): 新增 PlayerCommandPreprocessEvent 拦截 /tell /msg /w /r 命令中的链接
### 编译部署
- Maven clean package 编译通过
- 推送到 git.ypshidifu.cn (main 分支)
- JAR 上传到 SFTP 服务器
- PHP 文件上传到 FTP /plugin/ 目录
-30
View File
@@ -1,30 +0,0 @@
[01:19] Web端交易三个核心问题修复
==================================================
1. 商品ID无效(ACACIA_BOAT)- 已修复
- PHP shop.php 传 item_id="ACACIA_BOAT"(Material名称)
- Java dispatchItemByMaterialOrId() 已支持Material匹配+ID fallback
- displayName获取已修复:优先从ItemMeta.getDisplayName()读取,而不是直接用itemId
2. 扣款变给钱(bond_log符号反了)- 已修复
- BondManager.deductBondsInternal(): logLegacy(player, -amount) → logLegacy(player, amount)
- bond_log表的amount字段现在记录正数,UI根据type判断加减
3. 交易轮询慢(30-90秒)- 已修复
- PHP shop.php 和 cdk.php 在购买/兑换成功后立即POST到Java notify_sync回调
- Java收到notify_sync后调用requestImmediateTransactionPull()
- 最小间隔5秒,避免过度频繁
4. 回调服务器并发问题 - 已修复
- handleCallback()分离notify_sync和register_callback两种请求
- notify_sync不阻塞注册回调处理
5. detail JSON解析 - 已修复
- 支持转义引号\"的嵌套JSON解析
==================================================
下一步:
1. 用IDEA重新编译打包(Maven → Lifecycle → package)
2. 将Sdf1_login.jar复制到MC服务器plugins目录覆盖
3. 重启服务器测试
4. 验证:Web购物→立即到账、CDK兑换→余额同步、扣款方向正确
+48 -6
View File
@@ -5,7 +5,7 @@
## 1. 功能介绍
| 名字 | 描述/作用 | 施工状态 |
| :--- | :--- | :--- |
| :---: | :---: | :---: |
| 登录 | 用户登录/注册 | 已完成 |
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
| 工单系统 | 用户提交工单 | 已完成 |
@@ -22,13 +22,14 @@
| 前置插件 | 选装类型 | 功能作用 |
| :--- | :--- | :--- |
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
| EssentialsX | 可选 | 主流经济系统
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
| [CY_beibao](https://git.ypshidifu.cn/youpaishidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 |
| EssentialsX | 可选 | 主流经济系统 |
| [sdf1](https://git.ypshidifu.cn/youpaishidifu/Sdf1_plugin) | 可选,推荐 | CDK兑换系统 |
### 2.1 下载
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
您可以从GitHub和Gitee、gitcode下载到插件,将其丢入服务器plugin目录,重启服务器即可。
### 2.2. 配置文件
@@ -505,6 +506,47 @@ http-port=443
```
## php端说明:
1. 默认密码ypshidifu2026<br>
2. 工单系统全面支持markdown语法:
```md
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
表格语法
| 表格列1 | 表格列2 |
| --- | --- |
| 表格1 | 表格2 |
>单嵌套语法
>嵌套块
>
>>语法
![](图片链接) 图片语法
[]() 超链接语法
<br>换行语法
代码块语法
```
## 工单状态:
| 状态名 | 是否可回复 | 备注说明 |
| :---: | :---: | :---: |
| 已提交 | 可回复 | 初始化状态 |
| 已回复 | 可回复 | 玩家自己回复了/管理员/服务商回复了 |
| 已完结 | 不可回复 | 管理员驳回了工单请求 |
| 已撤销 | 不可回复 | 用户撤销了工单请求 |
## 服务商说明
- 若您需要授权某个玩家为服务商,请登录游戏,打开工单系统-管理面板-添加服务商,然后输入他的名字即可邀请服务商
## 运行截图
主界面
@@ -563,8 +605,8 @@ http-port=443
商城退单
![](img/shop2.png)
## 4. 下载地址
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
+2 -9
View File
@@ -34,12 +34,6 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
@@ -55,10 +49,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<version>3.15.0</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+107
View File
@@ -11,6 +11,26 @@ public class BondManager {
private final Main plugin;
private Connection db;
// ===== 交易监听器 =====
public interface TransactionListener {
void onTransaction(String playerName, String type, int amount, String targetPlayer);
}
private final List<TransactionListener> transactionListeners = new ArrayList<>();
public void addTransactionListener(TransactionListener listener) {
transactionListeners.add(listener);
}
private void fireTransactionEvent(String playerName, String type, int amount, String targetPlayer) {
for (TransactionListener listener : transactionListeners) {
try {
listener.onTransaction(playerName, type, amount, targetPlayer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// ===== 交易类型常量 =====
public static final String TX_ADMIN_GIVE = "admin_give";
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
@@ -361,6 +381,8 @@ public class BondManager {
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
// 触发交易监听器
fireTransactionEvent(player, type, amount, targetPlayer);
} catch (SQLException e) {
e.printStackTrace();
}
@@ -697,4 +719,89 @@ public class BondManager {
this.message = message;
}
}
// ===== 获取玩家最近1小时出售总额 =====
public int getTodaySellTotal(String player) {
return getSellTotalInWindow(player, System.currentTimeMillis() - 3600000L);
}
private int getSellTotalInWindow(String player, long since) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT COALESCE(SUM(amount), 0) FROM bond_transaction "
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
ps.setString(1, player);
ps.setLong(2, since);
ResultSet rs = ps.executeQuery();
int total = 0;
if (rs.next()) {
total = rs.getInt(1);
}
rs.close();
ps.close();
return total;
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取玩家最近1小时出售窗口中最早交易的时间戳。
* 返回0表示窗口内无交易。
*/
public long getEarliestSellTimeInWindow(String player) {
long since = System.currentTimeMillis() - 3600000L;
try {
PreparedStatement ps = db.prepareStatement(
"SELECT MIN(time) FROM bond_transaction "
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
ps.setString(1, player);
ps.setLong(2, since);
ResultSet rs = ps.executeQuery();
long earliest = 0;
if (rs.next()) {
earliest = rs.getLong(1);
}
rs.close();
ps.close();
return earliest;
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取指定时间之后的所有交易记录(用于Web同步)
*/
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
List<Map<String, Object>> transactions = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT id, player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
ps.setLong(1, afterTime);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> tx = new LinkedHashMap<>();
tx.put("id", rs.getInt("id"));
tx.put("player_name", rs.getString("player_name"));
tx.put("type", rs.getString("type"));
tx.put("amount", rs.getInt("amount"));
tx.put("target_player", rs.getString("target_player"));
tx.put("operator", rs.getString("operator"));
tx.put("reason", rs.getString("reason"));
tx.put("balance_before", rs.getInt("balance_before"));
tx.put("balance_after", rs.getInt("balance_after"));
tx.put("time", rs.getLong("time"));
transactions.add(tx);
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return transactions;
}
}
+17 -52
View File
@@ -170,64 +170,29 @@ public class CDKManager {
if ("points".equals(input.type)) {
handlePointsInput(p, msg.trim(),
input.context);
return;
}
if ("gift_target".equals(input.type)) {
// 赠予用户组:输入目标玩家名
String targetName = msg.trim();
if (!targetName.isEmpty()) {
GUIManager guiMgr = plugin.getGui();
if (guiMgr != null) {
guiMgr.handleGiftTargetInput(p, targetName);
}
} else {
p.sendMessage("§c请输入有效的玩家名");
}
return;
}
}
private void handleEconInput(Player admin,
String msg,
String target) {
net.milkbowl.vault.economy.Economy econ
= null;
try {
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault
.economy
.Economy.class);
if (reg != null)
econ = reg.getProvider();
} catch (Exception ignored) {}
if (econ == null) {
admin.sendMessage("§c经济系统不可用");
return;
}
if (!msg.startsWith("+")
&& !msg.startsWith("-")) {
admin.sendMessage("§c格式: §a+100"
+ " §7或 §c-50");
return;
}
try {
String numStr =
msg.substring(1).trim();
double amount =
Double.parseDouble(numStr);
if (amount <= 0) {
admin.sendMessage("§c金额必须>0");
return;
}
if (msg.startsWith("-")) {
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp == null) {
admin.sendMessage("§c" + target
+ " 不在线");
return;
}
if (!econ.has(tp, amount)) {
admin.sendMessage("§c余额不足");
return;
}
econ.withdrawPlayer(tp, amount);
admin.sendMessage("§a扣款成功");
} else {
econ.depositPlayer(target, amount);
admin.sendMessage("§a到账成功");
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误");
}
// Economy已移除,所有经济操作改用债券系统
admin.sendMessage("§c[经济操作] 功能已移除,请使用债券系统");
}
private void handleBondInput(Player admin,
File diff suppressed because it is too large Load Diff
+15 -8
View File
@@ -90,9 +90,8 @@ public class ChatInputManager {
name, "password_salt");
String hash =
PasswordUtils.hash(msg, salt);
boolean mainOk =
db.checkPassword(name, hash);
if (mainOk) {
String result = db.checkPasswordWithFallback(name, hash);
if ("main".equals(result)) {
state.type =
InputType.CHANGE_PWD_STEP2;
state.ticketTitle = "main";
@@ -100,10 +99,7 @@ public class ChatInputManager {
"§e请输入新密码:");
return;
}
boolean tempOk =
db.checkPasswordOrTemp(
name, hash);
if (tempOk) {
if ("temp".equals(result)) {
state.type =
InputType.CHANGE_PWD_STEP2;
state.ticketTitle = "temp";
@@ -164,10 +160,21 @@ public class ChatInputManager {
reset(p);
return;
}
String emailAddr = msg.trim();
// 邮箱后缀黑白名单校验
if (!config.isEmailSuffixAllowed(emailAddr)) {
String mode = config.getEmailValidationMode();
if ("白名单".equals(mode)) {
p.sendMessage("§c该邮箱后缀不在允许列表中");
} else if ("黑名单".equals(mode)) {
p.sendMessage("§c该邮箱后缀被禁止使用");
}
reset(p);
return;
}
String oldEmail = (String)
db.getField(p.getName(),
"email");
String emailAddr = msg.trim();
// 已绑定邮箱:验证登录密码
if (oldEmail != null
&& !oldEmail.isEmpty()) {
@@ -149,18 +149,8 @@ public class CommissionManager {
originalAmount * rate;
if (commission <= 0) return;
// 通过 Vault 发放
var rsp = main.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault
.economy.Economy.class);
if (rsp == null) return;
net.milkbowl.vault.economy.Economy econ =
rsp.getProvider();
if (econ == null) return;
econ.depositPlayer(inviter, commission);
// Economy已移除,佣金提成功能不可用
// 邀请人可通过债券系统获取奖励
// 通知邀请人
Player inviterP =
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+176
View File
@@ -0,0 +1,176 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.scheduler.BukkitRunnable;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 死亡报告系统
* - 记录玩家死亡坐标(一次性,新死亡覆盖)
* - 死亡后发送包含坐标的消息
*/
public class DeathReport implements Listener {
private final Main plugin;
// 内存缓存: player_name → DeathData
private final ConcurrentHashMap<String, DeathData> deathReports = new ConcurrentHashMap<>();
public static class DeathData {
public final String playerName;
public String worldName;
public final double x, y, z;
public long deathTime;
public final UUID playerId;
DeathData(String playerName, UUID playerId, Location loc) {
this.playerName = playerName;
this.playerId = playerId;
this.worldName = loc.getWorld() != null ? loc.getWorld().getName() : "world";
this.x = loc.getX();
this.y = loc.getY();
this.z = loc.getZ();
this.deathTime = System.currentTimeMillis();
}
}
public DeathReport(Main plugin) {
this.plugin = plugin;
initTable();
loadAllFromDB();
}
// ==================== 事件监听 ====================
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getPlayer();
Location deathLoc = player.getLocation();
if (deathLoc == null) return;
DeathData data = new DeathData(player.getName(), player.getUniqueId(), deathLoc);
deathReports.put(player.getName(), data);
saveToDB(data);
// 发送死亡报告消息,带可点击的返回按钮
String timeStr = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(data.deathTime));
Component header = Component.text("§c✦ 你已死亡 ✦ ").append(
Component.text("世界: §f" + data.worldName + " §| §7坐标: §e" +
String.format("%.1f, %.1f, %.1f", data.x, data.y, data.z) + " §7时间: §f" + timeStr)
);
// "返回死亡地点" 按钮 - 通过 /back 命令
Component backButton = Component.text("§e[点击返回死亡地点] ")
.clickEvent(ClickEvent.runCommand("/back"))
.hoverEvent(HoverEvent.showText(Component.text("§7传送到你死亡的位置")));
player.sendMessage(header);
player.sendMessage(backButton);
}
// ==================== 查询/获取 ====================
/**
* 获取玩家的最新死亡坐标(如果有的话)
*/
public DeathData getDeathReport(String playerName) {
return deathReports.get(playerName);
}
/**
* 清除玩家的死亡报告
*/
public void clearDeathReport(String playerName) {
deathReports.remove(playerName);
}
// ==================== 数据库操作 ====================
private void loadAllFromDB() {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name, player_uuid, world, x, y, z, death_time FROM death_reports");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
DeathData data = new DeathData(
rs.getString("player_name"),
UUID.fromString(rs.getString("player_uuid")),
new Location(null, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"))
);
data.worldName = rs.getString("world");
data.deathTime = rs.getLong("death_time");
deathReports.put(data.playerName, data);
}
rs.close();
ps.close();
} catch (SQLException e) {
// 表可能不存在,忽略(会在第一次死亡时自动创建)
}
}
private void saveToDB(DeathData data) {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
PreparedStatement ps = conn.prepareStatement(
"INSERT OR REPLACE INTO death_reports (player_name, player_uuid, world, x, y, z, death_time) VALUES (?, ?, ?, ?, ?, ?, ?)");
ps.setString(1, data.playerName);
ps.setString(2, data.playerId.toString());
ps.setString(3, data.worldName);
ps.setDouble(4, data.x);
ps.setDouble(5, data.y);
ps.setDouble(6, data.z);
ps.setLong(7, data.deathTime);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[死亡报告] 保存失败: " + e.getMessage());
}
}
private void persistAll() {
for (DeathData data : deathReports.values()) {
saveToDB(data);
}
}
// ==================== 初始化 ====================
public void initTable() {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
java.sql.Statement st = conn.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS death_reports ("
+ "player_name TEXT PRIMARY KEY, "
+ "player_uuid TEXT NOT NULL, "
+ "world TEXT, "
+ "x REAL, y REAL, z REAL, "
+ "death_time LONG)");
st.close();
} catch (SQLException e) {
plugin.getLogger().warning("[死亡报告] 建表失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,184 @@
package Sdf1_login;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 统一时长解析器(供 /tempban 与 /chat mute 共用)
*
* parseToExpireMs(String) 返回「绝对到期时间戳(毫秒)」,调用方自行校验最小/最大区间。
*
* 支持的写法:
* 1) 纯数字(Unix 秒时间戳): 253402271999
* 2) 中文日期(绝对): 2026年7月9日
* 3) ISO 日期(绝对): 2025-07-09
* 4) 相对时长(中文/英文单位): 1秒 / 1s / 1年 / 1年2月3天 / 2天12小时
*
* 解析规则优先级:纯数字 → 时间戳;含「年/月/日」且为完整日期格式 → 绝对日期;
* 否则按「数字+单位」逐段累加为相对时长。
*/
public class DurationParser {
private static final Map<String, Long> UNIT_MS = new HashMap<>();
static {
// 秒
UNIT_MS.put("秒", 1000L);
UNIT_MS.put("秒钟", 1000L);
UNIT_MS.put("s", 1000L);
UNIT_MS.put("sec", 1000L);
// 分
UNIT_MS.put("分", 60000L);
UNIT_MS.put("分钟", 60000L);
UNIT_MS.put("mins", 60000L);
UNIT_MS.put("min", 60000L);
UNIT_MS.put("m", 60000L);
// 时
UNIT_MS.put("时", 3600000L);
UNIT_MS.put("小时", 3600000L);
UNIT_MS.put("钟头", 3600000L);
UNIT_MS.put("h", 3600000L);
UNIT_MS.put("hr", 3600000L);
// 天
UNIT_MS.put("天", 86400000L);
UNIT_MS.put("日", 86400000L);
UNIT_MS.put("d", 86400000L);
// 周
UNIT_MS.put("周", 604800000L);
UNIT_MS.put("星期", 604800000L);
UNIT_MS.put("礼拜", 604800000L);
UNIT_MS.put("w", 604800000L);
// 月(按30天近似)
UNIT_MS.put("个月", 2592000000L);
UNIT_MS.put("月", 2592000000L);
UNIT_MS.put("mo", 2592000000L);
// 年(按365天近似)
UNIT_MS.put("年", 31536000000L);
UNIT_MS.put("y", 31536000000L);
}
/** 解析为绝对到期毫秒;无法解析抛出 IllegalArgumentException */
public static long parseToExpireMs(String input)
throws IllegalArgumentException {
if (input == null || input.trim().isEmpty())
throw new IllegalArgumentException("时长为空");
String s = input.trim().replace(" ", "");
// 1) 纯数字 → Unix 秒时间戳
if (s.matches("\\d+")) {
long ts = Long.parseLong(s);
return ts * 1000L;
}
// 2) 中文日期 YYYY年M月D日
Matcher mc = Pattern.compile(
"(\\d{4})年(\\d{1,2})月(\\d{1,2})日").matcher(s);
if (mc.find()) {
return buildDateMillis(Integer.parseInt(mc.group(1)),
Integer.parseInt(mc.group(2)),
Integer.parseInt(mc.group(3)));
}
// 3) ISO 日期 YYYY-MM-DD / YYYY/MM/DD
Matcher mi = Pattern.compile(
"(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})").matcher(s);
if (mi.find()) {
return buildDateMillis(Integer.parseInt(mi.group(1)),
Integer.parseInt(mi.group(2)),
Integer.parseInt(mi.group(3)));
}
// 4) 相对时长(可多段组合)
long rel = parseRelative(s);
if (rel >= 0) {
return System.currentTimeMillis() + rel;
}
throw new IllegalArgumentException("无法解析时长: " + input);
}
private static long parseRelative(String s) {
// 按「数字 + 单位」逐段匹配并累加
StringBuilder units = new StringBuilder();
for (String u : UNIT_MS.keySet()) {
if (units.length() > 0) units.append("|");
// 转义,避免正则特殊字符
units.append(Pattern.quote(u));
}
Pattern p = Pattern.compile(
"([0-9零一二两三四五六七八九十百千万]+)(" + units + ")");
Matcher m = p.matcher(s);
long total = 0;
int found = 0;
while (m.find()) {
found++;
long num = parseNumber(m.group(1));
Long u = UNIT_MS.get(m.group(2));
if (u == null) continue;
total += num * u;
}
return found > 0 ? total : -1;
}
private static long buildDateMillis(int y, int mo, int d) {
Calendar c = Calendar.getInstance();
c.set(y, mo - 1, d, 0, 0, 0);
c.set(Calendar.MILLISECOND, 0);
return c.getTimeInMillis();
}
/** 解析数字(阿拉伯数字优先,其次中文数字 零-九/十百千万/两/壹贰叁…) */
private static long parseNumber(String s) {
if (s.matches("\\d+")) return Long.parseLong(s);
Map<Character, Long> cn = new HashMap<>();
cn.put('零', 0L); cn.put('〇', 0L);
cn.put('一', 1L); cn.put('壹', 1L);
cn.put('二', 2L); cn.put('贰', 2L); cn.put('两', 2L);
cn.put('三', 3L); cn.put('叁', 3L);
cn.put('四', 4L); cn.put('肆', 4L);
cn.put('五', 5L); cn.put('伍', 5L);
cn.put('六', 6L); cn.put('陆', 6L);
cn.put('七', 7L); cn.put('柒', 7L);
cn.put('八', 8L); cn.put('捌', 8L);
cn.put('九', 9L); cn.put('玖', 9L);
long section = 0, num = 0;
boolean hasNum = false;
for (char ch : s.toCharArray()) {
if (cn.containsKey(ch)) {
num = cn.get(ch);
hasNum = true;
} else if (ch == '十') {
if (!hasNum) num = 1;
section += num * 10; hasNum = false; num = 0;
} else if (ch == '百') {
if (!hasNum) num = 1;
section += num * 100; hasNum = false; num = 0;
} else if (ch == '千') {
if (!hasNum) num = 1;
section += num * 1000; hasNum = false; num = 0;
} else if (ch == '万') {
if (!hasNum) num = 1;
section = (section + num) * 10000; hasNum = false; num = 0;
} else {
num = 0;
}
}
return section + (hasNum ? num : 0);
}
/** 格式化绝对到期时间戳为「yyyy年M月d日 HH:mm」 */
public static String formatExpire(long ms) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(ms);
return String.format("%d年%d月%d日 %02d:%02d",
c.get(Calendar.YEAR),
c.get(Calendar.MONTH) + 1,
c.get(Calendar.DAY_OF_MONTH),
c.get(Calendar.HOUR_OF_DAY),
c.get(Calendar.MINUTE));
}
}
+499 -109
View File
@@ -45,6 +45,8 @@ public class GUIManager implements Listener {
"§6§l邀请数据";
public static final String T_USER_MGMT =
"§6§l用户管理";
public static final String T_USER_GROUP =
"§6§l用户组管理";
public static final String T_TASK_CENTER =
"§6§l任务中心";
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
@@ -193,18 +195,7 @@ public class GUIManager implements Listener {
}
// 在 openMain 方法中,Row3 区域加:
// ★ 余额查询 - 所有人都能看 ★
double myBal = 0;
try {
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault.economy
.Economy.class);
if (reg != null)
myBal = reg.getProvider()
.getBalance(p);
} catch (Exception ignored) {}
// 余额查询已移除Vault支持,仅显示债券余额
int myBonds = plugin.getBonds().getBonds(p.getName());
g.setItem(32, mkItem(Material.EMERALD,
"§b§l我的钱包",
@@ -217,6 +208,20 @@ public class GUIManager implements Listener {
"§7给钱、扣钱"));
}
// ★ 领地系统 - 39号位(读取用户偏好)
int landUiMode = 1;
try {
landUiMode = plugin.getDb()
.getUiMode(p.getName());
} catch (Exception ignored) {}
g.setItem(39, mkItem(Material.GRASS_BLOCK,
"§a§l领地系统",
"§7管理你的领地和权限",
"",
landUiMode == 0
? "§e点击打开GUI"
: "§e点击打开CLI"));
// ===== Row0(0-8): 自定义菜单项 =====
@@ -327,28 +332,19 @@ public class GUIManager implements Listener {
public void openBalanceOps(Player p) {
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
fillBg(g);
net.milkbowl.vault.economy.Economy eco = null;
try {
var reg = plugin.getServer().getServicesManager()
.getRegistration(net.milkbowl.vault.economy.Economy.class);
if (reg != null) eco = reg.getProvider();
} catch (Exception ignored) {}
// Vault经济支持已移除,仅显示债券余额
int slot = 0;
for (Player op : Bukkit.getOnlinePlayers()) {
if (slot >= 45) break;
double bal = 0;
if (eco != null) bal = eco.getBalance(op);
boolean isSelf = op.getName().equals(p.getName());
int bondBal = plugin.getBonds().getBonds(op.getName());
g.setItem(slot, mkItem(
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
"§e" + op.getName(),
"§7金币: §a$" + String.format("%.2f", bal),
"§7债券: §e" + bondBal + " §6枚",
"",
"§a左键经济(±) §6右键债券(±)"));
"§a仅显示债券余额(Vault经济支持已移除)"));
slot++;
}
@@ -399,6 +395,8 @@ public class GUIManager implements Listener {
public void openSubMenu(
Player p, String fileName) {
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
String menuTitle =
"§6§l" + fileName
.replace(".txt", "");
@@ -406,21 +404,153 @@ public class GUIManager implements Listener {
null, 54, menuTitle);
fillBg(g);
java.io.File f = new java.io.File(
plugin.getDataFolder(), fileName);
if (!f.exists()) {
p.sendMessage("§c文件不存在: "
java.io.File dir = plugin.getDataFolder();
java.io.File f = null;
String cleanName = fileName.trim();
plugin.getLogger().info(
"[Menu] openSubMenu: searching '"
+ cleanName + "' in '"
+ dir.getAbsolutePath() + "'");
// ★ 第1步:listFiles遍历匹配
java.io.File[] allTxt = dir.listFiles(
(d, n) -> n.toLowerCase()
.endsWith(".txt"));
if (allTxt != null) {
plugin.getLogger().info(
"[Menu] 目录.txt文件数: "
+ allTxt.length);
for (java.io.File ff : allTxt) {
plugin.getLogger().info(
"[Menu] > " + ff.getName()
+ " size=" + ff.length());
if (f == null && ff.getName()
.equalsIgnoreCase(cleanName)) {
f = ff;
}
}
} else {
plugin.getLogger().warning(
"[Menu] listFiles返回null!");
}
// ★ 第2步:直接构造路径
if (f == null) {
f = new java.io.File(dir, cleanName);
plugin.getLogger().info(
"[Menu] 回退构造路径: "
+ f.getAbsolutePath()
+ " exists=" + f.exists()
+ " size=" + f.length());
}
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
List<String> lines = null;
if (f != null) {
// 方法1:NIO UTF-8
try {
lines = java.nio.file.Files
.readAllLines(f.toPath(),
java.nio.charset
.StandardCharsets
.UTF_8);
plugin.getLogger().info(
"[Menu] UTF-8读取: "
+ lines.size() + "行");
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] UTF-8失败: "
+ t.getClass().getSimpleName()
+ ": " + t.getMessage());
}
// 方法2:NIO GBK
if (lines == null || lines.isEmpty()) {
try {
lines = java.nio.file.Files
.readAllLines(f.toPath(),
java.nio.charset
.Charset
.forName("GBK"));
plugin.getLogger().info(
"[Menu] GBK读取: "
+ lines.size() + "行");
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] GBK失败: "
+ t.getMessage());
}
}
// 方法3:FileInputStream原始字节
if (lines == null || lines.isEmpty()) {
try {
java.io.FileInputStream fis =
new java.io.FileInputStream(f);
byte[] raw = fis.readAllBytes();
fis.close();
if (raw.length > 0) {
plugin.getLogger().info(
"[Menu] 原始字节: "
+ raw.length + "B");
// 尝试UTF-8解码
String content = new String(raw,
java.nio.charset
.StandardCharsets
.UTF_8);
java.util.List<String> rawLines =
java.util.Arrays.asList(
content.split("\\r?\\n"));
lines = rawLines;
plugin.getLogger().info(
"[Menu] 原始读取: "
+ lines.size() + "行");
}
} catch (Throwable t) {
plugin.getLogger().warning(
"[Menu] 原始读取失败: "
+ t.getMessage());
}
}
}
// ★ 打印前5行调试
if (lines != null && !lines.isEmpty()) {
plugin.getLogger().info(
"[Menu] 最终行数: "
+ lines.size());
for (int i = 0;
i < Math.min(5, lines.size());
i++) {
plugin.getLogger().info(
"[Menu] L" + (i + 1)
+ ": [" + lines.get(i)
+ "]");
}
}
if (lines == null || lines.isEmpty()) {
p.sendMessage("§c§l子菜单读取失败§r §7"
+ fileName);
p.sendMessage("§7文件: "
+ (f != null
? f.getAbsolutePath()
+ " (" + f.length() + "B)"
: "null"));
p.sendMessage("§7存在: "
+ (f != null ? f.exists() : "N/A"));
if (allTxt != null) {
p.sendMessage("§7目录.txt文件:");
for (java.io.File ff : allTxt) {
p.sendMessage("§7 "
+ ff.getName()
+ " (" + ff.length()
+ "B)");
}
}
return;
}
try {
List<String> lines =
java.nio.file.Files
.readAllLines(
f.toPath(),
java.nio.charset
.StandardCharsets
.UTF_8);
List<String[]> items =
new ArrayList<>();
StringBuilder buf =
@@ -484,11 +614,6 @@ public class GUIManager implements Listener {
it.setItemMeta(im);
g.setItem(i, it);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 子菜单读取失败: "
+ e.getMessage());
}
g.setItem(49, mkItem(
Material.ARROW,
@@ -609,78 +734,12 @@ public class GUIManager implements Listener {
Collections.shuffle(candidates);
// 3. 主线程分批检测,每tick处理30个坐标
final int[] idx = {0};
new BukkitRunnable() {
@Override
public void run() {
World w = p.getWorld();
int end = Math.min(
idx[0] + BIOME_BATCH,
candidates.size());
for (int i = idx[0]; i < end; i++) {
int x = candidates.get(i)[0];
int z = candidates.get(i)[1];
// 跳过未加载区块
if (!w.isChunkLoaded(x >> 4, z >> 4))
continue;
// 获取地表高度
int surfaceY =
w.getHighestBlockYAt(x, z);
// 在地表Y取群系(避免地下误判)
String key = w.getBlockAt(
x, surfaceY, z)
.getBiome()
.getKey().toString();
String cat =
mapBiomeCategory(
key, w.getName());
if (category.equals(cat)) {
this.cancel();
Bukkit.getScheduler()
.runTask(plugin, () -> {
Location tp =
new Location(
w,
x + 0.5,
surfaceY + 1,
z + 0.5);
p.teleport(tp);
p.sendMessage(
"§a已传送到 "
+ category
+ " ("
+ x + ","
+ (surfaceY + 1)
+ ","
+ z + ")");
});
return;
}
}
idx[0] = end;
if (idx[0] >= candidates.size()) {
this.cancel();
Bukkit.getScheduler()
.runTask(plugin, () ->
p.sendMessage(
"§c附近未找到 "
+ category
+ ",请换个位置再试"));
}
}
}.runTaskTimer(plugin, 0L, 1L);
new BiomeScanTask(plugin, p, category, candidates).runTaskTimer(plugin, 0L, 1L);
}
private String mapBiomeCategory(
private static String mapBiomeCategory(
String biome, String worldName) {
if (biome == null) return "平原";
String b = biome.toLowerCase();
@@ -863,7 +922,7 @@ public class GUIManager implements Listener {
// ===== 余额操作 =====
if ("§d§l余额操作".equals(title)) {
event.setCancelled(true);
if (raw == 49) { openAdmin(p); return; }
if (raw == 49) { plugin.openMyWallet(p); return; }
if (raw < 0 || raw >= 45) return;
ItemStack item = event.getView()
.getTopInventory().getItem(raw);
@@ -927,6 +986,13 @@ public class GUIManager implements Listener {
return;
}
// ===== 用户组管理 =====
if (T_USER_GROUP.equals(title)) {
event.setCancelled(true);
handleUserGroupClick(p, raw, event.isLeftClick(), event.isRightClick(), event.isShiftClick());
return;
}
// ===== 打包确认 =====
if ("§6§l选择包装方式".equals(title)) {
event.setCancelled(true);
@@ -2711,6 +2777,279 @@ public class GUIManager implements Listener {
plugin.getConfig2().adminTag);
}
// ==================== 用户组管理GUI ====================
/** 打开用户组管理主界面 */
public void openUserGroup(Player p) {
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
Inventory g = Bukkit.createInventory(null, 54, T_USER_GROUP);
int bonds = plugin.getBonds().getBonds(p.getName());
g.setItem(4, mkItem(Material.EMERALD, "§b§l我的债券: " + bonds + " 张", "§7用于购买/续费用户组"));
// Row1 (9-17): 可购买的用户组
List<UserGroupManager.UserGroupConfig> allGroups = ugm.getAllGroups();
int slot = 9;
for (UserGroupManager.UserGroupConfig cfg : allGroups) {
if (slot > 17) break;
if (cfg.joinPrice <= 0) continue; // 不开放付费的组不显示
boolean isMember = ugm.isPlayerInGroup(p.getName(), cfg.name);
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
if (isMember) {
// 已加入 → 显示在 Row2
continue;
}
Material mat = isMember ? Material.LIME_DYE : Material.WHITE_DYE;
List<String> lore = new ArrayList<>();
lore.add("§7价格: §e" + cfg.joinPrice + " 张债券");
if (cfg.durationMinutes > 0) {
long days = cfg.durationMinutes / 1440;
long hours = (cfg.durationMinutes % 1440) / 60;
String dur = days > 0 ? days + "天" : hours + "小时";
lore.add("§7有效期: §e" + dur);
} else {
lore.add("§7有效期: §a永久");
}
if (cfg.renewPrice > 0) {
lore.add("§7续费价格: §e" + cfg.renewPrice + " 张债券");
}
lore.add("");
lore.add("§a左键: 购买给自己");
lore.add("§b右键: 赠送给他人");
g.setItem(slot, mkItem(mat, cfg.displayColor + "§l" + displayName + " §7(购买)", lore.toArray(new String[0])));
slot++;
}
// Row2 (18-26): 我的用户组
slot = 18;
List<Map<String, Object>> myGroups = ugm.getPlayerGroups(p.getName());
for (Map<String, Object> grp : myGroups) {
if (slot > 26) break;
String grpName = String.valueOf(grp.get("group_name"));
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
if (cfg == null) continue;
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
long expiry = ((Number) grp.getOrDefault("expiry_time", 0)).longValue();
long now = System.currentTimeMillis();
List<String> lore = new ArrayList<>();
if (expiry > 0) {
if (expiry <= now) {
lore.add("§c已过期");
} else {
long remainMs = expiry - now;
long remainMin = remainMs / 60000;
long rDays = remainMin / 1440;
long rHours = (remainMin % 1440) / 60;
lore.add("§7剩余: §e" + (rDays > 0 ? rDays + "天" : rHours + "小时"));
lore.add("§7到期: §7" + new java.text.SimpleDateFormat("MM-dd HH:mm").format(new java.util.Date(expiry)));
}
} else {
lore.add("§7有效期: §a永久");
}
// 自动续费状态
boolean autoRenew = getAutoRenew(p.getName(), grpName);
lore.add("§7自动续费: " + (autoRenew ? "§a开启" : "§c关闭"));
lore.add("");
lore.add("§a左键: 续费");
lore.add("§b右键: 赠送给他人");
lore.add("§eShift+右键: " + (autoRenew ? "§c关闭" : "§a开启") + "自动续费");
g.setItem(slot, mkItem(Material.LIME_DYE, cfg.displayColor + "§l" + displayName, lore.toArray(new String[0])));
slot++;
}
// Row4 (40): 赠送入口
g.setItem(40, mkItem(Material.SPECTRAL_ARROW, "§e§l赠予用户组", "§7为其他玩家购买/续费用户组"));
// Row5 (49): 返回
g.setItem(49, mkItem(Material.ARROW, "§7返回"));
fillBg(g);
p.openInventory(g);
}
/** 处理用户组GUI点击 */
public boolean handleUserGroupClick(Player p, int raw, boolean left, boolean right, boolean shift) {
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) return false;
if (raw == 49) { openMain(p); return true; } // 返回
// 赠送按钮
if (raw == 40) {
p.closeInventory();
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
plugin.getCDK().requestInput(p, "gift_target", "");
return true;
}
// Row1: 可购买的组 (9-17)
if (raw >= 9 && raw <= 17) {
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
String grpName = extractGroupNameFromItem(item);
if (grpName == null) return false;
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
if (cfg == null) return false;
if (left && !right) {
// 购买给自己
p.closeInventory();
p.sendMessage("§7正在购买 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
String err = ugm.joinGroupByPrice(p.getName(), cfg.name);
if (err != null) {
p.sendMessage("§c购买失败: " + err);
} else {
p.sendMessage("§a§l购买成功! §7你已加入用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
}
return true;
} else if (right && !left) {
// 赠送给他人
p.closeInventory();
// 记住赠送的组名
pendingGiftGroup.put(p.getName(), grpName);
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
plugin.getCDK().requestInput(p, "gift_target", grpName);
return true;
}
}
// Row2: 我的组 (18-26)
if (raw >= 18 && raw <= 26) {
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
String grpName = extractGroupNameFromItem(item);
if (grpName == null) return false;
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
if (cfg == null) return false;
if (shift && right) {
// Shift+右键: 切换自动续费
boolean current = getAutoRenew(p.getName(), grpName);
setAutoRenew(p.getName(), grpName, !current);
p.sendMessage("§a自动续费已" + (!current ? "开启" : "关闭") + ": §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
openUserGroup(p); // 刷新界面
return true;
}
if (left && !right) {
// 续费
p.closeInventory();
p.sendMessage("§7正在续费 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
String err = ugm.renewGroup(p.getName(), cfg.name);
if (err != null) {
p.sendMessage("§c续费失败: " + err);
} else {
p.sendMessage("§a§l续费成功! §7用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7已续期");
}
return true;
} else if (right && !left && !shift) {
// 赠送给他人
p.closeInventory();
pendingGiftGroup.put(p.getName(), grpName);
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
plugin.getCDK().requestInput(p, "gift_target", grpName);
return true;
}
}
return false;
}
/** 处理赠予玩家名输入 */
public void handleGiftTargetInput(Player p, String targetName) {
String grpName = pendingGiftGroup.remove(p.getName());
if (grpName == null) return;
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
if (cfg == null) { p.sendMessage("§c用户组不存在: " + grpName); return; }
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
// 检查目标玩家是否存在
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr != null && !dbMgr.userExists(targetName)) {
p.sendMessage("§c玩家 §e" + targetName + " §c不存在");
return;
}
// ★ 使用新的赠送方法:送礼人(p)付费,收礼人(targetName)免费获得
p.sendMessage("§7正在为 §e" + targetName + " §7购买用户组 §e" + displayName + " §7...");
String err = ugm.giftGroupToPlayer(p.getName(), targetName, cfg.name);
if (err != null) {
p.sendMessage("§c赠予失败: " + err);
} else {
p.sendMessage("§a§l赠予成功!");
}
}
// ★ 赠予流程:记录赠送的组名
private final Map<String, String> pendingGiftGroup = new ConcurrentHashMap<>();
// ★ 自动续费偏好(per-player)
private final Map<String, Boolean> autoRenewCache = new ConcurrentHashMap<>();
private boolean getAutoRenew(String player, String group) {
String key = player + "|" + group;
if (autoRenewCache.containsKey(key)) return autoRenewCache.get(key);
// 从数据库读取
boolean val = false;
try {
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr != null) {
val = dbMgr.getPlayerAutoRenew(player, group);
}
} catch (Exception ignored) {}
autoRenewCache.put(key, val);
return val;
}
private void setAutoRenew(String player, String group, boolean value) {
String key = player + "|" + group;
autoRenewCache.put(key, value);
try {
DatabaseManager dbMgr = plugin.getDb();
if (dbMgr != null) {
dbMgr.setPlayerAutoRenew(player, group, value);
}
} catch (Exception ignored) {}
}
/** 从GUI物品名称中提取用户组名(去掉格式化前缀和后缀) */
private String extractGroupNameFromItem(ItemStack item) {
if (item == null || !item.hasItemMeta()) return null;
ItemMeta meta = item.getItemMeta();
if (meta == null || meta.getDisplayName() == null) return null;
String name = meta.getDisplayName();
// 去掉所有§格式化代码
name = name.replaceAll("§[0-9a-fk-orA-FK-OR]", "");
// 去掉后缀 " (购买)"
name = name.replaceAll("\\s*\\(购买\\)$", "").trim();
// 匹配已知组名
UserGroupManager ugm = plugin.getUserGroup();
if (ugm == null) return null;
for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) {
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
if (name.equals(displayName) || name.equals(cfg.name)) {
return cfg.name;
}
}
return null;
}
private void fillBg(Inventory inv) {
ItemStack glass = new ItemStack(
Material.GRAY_STAINED_GLASS_PANE);
@@ -2774,4 +3113,55 @@ public class GUIManager implements Listener {
}
return it;
}
/** 命名静态内部类替代匿名BukkitRunnable,修复Paper 26 ClassLoader兼容性 */
private static class BiomeScanTask extends BukkitRunnable {
private final Main plugin;
private final Player p;
private final String category;
private final java.util.List<int[]> candidates;
private int idx = 0;
BiomeScanTask(Main plugin, Player p, String category, java.util.List<int[]> candidates) {
this.plugin = plugin;
this.p = p;
this.category = category;
this.candidates = candidates;
}
@Override
public void run() {
World w = p.getWorld();
int end = Math.min(idx + BIOME_BATCH, candidates.size());
for (int i = idx; i < end; i++) {
int x = candidates.get(i)[0];
int z = candidates.get(i)[1];
if (!w.isChunkLoaded(x >> 4, z >> 4)) continue;
int surfaceY = w.getHighestBlockYAt(x, z);
String key = w.getBlockAt(x, surfaceY, z)
.getBiome().getKey().toString();
String cat = mapBiomeCategory(key, w.getName());
if (category.equals(cat)) {
this.cancel();
Bukkit.getScheduler().runTask(plugin, () -> {
Location tp = new Location(w, x + 0.5, surfaceY + 1, z + 0.5);
p.teleport(tp);
p.sendMessage("§a已传送到 " + category + " (" + x + "," + (surfaceY + 1) + "," + z + ")");
});
return;
}
}
idx = end;
if (idx >= candidates.size()) {
this.cancel();
Bukkit.getScheduler().runTask(plugin, () ->
p.sendMessage("§c附近未找到 " + category + ",请换个位置再试"));
}
}
}
}
+2 -7
View File
@@ -1,7 +1,6 @@
package Sdf1_login;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Item;
@@ -12,7 +11,6 @@ import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.sql.Date;
@@ -287,11 +285,8 @@ public class GarbageManager {
.decode(base64);
String json = new String(bytes,
StandardCharsets.UTF_8);
Type type = new TypeToken<
Map<String, Object>>() {}
.getType();
Map<String, Object> map =
new Gson().fromJson(json, type);
new Gson().fromJson(json, Map.class);
map = fixNumberTypes(map);
return ItemStack.deserialize(map);
} catch (Exception e) {
@@ -425,7 +420,7 @@ public class GarbageManager {
"§7已是最后一页"));
}
// 52 格:返回
// 52 格:返回主菜单
g.setItem(52, mk(Material.ARROW,
"§7返回",
"§7返回主菜单"));
+19 -30
View File
@@ -194,28 +194,21 @@ public class GiftManager {
giveItem(p, Material.WOODEN_AXE);
giveItem(p, Material.WOODEN_SHOVEL);
} else if (stage == 2) {
if (plugin.getEconomy() != null) {
double before = plugin.getEconomy()
.getBalance(p);
plugin.getEconomy()
.depositPlayer(p, 500);
double after = plugin.getEconomy()
.getBalance(p);
p.sendMessage("§a§l[礼包] §f游戏币: §e"
+ (long) before
+ "§f → §a" + (long) after);
// 游戏币改为债券
BondManager bm = plugin.getBonds();
if (bm != null) {
int before = bm.getBonds(p.getName());
bm.addBonds(p.getName(), 500, "gift_stage2", "", "新手礼包", "阶段2奖励");
int after = bm.getBonds(p.getName());
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
}
} else if (stage == 3) {
if (plugin.getEconomy() != null) {
double before = plugin.getEconomy()
.getBalance(p);
plugin.getEconomy()
.depositPlayer(p, 1500);
double after = plugin.getEconomy()
.getBalance(p);
p.sendMessage("§a§l[礼包] §f游戏币: §e"
+ (long) before
+ "§f → §a" + (long) after);
BondManager bm = plugin.getBonds();
if (bm != null) {
int before = bm.getBonds(p.getName());
bm.addBonds(p.getName(), 1500, "gift_stage3", "", "新手礼包", "阶段3奖励");
int after = bm.getBonds(p.getName());
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
}
giveItem(p, Material.IRON_PICKAXE);
giveItem(p, Material.IRON_AXE);
@@ -225,16 +218,12 @@ public class GiftManager {
giveItem(p, Material.IRON_LEGGINGS);
giveItem(p, Material.IRON_BOOTS);
} else if (stage == 4) {
if (plugin.getEconomy() != null) {
double before = plugin.getEconomy()
.getBalance(p);
plugin.getEconomy()
.depositPlayer(p, 2000);
double after = plugin.getEconomy()
.getBalance(p);
p.sendMessage("§a§l[礼包] §f游戏币: §e"
+ (long) before
+ "§f → §a" + (long) after);
BondManager bm = plugin.getBonds();
if (bm != null) {
int before = bm.getBonds(p.getName());
bm.addBonds(p.getName(), 2000, "gift_stage4", "", "新手礼包", "阶段4奖励");
int after = bm.getBonds(p.getName());
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
}
giveItem(p, Material.DIAMOND_HELMET);
giveItem(p, Material.DIAMOND_CHESTPLATE);
+472
View File
@@ -0,0 +1,472 @@
package Sdf1_login;
import org.bukkit.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Home传送点管理器
* 支持: sethome, delhome, home, homes
* 集成用户组系统: home_limit (0=全局默认, >0=组限制, -1=无限)
*/
public class HomeManager implements Listener {
private final Main plugin;
// 床位置缓存: player_name → Location (用于自动记录床位置)
private final Map<String, Location> bedLocations = new ConcurrentHashMap<>();
public HomeManager(Main plugin) {
this.plugin = plugin;
initDatabase();
}
// ==================== 数据结构 ====================
public static class HomeData {
public String name;
public String world;
public double x, y, z;
public float yaw, pitch;
public long createdAt;
public Location toLocation(World world) {
return new Location(world, x, y, z, yaw, pitch);
}
}
// ==================== 数据库初始化 ====================
private void initDatabase() {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) {
plugin.getLogger().warning("[Home] 无法获取数据库连接");
return;
}
java.sql.Statement st = db.createStatement();
// homes表已在DatabaseManager中创建,此处确保索引
st.execute("CREATE INDEX IF NOT EXISTS idx_homes_player ON homes(player_name)");
st.close();
plugin.getLogger().info("[Home] 数据库初始化完成");
} catch (Exception e) {
plugin.getLogger().warning("[Home] 数据库初始化失败: " + e.getMessage());
}
}
// ==================== 命令处理 ====================
public boolean handleCommand(Player player, String label, String[] args) {
String lowerLabel = label.toLowerCase();
switch (lowerLabel) {
case "sethome":
return handleSetHome(player, args);
case "delhome":
return handleDelHome(player, args);
case "home":
return handleHome(player, args);
case "homes":
return handleHomes(player, args);
default:
return false;
}
}
/**
* /sethome <名称> - 设置家
*/
private boolean handleSetHome(Player player, String[] args) {
if (args.length == 0) {
player.sendMessage("§c用法: /sethome <名称>");
return true;
}
String homeName = args[0].toLowerCase();
// 校验名称格式
if (!homeName.matches("^[a-zA-Z0-9_]{1,16}$")) {
player.sendMessage("§c家名只能包含字母、数字、下划线,长度1-16");
return true;
}
// 检查家数量限制
int maxHomes = getMaxHomes(player);
if (maxHomes != -1) {
List<HomeData> homes = getHomes(player.getName());
if (homes.size() >= maxHomes) {
player.sendMessage("§c你已达到家数量上限 (" + maxHomes + "个)");
return true;
}
}
// 检查是否已存在同名家
if (homeExists(player.getName(), homeName)) {
player.sendMessage("§c你已有一个名为 §e" + homeName + " §c的家");
return true;
}
// 保存家
Location loc = player.getLocation();
saveHome(player.getName(), homeName, loc);
player.sendMessage("§a家 §e" + homeName + " §a已设置!");
player.sendMessage("§7使用 §e/home " + homeName + " §7传送回家");
return true;
}
/**
* /delhome <名称> - 删除家
*/
private boolean handleDelHome(Player player, String[] args) {
if (args.length == 0) {
player.sendMessage("§c用法: /delhome <名称>");
return true;
}
String homeName = args[0].toLowerCase();
if (!homeExists(player.getName(), homeName)) {
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
return true;
}
deleteHome(player.getName(), homeName);
player.sendMessage("§a家 §e" + homeName + " §a已删除!");
return true;
}
/**
* /home [名称] - 传送到家
*/
private boolean handleHome(Player player, String[] args) {
if (args.length == 0) {
// 无参数: 传送到第一个家
List<HomeData> homes = getHomes(player.getName());
if (homes.isEmpty()) {
player.sendMessage("§c你还没有设置任何家");
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
return true;
}
teleportToHome(player, homes.get(0));
return true;
}
String homeName = args[0].toLowerCase();
HomeData home = getHome(player.getName(), homeName);
if (home == null) {
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
return true;
}
teleportToHome(player, home);
return true;
}
/**
* /homes - 查看所有家
*/
private boolean handleHomes(Player player, String[] args) {
List<HomeData> homes = getHomes(player.getName());
if (homes.isEmpty()) {
player.sendMessage("§e你还没有设置任何家");
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
return true;
}
int maxHomes = getMaxHomes(player);
player.sendMessage("§6§l你的家 §7(" + homes.size() + (maxHomes == -1 ? "/" + "∞" : "/" + maxHomes) + ")");
player.sendMessage("§7────────────────────");
for (HomeData home : homes) {
String worldName = home.world != null ? home.world : "未知";
player.sendMessage("§e • " + home.name + " §7[" + worldName + "]");
}
player.sendMessage("§7────────────────────");
player.sendMessage("§7使用 §e/home <名称> §7传送");
player.sendMessage("§7使用 §e/sethome <名称> §7设置新家");
player.sendMessage("§7使用 §e/delhome <名称> §7删除家");
return true;
}
// ==================== 床事件监听 ====================
/**
* 玩家上床时自动记录床位置
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerBedEnter(PlayerBedEnterEvent event) {
Player player = event.getPlayer();
Location bedLoc = event.getBed().getLocation();
bedLocations.put(player.getName(), bedLoc);
// 自动保存为"bed"家(如果不存在)
if (!homeExists(player.getName(), "bed")) {
saveHome(player.getName(), "bed", bedLoc);
player.sendMessage("§a床位置已自动保存为 §e/home bed");
}
}
/**
* 玩家重生时更新床位置
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
Player player = event.getPlayer();
Location respawnLoc = event.getRespawnLocation();
// 如果重生点是床位置,更新bed家
Location bedLoc = bedLocations.get(player.getName());
if (bedLoc != null && bedLoc.getWorld() != null) {
if (respawnLoc.getWorld() != null &&
respawnLoc.getWorld().getName().equals(bedLoc.getWorld().getName()) &&
respawnLoc.distance(bedLoc) < 5) {
// 重生点就是床,更新bed家
updateHome(player.getName(), "bed", respawnLoc);
}
}
}
// ==================== 家数量限制 ====================
/**
* 获取玩家最大家数量
* @return -1=无限, 0=使用全局默认, >0=用户组独立限制
*/
private int getMaxHomes(Player player) {
UserGroupManager userGroupMgr = plugin.getUserGroup();
if (userGroupMgr == null) {
return getGlobalHomeLimit();
}
// 获取玩家最高优先级用户组
UserGroupManager.UserGroupConfig highestGroup = userGroupMgr.getHighestGroup(player.getName());
if (highestGroup == null) {
return getGlobalHomeLimit();
}
// 获取用户组的home_limit配置
int groupLimit = userGroupMgr.getHomeLimit(highestGroup.name);
if (groupLimit == -1) {
// 无限
return -1;
} else if (groupLimit == 0) {
// 跟随全局
return getGlobalHomeLimit();
} else {
// 独立限制
return groupLimit;
}
}
/**
* 获取全局家数量限制
* 读取area_config中的max_home_per_player配置(管理员可在Web后台调整)
* 解析失败或值<=0时回退到默认5
*/
private int getGlobalHomeLimit() {
try {
if (plugin.areaProtection != null) {
int limit = plugin.areaProtection.getGlobalMaxHomePerPlayer();
if (limit > 0) {
return limit;
}
}
} catch (Exception ignored) {}
// 默认5个家
return 5;
}
// ==================== 数据库操作 ====================
private boolean homeExists(String playerName, String homeName) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return false;
PreparedStatement ps = db.prepareStatement(
"SELECT COUNT(*) FROM homes WHERE player_name=? AND home_name=?");
ps.setString(1, playerName);
ps.setString(2, homeName);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next() && rs.getInt(1) > 0;
rs.close();
ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
private List<HomeData> getHomes(String playerName) {
List<HomeData> homes = new ArrayList<>();
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return homes;
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM homes WHERE player_name=? ORDER BY created_at");
ps.setString(1, playerName);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
HomeData home = new HomeData();
home.name = rs.getString("home_name");
home.world = rs.getString("world");
home.x = rs.getDouble("x");
home.y = rs.getDouble("y");
home.z = rs.getDouble("z");
home.yaw = rs.getFloat("yaw");
home.pitch = rs.getFloat("pitch");
home.createdAt = rs.getLong("created_at");
homes.add(home);
}
rs.close();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 获取家列表失败: " + e.getMessage());
}
return homes;
}
private HomeData getHome(String playerName, String homeName) {
List<HomeData> homes = getHomes(playerName);
for (HomeData home : homes) {
if (home.name.equalsIgnoreCase(homeName)) {
return home;
}
}
return null;
}
private void saveHome(String playerName, String homeName, Location loc) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return;
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO homes (player_name, home_name, world, x, y, z, yaw, pitch, created_at) "
+ "VALUES (?,?,?,?,?,?,?,?,?)");
ps.setString(1, playerName);
ps.setString(2, homeName);
ps.setString(3, loc.getWorld() != null ? loc.getWorld().getName() : "world");
ps.setDouble(4, loc.getX());
ps.setDouble(5, loc.getY());
ps.setDouble(6, loc.getZ());
ps.setFloat(7, loc.getYaw());
ps.setFloat(8, loc.getPitch());
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 保存家失败: " + e.getMessage());
}
}
private void updateHome(String playerName, String homeName, Location loc) {
saveHome(playerName, homeName, loc); // INSERT OR REPLACE 自动更新
}
private void deleteHome(String playerName, String homeName) {
try {
java.sql.Connection db = plugin.getDb().getConnection();
if (db == null) return;
PreparedStatement ps = db.prepareStatement(
"DELETE FROM homes WHERE player_name=? AND home_name=?");
ps.setString(1, playerName);
ps.setString(2, homeName);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
plugin.getLogger().warning("[Home] 删除家失败: " + e.getMessage());
}
}
/**
* 传送玩家到家
*/
private void teleportToHome(Player player, HomeData home) {
World world = Bukkit.getWorld(home.world);
if (world == null) {
player.sendMessage("§c世界 §e" + home.world + " §c不存在");
return;
}
Location loc = home.toLocation(world);
// 安全检查: 确保位置安全
if (loc.getBlock().getType().isSolid()) {
// 向上寻找安全位置
for (int y = (int) loc.getY(); y < 256; y++) {
Location safeLoc = new Location(world, loc.getX(), y + 1, loc.getZ());
if (!safeLoc.getBlock().getType().isSolid()) {
loc = safeLoc;
break;
}
}
}
player.teleport(loc);
player.sendMessage("§a已传送到家 §e" + home.name);
}
// ==================== Tab补全 ====================
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!(sender instanceof Player)) {
return Collections.emptyList();
}
Player player = (Player) sender;
String cmdName = command.getName().toLowerCase();
if (cmdName.equals("home") || cmdName.equals("delhome")) {
if (args.length == 1) {
List<String> homes = new ArrayList<>();
for (HomeData home : getHomes(player.getName())) {
if (home.name.toLowerCase().startsWith(args[0].toLowerCase())) {
homes.add(home.name);
}
}
return homes;
}
} else if (cmdName.equals("sethome") || cmdName.equals("homes")) {
return Collections.emptyList();
}
return Collections.emptyList();
}
// ==================== 清理 ====================
public void shutdown() {
bedLocations.clear();
}
}
@@ -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();
@@ -0,0 +1,80 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.IOException;
/**
* 背包滚动自动备份。
* <p>
* ★ 功能:
* 1. 由 Main 的 15 分钟定时任务驱动,遍历所有在线玩家。
* 2. 在 PVP 竞技场 / 练习场(测试场)的玩家不备份 —— 避免竞技场装备覆盖真实背包。
* 3. 序列化 主背包 / 护甲 / 副手(extra) / 等级 / 经验,写入 inv_backup_auto 表。
* 4. 每玩家保留最近 3 份(滚动覆盖),由 DatabaseManager.saveAutoBackup 负责清理。
* <p>
* 与登录时的 saveInventoryBackup 共用同一套存储语义(level=玩家等级,
* experience=当前等级进度 p.getExp()),便于后续统一还原。
*/
public class InventoryAutoBackup {
private final Main plugin;
/** 备份周期:15 分钟(单位 tick) */
public static final long INTERVAL_TICKS = 15L * 60L * 20L;
public InventoryAutoBackup(Main plugin) {
this.plugin = plugin;
}
/** 对所有在线玩家执行一次滚动备份 */
public void runBackup() {
for (Player p : Bukkit.getOnlinePlayers()) {
try {
backupPlayer(p);
} catch (Exception e) {
plugin.getLogger().warning(
"[自动备份] 玩家 " + p.getName()
+ " 备份失败: " + e.getMessage());
}
}
}
private void backupPlayer(Player p) {
String name = p.getName();
// ★ 跳过 PVP 竞技场:竞技场内不备份,避免 PVP 装备覆盖真实背包
if (plugin.getPVPArenaManager().isInPVPArena(name)) {
return;
}
// ★ 跳过练习场 / 测试世界
if (plugin.getPVPTestManager().isInTestWorld(p)) {
return;
}
ItemStack[] contents = p.getInventory().getContents();
ItemStack[] armor = p.getInventory().getArmorContents();
ItemStack[] extra = p.getInventory().getExtraContents();
String contentsB64, armorB64, extraB64;
try {
contentsB64 = InventorySerializer.toBase64(contents);
armorB64 = InventorySerializer.toBase64(armor);
extraB64 = InventorySerializer.toBase64(extra);
} catch (IOException e) {
plugin.getLogger().warning(
"[自动备份] 序列化失败 (" + name + "): "
+ e.getMessage());
return;
}
int level = p.getLevel();
double experience = p.getExp(); // 当前等级进度 0.0~1.0
plugin.getDb().saveAutoBackup(
name, contentsB64, armorB64, extraB64,
level, experience);
plugin.getLogger().info("[自动备份] " + name + " 背包已备份");
}
}
@@ -0,0 +1,550 @@
package Sdf1_login;
import Sdf1_login.AreaProtection.AreaConfig;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.Inventory;
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;
/**
* 领地方块/容器记录系统 —— 回声碎片(echo_shard)工具。
* <p>
* ★ 功能:
* 1. 领地内方块破坏/放置 → 自动写入 land_block_log
* 2. 领地内容器打开/拿取 → 自动写入 land_container_log
* 3. 玩家手持回声碎片右键/左键方块或容器 → 聊天栏打印该坐标的历史记录
* 4. /landrec give [玩家] → 发放回声碎片(由插件/管理员发放)
* <p>
* 记录保留 7 天滚动(由 Main 的每日清理定时器调用 prune)。
*/
public class LandRecordManager implements Listener {
private final Main plugin;
/** 回声碎片物品的 NBT 标识 key */
public static final String ECHO_SHARD_KEY = "sdf1_echo_shard";
/** 回声碎片物品材质(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;
/** 记录保留天数 */
public static final int RETENTION_DAYS = 7;
private final SimpleDateFormat sdf =
new SimpleDateFormat("MM-dd HH:mm");
/**
* 容器打开时的快照:用于关闭时计算"拿了什么"。
* key = 玩家名(MC 同一时刻只能打开一个容器 GUI,故按玩家唯一即可)。
*/
private final Map<String, ContainerSnapshot> openSnapshots =
new ConcurrentHashMap<>();
static class ContainerSnapshot {
String landName;
int landId;
String world;
int x, y, z;
String containerType;
Map<String, Integer> before = new HashMap<>();
}
public LandRecordManager(Main plugin) {
this.plugin = plugin;
}
// ==================== 回声碎片物品 ====================
/** 创建一枚回声碎片(带 NBT 标识,便于精准识别) */
public ItemStack createEchoShard() {
ItemStack item = new ItemStack(ECHO_SHARD_MATERIAL);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§b§l回声碎片");
meta.setLore(Arrays.asList(
"§7右键/左键 方块或容器",
"§7查看领地内的破坏/拿取记录",
"§8(领地监控工具)"));
NamespacedKey key =
new NamespacedKey(plugin, ECHO_SHARD_KEY);
meta.getPersistentDataContainer().set(
key, PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta);
}
return item;
}
/** 判断物品是否为回声碎片(材质 + NBT 双重校验) */
public boolean isEchoShard(ItemStack item) {
if (item == null || item.getType() != ECHO_SHARD_MATERIAL)
return false;
ItemMeta meta = item.getItemMeta();
if (meta == null) return false;
NamespacedKey key =
new NamespacedKey(plugin, ECHO_SHARD_KEY);
return meta.getPersistentDataContainer().has(
key, PersistentDataType.BYTE);
}
/** 直接发放一枚回声碎片给玩家(GUI 菜单调用) */
public void giveEchoShard(Player p) {
ItemStack shard = createEchoShard();
HashMap<Integer, ItemStack> left =
p.getInventory().addItem(shard);
if (!left.isEmpty()) {
p.sendMessage("§c§l[回声碎片] §f你的背包已满,无法发放");
} else {
p.sendMessage("§a§l[回声碎片] §f你获得了§b回声碎片§f,右键/左键方块或容器查看领地记录");
}
}
// ==================== 记录写入(由 AreaProtection 钩子调用)====================
/** 记录方块破坏 */
public void recordBlockBreak(AreaConfig ac, Player p,
Block block) {
recordBlock(ac, p, block, "break");
}
/** 记录方块放置 */
public void recordBlockPlace(AreaConfig ac, Player p,
Block block) {
recordBlock(ac, p, block, "place");
}
/** 记录红石元器件状态变化(压力板、按钮、拉杆等) */
public void recordRedstoneStateChange(AreaConfig ac, Player p,
Block block) {
recordBlock(ac, p, block, "state_change");
}
/** ★ 记录告示牌放置/编辑(含内容) */
public void recordSignEdit(AreaConfig ac, Player p, Block block,
String[] lines, boolean isEdit) {
try {
int landId = plugin.areaProtection.getLandIdFromDb(ac.name);
// 拼接4行内容,空行跳过
StringBuilder sb = new StringBuilder();
for (String line : lines) {
if (line != null && !line.isEmpty()) {
if (sb.length() > 0) sb.append(" | ");
sb.append(line);
}
}
String detail = sb.toString();
String action = isEdit ? "sign_edit" : "sign_place";
plugin.getDb().recordLandBlockWithDetail(
ac.name, landId,
block.getWorld().getName(),
block.getX(), block.getY(), block.getZ(),
p.getName(), action,
block.getType().name(), detail);
} catch (Exception e) {
e.printStackTrace();
}
}
private void recordBlock(AreaConfig ac, Player p, Block block,
String action) {
try {
int landId = plugin.areaProtection.getLandIdFromDb(
ac.name);
plugin.getDb().recordLandBlock(
ac.name, landId,
block.getWorld().getName(),
block.getX(), block.getY(), block.getZ(),
p.getName(), action,
block.getType().name());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 记录容器打开,并保存打开瞬间容器快照(用于关闭时计算拿取差量)。
* 仅在容器确实被允许打开时调用(钩子位于 AreaProtection
* 权限/取消检查之后)。
*/
public void recordContainerOpen(AreaConfig ac, Player p,
Location loc, String containerType,
Inventory inv) {
try {
int landId = plugin.areaProtection.getLandIdFromDb(
ac.name);
plugin.getDb().recordLandContainer(
ac.name, landId,
loc.getWorld().getName(),
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(),
p.getName(), "open", containerType, "", 0);
ContainerSnapshot snap = new ContainerSnapshot();
snap.landName = ac.name;
snap.landId = landId;
snap.world = loc.getWorld().getName();
snap.x = loc.getBlockX();
snap.y = loc.getBlockY();
snap.z = loc.getBlockZ();
snap.containerType = containerType;
snap.before = scanInventory(inv);
openSnapshots.put(p.getName(), snap);
} catch (Exception e) {
e.printStackTrace();
}
}
// ==================== 容器关闭:计算拿取差量 ====================
@EventHandler
public void onContainerClose(InventoryCloseEvent e) {
if (!(e.getPlayer() instanceof Player)) return;
Player p = (Player) e.getPlayer();
ContainerSnapshot snap = openSnapshots.remove(p.getName());
if (snap == null) return;
Map<String, Integer> after = scanInventory(e.getInventory());
// 1. 拿取:before > after
for (Map.Entry<String, Integer> en : snap.before.entrySet()) {
int taken = en.getValue() - after.getOrDefault(en.getKey(), 0);
if (taken > 0) {
recordContainerChange(snap, p.getName(), "take",
en.getKey(), taken);
}
}
// 2. 存入:after > before
for (Map.Entry<String, Integer> en : after.entrySet()) {
int put = en.getValue() - snap.before.getOrDefault(en.getKey(), 0);
if (put > 0) {
recordContainerChange(snap, p.getName(), "put",
en.getKey(), put);
}
}
}
private void recordContainerChange(ContainerSnapshot snap,
String playerName, String action,
String detail, int amount) {
try {
plugin.getDb().recordLandContainer(
snap.landName, snap.landId,
snap.world, snap.x, snap.y, snap.z,
playerName, action, snap.containerType,
detail, amount);
} catch (Exception ex) {
ex.printStackTrace();
}
}
/** 扫描容器库存,按"类型名"聚合数量 */
private Map<String, Integer> scanInventory(Inventory inv) {
Map<String, Integer> map = new HashMap<>();
for (ItemStack it : inv.getContents()) {
if (it == null || it.getType() == Material.AIR)
continue;
map.merge(it.getType().name(),
it.getAmount(), Integer::sum);
}
return map;
}
// ==================== 回声碎片查询(右键/左键)====================
@EventHandler
public void onShardInteract(PlayerInteractEvent e) {
ItemStack item = e.getItem();
if (item == null || !isEchoShard(item)) return;
Action a = e.getAction();
if (a != Action.RIGHT_CLICK_BLOCK && a != Action.LEFT_CLICK_BLOCK
&& a != Action.RIGHT_CLICK_AIR
&& a != Action.LEFT_CLICK_AIR) return;
Player p = e.getPlayer();
// 阻止误触方块/打开容器
e.setCancelled(true);
Block target = e.getClickedBlock();
if (target == null) {
// 视线选中的方块(非 deprecated 的 getTargetBlockExact)
Block sight = p.getTargetBlockExact(6);
if (sight != null && sight.getType() != Material.AIR)
target = sight;
}
if (target == null) {
p.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
return;
}
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, page);
// 按天分页:page=0今日,1昨日,2前日...
List<Map<String, Object>> containers =
plugin.getDb().getLandContainerLogAt(
world, x, y, z, QUERY_LIMIT, page);
if (blocks.isEmpty() && containers.isEmpty()) {
if (page > 0) {
p.sendMessage("§6§l[回声碎片] §e坐标 ("
+ x + "," + y + "," + z + ") 第" + (page + 1) + "页无记录");
} else {
p.sendMessage("§6§l[回声碎片] §f坐标 ("
+ x + "," + y + "," + z + ") 暂无领地记录");
}
return;
}
// 收集所有需要翻译的材料名
Set<String> materials = new LinkedHashSet<>();
for (Map<String, Object> r : blocks) {
materials.add((String) r.get("material"));
}
for (Map<String, Object> r : containers) {
if ("take".equals(r.get("action")) || "put".equals(r.get("action"))) {
materials.add((String) r.get("detail"));
}
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, currentPage, hasNextPage);
});
});
}
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) {
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 + ") 的记录§7(" + pageLabel + "):");
if (!blocks.isEmpty()) {
p.sendMessage("§b— 方块记录 —");
for (Map<String, Object> r : blocks) {
String action = (String) r.get("action");
String act;
if ("break".equals(action)) {
act = "§c破坏";
} else if ("place".equals(action)) {
act = "§a放置";
} else if ("state_change".equals(action)) {
act = "§e状态变化";
} else if ("sign_place".equals(action)) {
act = "§a放置告示牌";
} else if ("sign_edit".equals(action)) {
act = "§b编辑告示牌";
} else {
act = "§7" + action;
}
String mat = (String) r.get("material");
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
StringBuilder msg = new StringBuilder();
msg.append(" §7").append(fmt((Long) r.get("time")))
.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))) {
msg.append(" §7[§f").append(detail).append("§7]");
}
p.sendMessage(msg.toString());
}
}
if (!containers.isEmpty()) {
p.sendMessage("§b— 容器记录 §7(" + pageLabel + ")—");
for (Map<String, Object> r : containers) {
String act = (String) r.get("action");
if ("open".equals(act)) {
String ct = (String) r.get("container_type");
String ctZh = translations.getOrDefault(ct, MaterialTranslator.toReadable(ct));
p.sendMessage(" §7" + fmt((Long) r.get("time"))
+ " §f" + r.get("player_name")
+ " §d打开§7[" + ct + "] §a" + ctZh);
} else if ("take".equals(act)) {
String mat = (String) r.get("detail");
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
p.sendMessage(" §7" + fmt((Long) r.get("time"))
+ " §f" + r.get("player_name")
+ " §c拿取 §e" + mat + " §a" + zh
+ " §7x" + r.get("amount"));
} else if ("put".equals(act)) {
String mat = (String) r.get("detail");
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
p.sendMessage(" §7" + fmt((Long) r.get("time"))
+ " §f" + r.get("player_name")
+ " §a存入 §e" + mat + " §a" + zh
+ " §7x" + r.get("amount"));
}
}
}
// ★ 可点击翻页导航
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) {
return sdf.format(new Date(ms));
}
// ==================== 命令:/landrec ====================
public boolean handleCommand(CommandSender sender,
String[] args) {
if (args.length == 0) {
sendHelp(sender);
return true;
}
String sub = args[0];
if (sub.equalsIgnoreCase("give")) {
// 发放权限...
boolean isOp = sender.isOp();
boolean isAreaAdmin = (sender instanceof Player)
&& plugin.areaProtection.isAreaAdmin((Player) sender);
Player target;
if (args.length >= 2) {
if (!isOp && !isAreaAdmin) {
sender.sendMessage("§c§l[回声碎片] §f你无权向他人发放回声碎片");
return true;
}
target = Bukkit.getPlayerExact(args[1]);
if (target == null) {
sender.sendMessage("§c§l[回声碎片] §f玩家 " + args[1] + " 不在线");
return true;
}
} else if (sender instanceof Player) {
target = (Player) sender;
} else {
sender.sendMessage("§c§l[回声碎片] §f请指定要发放的玩家");
return true;
}
giveEchoShard(target);
if (target != sender)
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;
}
private void sendHelp(CommandSender sender) {
sender.sendMessage("§6§l[回声碎片] §e指令:");
sender.sendMessage(" §7/landrec give §f- 给自己发放一枚回声碎片");
sender.sendMessage(" §7/landrec give <玩家> §f- 向他人发放(需 OP/领地管理员)");
sender.sendMessage(" §7/landrec log §f- 查看当前方块今日记录");
sender.sendMessage(" §7/landrec log <页码> §f- 查看指定日期的记录(1=今日 2=昨日 ...)");
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处今日记录");
}
}
+61 -3
View File
@@ -66,6 +66,10 @@ public class LoginManager {
plugin.getDb().createUser(
name, hash, salt);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
plugin.webManager.recordJavaLogin(name);
}
p.setAllowFlight(false);
p.setFlying(false);
plugin.getDb().setLoggedIn(name, true);
@@ -100,6 +104,17 @@ public class LoginManager {
}
} catch (Exception ignored) {
}
// ★ 注册后立即同步用户数据到PHP
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try { plugin.webManager.syncUserRegistrations(); }
catch (Exception e) {
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
}
});
}
return true;
}
@@ -139,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;
@@ -161,7 +186,13 @@ 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) {
plugin.webManager.recordJavaLogin(name);
}
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setLoggedIn(
@@ -172,6 +203,16 @@ public class LoginManager {
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
if (plugin.webManager != null) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
plugin.webManager.syncOnlinePlayers();
} catch (Exception e) {
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
}
});
}
// 登录成功关闭飞行
p.setAllowFlight(false);
p.setFlying(false);
@@ -191,12 +232,18 @@ public class LoginManager {
return true;
}
boolean matchTemp =
String pwdResult =
plugin.getDb()
.checkPasswordOrTemp(
.checkPasswordWithFallback(
name, hash);
if (matchTemp) {
if (pwdResult != null) {
// ★ 临时密码登录成功,也重置风控
plugin.getRiskControl().onLoginSuccess(name);
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
plugin.webManager.recordJavaLogin(name);
}
plugin.getDb().setLoggedIn(name, true);
p.setAllowFlight(false);
p.setFlying(false);
@@ -223,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
@@ -0,0 +1,326 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
/**
* 多源在线翻译工具 —— 将MC英文材料名翻译为中文。
* 优先使用国内接口(有道/简心),失败后降级到Google。
* 结果缓存避免重复请求。
*/
public class MaterialTranslator {
private static final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
private static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
// 翻译源优先级:国内接口优先
private static final List<TranslationSource> SOURCES = Arrays.asList(
TranslationSource.YOUDAO,
TranslationSource.JIANXIN,
TranslationSource.GOOGLE
);
/**
* 翻译源枚举
*/
private enum TranslationSource {
YOUDAO("有道翻译"),
JIANXIN("简心翻译"),
GOOGLE("Google翻译");
final String name;
TranslationSource(String name) {
this.name = name;
}
}
/**
* 将材料名(如 GRASS_BLOCK)转为可读英文(Grass Block)
*/
public static String toReadable(String materialName) {
if (materialName == null || materialName.isEmpty()) return "";
return Arrays.stream(materialName.split("_"))
.map(w -> w.substring(0, 1).toUpperCase() + w.substring(1).toLowerCase())
.reduce((a, b) -> a + " " + b)
.orElse(materialName);
}
/**
* 同步翻译单个材料名(有缓存则直接返回)。
* 优先走 MC 官方 zh_cn.json 翻译,查不到才走机翻。
*/
public static String translate(String materialName) {
if (materialName == null || materialName.isEmpty()) return "";
String key = materialName.toUpperCase();
// 缓存命中
if (cache.containsKey(key)) {
return cache.get(key);
}
// 1. 官方翻译(基于原始 Material 名 GRASS_BLOCK → block.minecraft.grass_block)
String official = OfficialTranslations.lookUp(materialName);
if (official != null && !official.isEmpty()) {
cache.put(key, official);
return official;
}
// 2. 机翻兜底
String readable = toReadable(key);
String zh = translateWithFallback(readable);
if (zh != null && !zh.isEmpty()) {
cache.put(key, zh);
return zh;
}
return readable; // fallback 可读英文
}
/**
* 批量异步翻译,返回 future map
*/
public static CompletableFuture<Map<String, String>> translateBatch(Collection<String> materials) {
Set<String> unique = new LinkedHashSet<>();
for (String m : materials) {
if (m != null && !m.isEmpty() && !cache.containsKey(m.toUpperCase())) {
unique.add(m);
}
}
if (unique.isEmpty()) {
Map<String, String> result = new HashMap<>();
for (String m : materials) {
if (m != null) result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
}
return CompletableFuture.completedFuture(result);
}
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (String m : unique) {
futures.add(CompletableFuture.runAsync(() -> translate(m), executor));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> {
Map<String, String> result = new HashMap<>();
for (String m : materials) {
if (m != null) {
result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
}
}
return result;
});
}
/**
* 多源翻译,按优先级尝试,失败自动降级
*/
private static String translateWithFallback(String text) {
for (TranslationSource source : SOURCES) {
try {
String result = null;
switch (source) {
case YOUDAO:
result = translateViaYoudao(text);
break;
case JIANXIN:
result = translateViaJianxin(text);
break;
case GOOGLE:
result = translateViaGoogle(text);
break;
}
if (result != null && !result.isEmpty()) {
return result;
}
} catch (Exception e) {
Bukkit.getLogger().warning("[翻译] 机翻 " + source.name + " 异常: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
return null;
}
// ==================== 翻译源实现 ====================
/**
* 有道翻译API
* URL: https://v.api.aa1.cn/api/api-fanyi-yd/index.php
* 参数: msg=翻译内容, type=1(中→英) / 2(英→中)
*/
private static String translateViaYoudao(String text) {
try {
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
// type=2 表示英→中
String url = "https://v.api.aa1.cn/api/api-fanyi-yd/index.php?msg=" + encoded + "&type=2";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "Mozilla/5.0")
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
// 返回: {"type":"中英互译","desc":"中文翻译英文","text":"翻译结果"}
return parseJsonField(resp.body(), "text");
}
} catch (Exception e) {
// 静默降级
}
return null;
}
/**
* 简心翻译API
* URL: https://api.qvqa.cn/api/fanyi
* 参数: text=翻译内容, source=en, target=zh
*/
private static String translateViaJianxin(String text) {
try {
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
String url = "https://api.qvqa.cn/api/fanyi?text=" + encoded + "&source=en&target=zh";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "Mozilla/5.0")
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
// 返回: {"code":200,"message":"OK","data":{"sourceText":"...","targetText":"翻译结果",...}}
return parseNestedJsonField(resp.body(), "data", "targetText");
}
} catch (Exception e) {
// 静默降级
}
return null;
}
/**
* Google Translate 免费接口
*/
private static String translateViaGoogle(String text) {
try {
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
String url = "https://clients5.google.com/translate_a/single"
+ "?client=gtx&sl=en&tl=zh-CN&dt=t&q=" + encoded;
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", "Mozilla/5.0")
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
// 响应格式: [["翻译结果","原文",...],...]
String body = resp.body();
int firstQuote = body.indexOf("[[\"");
if (firstQuote >= 0) {
int endQuote = body.indexOf("\"", firstQuote + 3);
if (endQuote > firstQuote) {
return body.substring(firstQuote + 3, endQuote);
}
}
}
} catch (Exception e) {
// 静默降级
}
return null;
}
// ==================== JSON解析工具 ====================
/**
* 简单JSON字段解析(单层)
* 从 {"key":"value"} 中提取 value
*/
private static String parseJsonField(String json, String field) {
try {
String pattern = "\"" + field + "\"";
int idx = json.indexOf(pattern);
if (idx < 0) return null;
// 找到冒号后的引号
int colon = json.indexOf(':', idx + pattern.length());
if (colon < 0) return null;
int quoteStart = json.indexOf('"', colon + 1);
if (quoteStart < 0) return null;
int quoteEnd = json.indexOf('"', quoteStart + 1);
if (quoteEnd < 0) return null;
return json.substring(quoteStart + 1, quoteEnd);
} catch (Exception e) {
return null;
}
}
/**
* 嵌套JSON字段解析
* 从 {"outer":{"inner":"value"}} 中提取 value
*/
private static String parseNestedJsonField(String json, String outerField, String innerField) {
try {
// 找到外层对象
String outerPattern = "\"" + outerField + "\"";
int outerIdx = json.indexOf(outerPattern);
if (outerIdx < 0) return null;
// 找到外层对象的开始大括号
int braceStart = json.indexOf('{', outerIdx);
if (braceStart < 0) return null;
// 从外层对象内部解析内层字段
int depth = 1;
int i = braceStart + 1;
while (i < json.length() && depth > 0) {
char c = json.charAt(i);
if (c == '{') depth++;
else if (c == '}') depth--;
i++;
}
String innerJson = json.substring(braceStart, i);
return parseJsonField(innerJson, innerField);
} catch (Exception e) {
return null;
}
}
/**
* 清理缓存(可选)
*/
public static void clearCache() {
cache.clear();
}
/**
* 获取当前使用的翻译源信息(调试用)
*/
public static String getSourceInfo() {
StringBuilder sb = new StringBuilder("翻译源优先级: ");
for (int i = 0; i < SOURCES.size(); i++) {
TranslationSource src = SOURCES.get(i);
sb.append(i + 1).append(".").append(src.name);
if (i < SOURCES.size() - 1) sb.append(" > ");
}
return sb.toString();
}
}
+23 -5
View File
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
return items;
}
// ===== 读取菜单.txt =====
// ===== 读取菜单.txt(主菜单唯一来源) =====
public void loadMenu() {
items.clear();
// 优先菜单.txt,备选menu.txt
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
if (!f.exists()) createDefault(f);
if (!f.exists()) {
File alt = new File(
plugin.getDataFolder(),
"menu.txt");
if (alt.exists()) {
f = alt;
} else {
createDefault(f);
}
}
parseFile(f);
plugin.getLogger().info(
"[Menu] 加载 "
+ items.size()
+ " 个菜单项");
"[Menu] 加载 " + items.size()
+ " 个菜单项 (源: "
+ f.getName() + ")");
}
private void parseFile(File f) {
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
// ===== 保存 =====
public void saveMenu() {
// 保存到加载源文件
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
if (!f.exists()) {
File alt = new File(
plugin.getDataFolder(),
"menu.txt");
if (alt.exists()) f = alt;
}
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
@@ -0,0 +1,229 @@
package Sdf1_login;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.bukkit.Bukkit;
import java.io.File;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
/**
* MC 官方中文翻译加载器。<br>
* <br>
* 工作流程优先级:<br>
* 1. 本地缓存 (plugins/Sdf1_login/zh_cn.json) —— 同步加载,开机即用<br>
* 2. Mojang API —— 异步下载最新版,写缓存并刷入内存<br>
* 3. 全部失败 —— 返回 null,调用者走机翻兜底<br>
* <br>
* zh_cn.json 键示例:<br>
* {@code block.minecraft.grass_block → "草方块"}<br>
* {@code item.minecraft.diamond_sword → "钻石剑"}<br>
* <br>
* 查找策略:直接 key → block.minecraft.* → item.minecraft.*
*/
public class OfficialTranslations {
private static final ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
private static final Gson gson = new Gson();
// ---------- 对外 API ----------
/**
* 初始化(在 onEnable 调用)。<br>
* 纯同步:先读本地缓存 &rarr; 没有则从 Mojang API 下载并写缓存。<br>
* 缓存文件由部署脚本随 JAR 一起上传,确保服务器不直连 Mojang 也能加载。
*/
public static void init(Main plugin) {
File cacheFile = new File(plugin.getDataFolder(), "zh_cn.json");
// 1. 本地缓存 —— 同步加载
if (cacheFile.isFile()) {
try {
String content = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8);
loadFromJson(content);
plugin.getLogger().info("[官方翻译] 已加载本地缓存 zh_cn.json(" + map.size() + " 条)");
return; // 缓存命中,完毕
} catch (Exception e) {
plugin.getLogger().warning("[官方翻译] 本地缓存损坏: " + e.getMessage());
}
}
// 2. 无缓存或损坏 → 同步从 Mojang API 下载
plugin.getLogger().info("[官方翻译] 本地缓存不存在/损坏,从 Mojang API 同步下载...");
try {
String version = getMinecraftVersion();
if (version == null) throw new RuntimeException("无法获取服务器版本号");
String json = downloadFromMojang(version);
if (json == null) throw new RuntimeException("下载返回空");
Files.writeString(cacheFile.toPath(), json, StandardCharsets.UTF_8);
loadFromJson(json);
plugin.getLogger().info("[官方翻译] Mojang 下载成功(" + map.size() + " 条),已缓存至 " + cacheFile.getName());
} catch (Exception e) {
plugin.getLogger().warning("[官方翻译] 同步下载失败: " + e.getMessage() + ",走机翻兜底");
plugin.getLogger().log(Level.FINE, "[官方翻译] 同步下载详情", e);
}
}
/**
* 查找官方翻译。<br>
* 传入原始 Material 名(如 {@code GRASS_BLOCK}),自动尝试多种 key 模式。
*
* @param materialName 原始 Material 名(大小写无关)
* @return 中文翻译,或 null(未命中)
*/
public static String lookUp(String materialName) {
if (materialName == null || materialName.isEmpty()) return null;
String lower = materialName.toLowerCase(Locale.ROOT);
// 日志:当前 map 状态
Bukkit.getLogger().info("[翻译] officialLookUp: " + materialName
+ " | map已加载=" + isLoaded() + " 条目数=" + map.size());
// 1. 精确匹配(部分 key 可直接用)
String val = map.get(lower);
if (val != null) {
Bukkit.getLogger().info("[翻译] officialLookUp 命中(精确): " + lower + " → " + val);
return val;
}
// 2. block.minecraft.<name>
val = map.get("block.minecraft." + lower);
if (val != null) {
Bukkit.getLogger().info("[翻译] officialLookUp 命中(block): " + lower + " → " + val);
return val;
}
// 3. item.minecraft.<name>
val = map.get("item.minecraft." + lower);
if (val != null) {
Bukkit.getLogger().info("[翻译] officialLookUp 命中(item): " + lower + " → " + val);
return val;
}
Bukkit.getLogger().info("[翻译] officialLookUp 未命中: " + lower + ",将走机翻兜底");
return null; // 未命中 -> 调用者走机翻
}
/** 当前加载的条目数(调试用) */
public static int size() {
return map.size();
}
/** 是否已完成至少一次加载(缓存或下载) */
public static boolean isLoaded() {
return !map.isEmpty();
}
// ---------- 内部 ----------
/** 解析 zh_cn.json 字符串灌入内存 */
static void loadFromJson(String json) {
JsonObject obj = gson.fromJson(json, JsonObject.class);
if (obj == null) return;
map.clear();
for (Map.Entry<String, com.google.gson.JsonElement> entry : obj.entrySet()) {
map.put(entry.getKey(), entry.getValue().getAsString());
}
}
/** 从 Mojang API 拉取 zh_cn.json(完整链:manifest → version → asset index → object) */
static String downloadFromMojang(String version) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// Step 1: Version manifest
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://launchermeta.mojang.com/mc/game/version_manifest.json"))
.timeout(Duration.ofSeconds(10))
.header("User-Agent", "Sdf1Login/1.0")
.GET()
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) return null;
JsonObject manifest = gson.fromJson(resp.body(), JsonObject.class);
// 在 versions 数组中找目标版本
String versionUrl = null;
JsonArray versions = manifest.getAsJsonArray("versions");
if (versions != null) {
for (int i = 0; i < versions.size(); i++) {
JsonObject v = versions.get(i).getAsJsonObject();
if (version.equals(v.get("id").getAsString())) {
versionUrl = v.get("url").getAsString();
break;
}
}
}
if (versionUrl == null) return null;
// Step 2: Version JSON (获取 assetIndex url)
req = HttpRequest.newBuilder()
.uri(URI.create(versionUrl))
.timeout(Duration.ofSeconds(10))
.header("User-Agent", "Sdf1Login/1.0")
.GET()
.build();
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) return null;
JsonObject versionJson = gson.fromJson(resp.body(), JsonObject.class);
JsonObject assetIndex = versionJson.getAsJsonObject("assetIndex");
if (assetIndex == null) return null;
String assetIndexUrl = assetIndex.get("url").getAsString();
// Step 3: Asset index (定位 zh_cn.json 的 hash)
req = HttpRequest.newBuilder()
.uri(URI.create(assetIndexUrl))
.timeout(Duration.ofSeconds(10))
.header("User-Agent", "Sdf1Login/1.0")
.GET()
.build();
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) return null;
JsonObject assets = gson.fromJson(resp.body(), JsonObject.class);
JsonObject objects = assets.getAsJsonObject("objects");
if (objects == null) return null;
JsonObject langEntry = objects.getAsJsonObject("minecraft/lang/zh_cn.json");
if (langEntry == null) return null;
String hash = langEntry.get("hash").getAsString();
if (hash == null || hash.length() < 2) return null;
// Step 4: 下载 zh_cn.json
String langUrl = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
req = HttpRequest.newBuilder()
.uri(URI.create(langUrl))
.timeout(Duration.ofSeconds(15))
.header("User-Agent", "Sdf1Login/1.0")
.GET()
.build();
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 200) {
return resp.body();
}
return null;
}
/** 从 Bukkit API 获取纯版本号,如 "1.21.4" */
private static String getMinecraftVersion() {
String ver = Bukkit.getBukkitVersion();
if (ver == null) return null;
int dash = ver.indexOf('-');
return dash > 0 ? ver.substring(0, dash) : ver;
}
}
+68 -13
View File
@@ -77,10 +77,21 @@ public class OrderManager implements Listener {
public static class OrderItem {
public String name; public String mat;
public int originalPrice; public int finalPrice; public int qty;
public String nbt; // 物品NBT快照(Base64 of ItemStack.serializeAsBytes),退款时用于精确匹配含NBT商品;null/空=无NBT或旧数据
public OrderItem(String n,String m,int op,int fp,int q){
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;}
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;nbt=null;}
public OrderItem(String n,String m,int op,int fp,int q,String nbtB64){
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;nbt=nbtB64;}
public int subtotal(){return finalPrice*qty;}
public int originalSubtotal(){return originalPrice*qty;}
/** 还原下单时的真实物品(含NBT),用于退款时精确匹配;无快照或还原失败返回null */
public org.bukkit.inventory.ItemStack toTemplateStack() {
if (nbt == null || nbt.isEmpty()) return null;
try {
byte[] b = java.util.Base64.getDecoder().decode(nbt);
return org.bukkit.inventory.ItemStack.deserializeBytes(b);
} catch (Exception e) { return null; }
}
}
public static class OrderRecord {
public long orderId; public UUID uuid; public String player;
@@ -384,13 +395,31 @@ public class OrderManager implements Listener {
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
return c;
}
/** 精确按材质+ItemMeta(NBT)计数,用于含NBT商品(如附魔书)退款校验 */
private int countSimilarItems(Player p, org.bukkit.inventory.ItemStack tmpl) {
if (tmpl == null) return 0;
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.isSimilar(tmpl)) c += is.getAmount();
return c;
}
private int countShulkers(Player p, String matName) {
Material m = Material.matchMaterial(matName); if (m == null) return 0;
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
return c;
}
private void removeItems(Player p, String matName, int amt) {
Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt));
/**
* 收走订单物品:优先按记录中的NBT快照精确匹配(材质+ItemMeta一致),
* 确保含NBT商品(附魔书等)既退钱又收回正确的实物,而非"仅退款"。
* 无快照(旧订单/普通物品)则按材质兜底。
*/
private void removeItems(Player p, OrderItem it) {
org.bukkit.inventory.ItemStack tmpl = it.toTemplateStack();
if (tmpl != null) {
tmpl.setAmount(it.qty);
p.getInventory().removeItem(tmpl); // removeItem 按 isSimilar 匹配,精确收走含NBT实物
} else {
Material m = Material.matchMaterial(it.mat);
if (m != null) p.getInventory().removeItem(new ItemStack(m, it.qty));
}
}
private boolean removeOneShulker(Player p,
String materialName) {
@@ -448,9 +477,10 @@ public class OrderManager implements Listener {
}
return "§c对应的潜影盒已不在背包中";
} else {
// ★ 散装:检测物品数量
// ★ 散装:检测物品数量(含NBT商品按材质+ItemMeta精确校验,必须"有货且有正确NBT")
for (OrderItem it : r.items) {
int have = countDirectItems(p, it.mat);
org.bukkit.inventory.ItemStack tmpl = it.toTemplateStack();
int have = (tmpl != null) ? countSimilarItems(p, tmpl) : countDirectItems(p, it.mat);
if (have < it.qty)
return "§c" + it.name + "不足(有"
+ have + "/" + it.qty + ")";
@@ -685,7 +715,7 @@ public class OrderManager implements Listener {
}
} else {
for (OrderItem it : r.items)
removeItems(p, it.mat, it.qty);
removeItems(p, it);
}
// 退钱
plugin.getBonds().addBonds(
@@ -821,13 +851,38 @@ public class OrderManager implements Listener {
p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚");
}
} else {
if (r.status == 0 || r.status == 1) {
if (r.status == 0) {
// ★ 管理员全额退款:同步收回物品(玩家在线时),避免"只退钱不退货"
Player target = Bukkit.getPlayer(r.uuid);
if (target != null && target.isOnline()) {
String pt = r.packType != null ? r.packType : "none";
boolean isShulker = pt.equals("custom")
|| pt.equals("default") || pt.equals("shulker");
if (isShulker) {
for (int i = 0; i < 36; i++) {
ItemStack is = target.getInventory().getItem(i);
if (is == null) continue;
if (!is.getType().name().contains("SHULKER")) continue;
ItemMeta im = is.getItemMeta();
boolean tagged = false;
if (im != null && im.getLore() != null) {
for (String line : im.getLore()) {
if (line.equals("§8#" + r.orderId)) { tagged = true; break; }
}
}
if (tagged) { target.getInventory().clear(i); break; }
}
} else {
for (OrderItem it : r.items) removeItems(target, it);
}
}
plugin.getBonds().addBonds(r.player, r.totalPaid);
r.status = 2;
r.refundedAmount = r.totalPaid;
r.refundType = "auto_return";
saveOrders();
p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚");
notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚");
p.sendMessage("§a全额退款(含退货) §e" + r.player + " §6" + r.totalPaid + "枚");
notifyPlayer(r.uuid, "§a[订单] 退款退货成功,§6" + r.totalPaid + "枚已退还");
openAdminOrders(p, tu, page);
}
}
@@ -932,7 +987,7 @@ public class OrderManager implements Listener {
Player target = Bukkit.getPlayer(r.uuid);
if (target != null && target.isOnline()) {
for (OrderItem it : r.items)
removeItems(target, it.mat, it.qty);
removeItems(target, it);
}
plugin.getBonds().addBonds(r.player, r.totalPaid);
r.status = 2;
@@ -1037,7 +1092,7 @@ public class OrderManager implements Listener {
}
} else {
for (OrderItem it : r.items)
removeItems(p, it.mat, it.qty);
removeItems(p, it);
}
plugin.getBonds().addBonds(r.player, r.totalPaid);
@@ -1280,7 +1335,7 @@ public class OrderManager implements Listener {
case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break;
case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break;
case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break;
case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; }
case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) { OrderItem oi = new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4])); if (pp.length >= 6) oi.nbt = pp[5]; cur.items.add(oi); } break; }
} else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; }
} if (cur != null && cur.uuid != null) orders.add(cur); br.close();
} catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); }
@@ -1296,7 +1351,7 @@ public class OrderManager implements Listener {
pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor));
pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount);
pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType));
for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty);
for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty + "|" + (it.nbt == null ? "" : it.nbt));
} } pw.flush(); pw.close();
} catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); }
}
File diff suppressed because it is too large Load Diff
+110 -6
View File
@@ -713,8 +713,10 @@ public class PVPManager implements Listener {
instanceof Player)) return;
Player victim =
(Player) event.getEntity();
org.bukkit.entity.Entity kc =
event.getDamageSource().getCausingEntity();
Player killer =
victim.getKiller();
(kc instanceof Player) ? (Player) kc : null;
if (killer == null) return;
if (killer.equals(victim)) return;
@@ -1074,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":
@@ -1112,9 +1121,31 @@ public class PVPManager implements Listener {
case "tempban":
return handleTempBan(
p, args);
case "join":
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);
return true;
default:
return showStats(p,
p.getName());
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区域列表");
return true;
}
}
@@ -1335,9 +1366,28 @@ public class PVPManager implements Listener {
if (region == null) {
p.sendMessage("§6§l" + target
+ " 的PVP战绩:");
// ★ PVP世界战绩(从PvPArenaManager获取)
int pvpWorldKills = 0;
int pvpWorldDeaths = 0;
if (plugin.getPVPArenaManager() != null) {
Map<String, Integer> wk = plugin.getPVPArenaManager().getPVPWorldKills();
Map<String, Integer> wd = plugin.getPVPArenaManager().getPVPWorldDeaths();
pvpWorldKills = wk.getOrDefault(target, 0);
pvpWorldDeaths = wd.getOrDefault(target, 0);
}
if (pvpWorldKills > 0 || pvpWorldDeaths > 0) {
double pvpKd = pvpWorldDeaths > 0
? (double) pvpWorldKills / pvpWorldDeaths : pvpWorldKills;
p.sendMessage("§e- §fPVP世界 §a击杀:" + pvpWorldKills
+ " §c死亡:" + pvpWorldDeaths
+ " §eKD:" + String.format("%.2f", pvpKd));
}
// PVP区域战绩
List<Map<String, Object>> all =
getAllRegions();
boolean has = false;
boolean has = pvpWorldKills > 0 || pvpWorldDeaths > 0;
for (Map<String, Object> row :
all) {
String rn = (String) row
@@ -1355,6 +1405,21 @@ public class PVPManager implements Listener {
has = true;
}
}
// ★ 总计
int totalKills = pvpWorldKills;
int totalDeaths = pvpWorldDeaths;
for (Map<String, Object> row : all) {
String rn = (String) row.get("name");
totalKills += getKills(target, rn);
totalDeaths += getDeaths(target, rn);
}
if (totalKills > 0 || totalDeaths > 0) {
double totalKd = totalDeaths > 0
? (double) totalKills / totalDeaths : totalKills;
p.sendMessage("§6§l- §f总计 §a击杀:" + totalKills
+ " §c死亡:" + totalDeaths
+ " §eKD:" + String.format("%.2f", totalKd));
}
if (!has)
p.sendMessage(
"§7暂无记录");
@@ -1377,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));
}
}
File diff suppressed because it is too large Load Diff
+13 -12
View File
@@ -259,20 +259,21 @@ public class PointsManager {
switch (slot) {
case 10:
if (points >= 100) {
plugin.getDb().deductPoints(
p.getName(), 100);
if (plugin.getEconomy() != null) {
plugin.getEconomy()
.depositPlayer(p, 1000);
BondManager bm = plugin.getBonds();
if (bm != null) {
int before = bm.getBonds(p.getName());
bm.addBonds(p.getName(), 1000, "points_exchange", "", "积分商店", "积分兑换债券");
int after = bm.getBonds(p.getName());
plugin.getDb().deductPoints(p.getName(), 100);
p.sendMessage(plugin.getConfig2().msg("points_purchase_success")
.replace("{count}", "100"));
p.sendMessage("§a[积分] §f债券: §e" + before + " → §a" + after);
} else {
p.sendMessage("§c债券系统不可用");
}
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
p.sendMessage(plugin.getConfig2().msg("points_insufficient")
.replace("{points}", String.valueOf(points)));
}
break;
case 12:
+10 -55
View File
@@ -1213,15 +1213,6 @@ public class QuestTracker {
}
Main main = (Main) plugin;
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
boolean useEconomy = "economy".equalsIgnoreCase(
main.getConfigMgr().rewardChannel);
// Vault 经济引用(仅 useEconomy 时使用)
net.milkbowl.vault.economy.Economy econ = null;
if (useEconomy) {
econ = main.getEconomy();
}
int sentCount = 0;
@@ -1247,53 +1238,17 @@ public class QuestTracker {
// 尝试识别经济金额
double amt = extractEconomyAmount(part);
if (amt > 0) {
// ★ 根据配置分发
if (useEconomy && econ != null) {
// —— Vault 经济 ——
double before =
econ.getBalance(player);
econ.depositPlayer(player, amt);
double after =
econ.getBalance(player);
player.sendMessage(
"§a§l[任务奖励] §f" + part
+ " §7(余额: §e"
+ String.format("%.2f", before)
+ " §7→ §a"
+ String.format("%.2f", after)
+ "§7)");
if (plugin instanceof Main) {
((Main) plugin).getCommission()
.payCommission(
player.getName(), amt);
}
// Economy已移除,全部转为债券发放
// —— 债券 ——
int bondAmt = (int) Math.round(amt);
BondManager bonds = main.getBonds();
if (bonds != null) {
int before = bonds.getBonds(player.getName());
bonds.addBonds(player.getName(), bondAmt, "quest_reward", qf.displayName, "任务系统", "任务奖励:" + qf.displayName);
int after = bonds.getBonds(player.getName());
player.sendMessage("§a§l[任务奖励] §f" + part + " §7(债券: §e" + before + " §7→ §a" + after + "§7)");
} else {
// —— 债券(默认) ——
int bondAmt =
(int) Math.round(amt);
BondManager bonds = main.getBonds();
if (bonds != null) {
int before = bonds.getBonds(
player.getName());
bonds.addBonds(
player.getName(),
bondAmt,
"quest_reward",
qf.displayName,
"任务系统",
"任务奖励:" + qf.displayName);
int after = bonds.getBonds(
player.getName());
player.sendMessage(
"§a§l[任务奖励] §f" + part
+ " §7(债券: §e"
+ before
+ " §7→ §a"
+ after + "§7)");
} else {
player.sendMessage(
"§c债券系统不可用");
}
player.sendMessage("§c债券系统不可用");
}
sentCount++;
continue;
+252
View File
@@ -0,0 +1,252 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 快速返回系统 (/back)
* - 每分钟记录一次在线玩家的坐标
* - /back 命令传送玩家到最近的一个记录坐标
*/
public class QuickBack implements Listener, CommandExecutor, TabCompleter {
private final Main plugin;
// 内存缓存: player_name → Location (每分钟刷新)
private final Map<String, Location> lastKnownLocations = new ConcurrentHashMap<>();
// 冷却: player_name → next_available_time
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
// ★ 定时器引用(用于 shutdown 时取消)
private BukkitTask scheduleTask = null;
private static final long COOLDOWN_MS = 10 * 1000; // 10秒冷却
public QuickBack(Main plugin) {
this.plugin = plugin;
loadAllFromDB();
// 每分钟记录所有在线玩家的坐标
scheduleTask = new BukkitRunnable() {
@Override
public void run() {
for (Player p : Bukkit.getOnlinePlayers()) {
recordLocation(p);
}
}
}.runTaskTimer(plugin, 1200L, 1200L);
}
// ==================== 事件监听 ====================
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerMove(PlayerMoveEvent event) {
// 移动超过8格时才记录(减少数据库写入压力)
Location from = event.getFrom();
Location to = event.getTo();
if (to == null || from.getWorld() != to.getWorld()) return;
double distance = from.distance(to);
if (distance < 8.0) return;
recordLocation(event.getPlayer());
}
// ==================== 命令处理 ====================
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c此命令仅限玩家使用");
return true;
}
Player player = (Player) sender;
// 检查冷却
Long nextAvailable = cooldowns.get(player.getName());
if (nextAvailable != null && System.currentTimeMillis() < nextAvailable) {
long remaining = (nextAvailable - System.currentTimeMillis()) / 1000;
player.sendMessage("§c快速返回冷却中,请等待 §e" + remaining + " §c秒");
return true;
}
// 获取最近的已知位置
Location loc = getLastKnownLocation(player);
if (loc == null) {
player.sendMessage("§c没有找到可返回的位置记录");
return true;
}
// 传送
if (loc.getWorld() == null) {
player.sendMessage("§c位置世界信息无效");
return true;
}
// ★ 禁止/back传送至PVP测试世界
String worldName = loc.getWorld().getName();
if (worldName != null && worldName.startsWith("pvp_test_")) {
player.sendMessage("§c§l[快速返回] §c无法返回PVP测试世界,请使用 /pvp test 进入测试场选装");
return true;
}
Location dest = loc.clone().add(0.5, 0, 0.5); // 对齐格子中心
player.teleport(dest);
// 设置冷却
cooldowns.put(player.getName(), System.currentTimeMillis() + COOLDOWN_MS);
player.sendMessage("§a§l[快速返回] §f已传送到最近记录点");
return true;
}
// ==================== 位置记录 ====================
private void recordLocation(Player player) {
Location loc = player.getLocation();
lastKnownLocations.put(player.getName(), loc);
saveToDB(player.getName(), loc);
}
public Location getLastKnownLocation(Player player) {
// 优先从内存获取
Location loc = lastKnownLocations.get(player.getName());
if (loc != null && loc.getWorld() != null) {
return loc;
}
// 从数据库获取
return loadFromDB(player.getName());
}
// ==================== 数据库操作 ====================
private void loadAllFromDB() {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name, world, x, y, z FROM quick_back_records");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String playerName = rs.getString("player_name");
Location loc = new Location(
Bukkit.getWorld(rs.getString("world")),
rs.getDouble("x"),
rs.getDouble("y"),
rs.getDouble("z")
);
lastKnownLocations.put(playerName, loc);
}
rs.close();
ps.close();
} catch (SQLException e) {
// 表可能不存在
}
}
private void saveToDB(String playerName, Location loc) {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
PreparedStatement ps = conn.prepareStatement(
"INSERT OR REPLACE INTO quick_back_records (player_name, world, x, y, z) VALUES (?, ?, ?, ?, ?)");
ps.setString(1, playerName);
ps.setString(2, loc.getWorld() != null ? loc.getWorld().getName() : "world");
ps.setDouble(3, loc.getX());
ps.setDouble(4, loc.getY());
ps.setDouble(5, loc.getZ());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
// 忽略
}
}
private Location loadFromDB(String playerName) {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return null;
PreparedStatement ps = conn.prepareStatement(
"SELECT world, x, y, z FROM quick_back_records WHERE player_name=? LIMIT 1");
ps.setString(1, playerName);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Location loc = new Location(
Bukkit.getWorld(rs.getString("world")),
rs.getDouble("x"),
rs.getDouble("y"),
rs.getDouble("z")
);
rs.close();
ps.close();
return loc;
}
rs.close();
ps.close();
} catch (SQLException e) {
// 忽略
}
return null;
}
// ==================== Tab补全 ====================
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
return new ArrayList<>();
}
// ==================== 初始化 ====================
public void initTable() {
try {
java.sql.Connection conn = plugin.getDb().getConnection();
if (conn == null) return;
java.sql.Statement st = conn.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS quick_back_records ("
+ "player_name TEXT PRIMARY KEY, "
+ "world TEXT, "
+ "x REAL, y REAL, z REAL)");
st.close();
} catch (SQLException e) {
plugin.getLogger().warning("[快速返回] 建表失败: " + e.getMessage());
}
}
// ==================== 关闭 ====================
public void shutdown() {
if (scheduleTask != null) {
scheduleTask.cancel();
}
lastKnownLocations.clear();
cooldowns.clear();
}
}
+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();
}
}
+4 -2
View File
@@ -6,8 +6,8 @@ public class ShopItem {
private final String id;
private final String displayName;
private final Material material;
private final int buyPrice;
private final int sellPrice;
private int buyPrice;
private int sellPrice;
private int stock;
private int hourlySales;
private int totalSales;
@@ -42,6 +42,8 @@ public class ShopItem {
public void setStock(int s) { this.stock = s; }
public void setHourlySales(int s) { this.hourlySales = s; }
public void setTotalSales(int s) { this.totalSales = s; }
public void setBuyPrice(int p) { this.buyPrice = p; }
public void setSellPrice(int p) { this.sellPrice = p; }
public void addSales(int n) {
this.hourlySales += n;
this.totalSales += n;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44 -12
View File
@@ -48,12 +48,16 @@ public class UpdateChecker {
private static final String GITHUB_REPO = "sdf1_login";
private static final String GITEE_OWNER = "nihaoshidifu";
private static final String GITEE_REPO = "sdf1_login";
private static final String GYP_OWNER = "youpaishidifu";
private static final String GYP_REPO = "Sdf1_login";
// API 地址(使用releases/latest获取最新版本)
private static final String GITHUB_API =
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
private static final String GITEE_API =
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
private static final String GYP_API =
"https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest";
// 第三路备选:宝塔静态HTML
private static final String FALLBACK_HTML =
@@ -64,6 +68,8 @@ public class UpdateChecker {
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
private static final String GITEE_LINK =
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
private static final String GYP_LINK =
"https://git.ypshidifu.cn/" + GYP_OWNER + "/" + GYP_REPO + "/releases";
// 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 5000;
@@ -157,18 +163,31 @@ public class UpdateChecker {
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
}
// ★ 双通道并行执行,一路成功掐断另一路
// ★ 三通道并行执行,一路成功掐断其余
AtomicBoolean cancelled = new AtomicBoolean(false);
AtomicReference<CheckResult> gypResult = new AtomicReference<>();
AtomicReference<CheckResult> ghResult = new AtomicReference<>();
AtomicReference<CheckResult> geResult = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(2);
CountDownLatch latch = new CountDownLatch(3);
// 通道0:git.ypshidifu.cn(最高优先级,Gitea格式)
Thread gypThread = new Thread(() -> {
if (!cancelled.get()) {
gypResult.set(tryChannel(GYP_API, "git.ypshidifu.cn", GYP_LINK));
if (gypResult.get().isSuccess()) {
cancelled.set(true);
}
}
latch.countDown();
});
gypThread.setDaemon(true);
// 通道1:GitHub
Thread ghThread = new Thread(() -> {
if (!cancelled.get()) {
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
if (ghResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路
cancelled.set(true);
}
}
latch.countDown();
@@ -180,34 +199,38 @@ public class UpdateChecker {
if (!cancelled.get()) {
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
if (geResult.get().isSuccess()) {
cancelled.set(true); // 成功则取消另一路
cancelled.set(true);
}
}
latch.countDown();
});
geThread.setDaemon(true);
gypThread.start();
ghThread.start();
geThread.start();
try {
latch.await(); // 等待两路都完成
latch.await();
} catch (InterruptedException ignored) {
}
CheckResult gyp = gypResult.get();
CheckResult gh = ghResult.get();
CheckResult ge = geResult.get();
// 选择成功的结果
// 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee)
CheckResult chosen = null;
if (gh != null && gh.isSuccess()) {
if (gyp != null && gyp.isSuccess()) {
chosen = gyp;
} else if (gh != null && gh.isSuccess()) {
chosen = gh;
} else if (ge != null && ge.isSuccess()) {
chosen = ge;
} else {
// ★ 双通道均失败,尝试第三路备选(宝塔静态HTML)
plugin.getLogger().info("[Sdf1_login] GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
CheckResult fbResult = tryFallbackChannel();
if (fbResult != null && fbResult.isSuccess()) {
chosen = fbResult;
@@ -215,7 +238,11 @@ public class UpdateChecker {
} else {
failCount++;
StringBuilder sb = new StringBuilder();
if (gyp != null && gyp.error != null) {
sb.append("§7git.ypshidifu.cn: §c").append(gyp.error);
}
if (gh != null && gh.error != null) {
if (sb.length() > 0) sb.append("\n");
sb.append("§7GitHub: §c").append(gh.error);
}
if (ge != null && ge.error != null) {
@@ -310,10 +337,15 @@ public class UpdateChecker {
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
}
// 显示另一通道的失败原因
CheckResult other = (src.equals("GitHub")) ? ge : gh;
if (other != null && !other.isSuccess() && other.error != null) {
sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error);
// 显示其他失败通道的错误信息
if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) {
sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.error);
}
if (gh != null && !gh.isSuccess() && gh.error != null && !src.equals("GitHub")) {
sender.sendMessage("§7[Sdf1_login] [更新] GitHub 不可用: " + gh.error);
}
if (ge != null && !ge.isSuccess() && ge.error != null && !src.equals("Gitee")) {
sender.sendMessage("§7[Sdf1_login] [更新] Gitee 不可用: " + ge.error);
}
});
}
File diff suppressed because it is too large Load Diff
@@ -3,14 +3,18 @@ package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
/**
* 玩家正版/离线身份验证管理器
*
* 核心逻辑:在离线模式(online-mode=false)服务器中,所有玩家UUID都是离线格式,
* 无法通过UUID区分正版/盗版。因此:
* - UUID非离线格式 → 服务器在线模式,正版玩家 → autoLogin
* - UUID离线格式 → 一律视为非正版,需手动登录或OAuth验证
* - 正版玩家通过 /mslogin OAuth验证后,由Main.addVerifiedPremiumPlayer()标记,
* 在onJoin检查点0直接autoLogin,不经过此验证器
*/
public class VerificationManager {
private final Main plugin;
@@ -29,6 +33,11 @@ public class VerificationManager {
return Bukkit.getServer().getOnlineMode();
}
/**
* 异步验证玩家身份
* 注意:此方法仅用于判断UUID格式,不执行自动登录
* 自动登录由onJoin中的检查点0/1/2控制
*/
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
@@ -50,49 +59,35 @@ public class VerificationManager {
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
java.util.UUID offlineUuid = java.util.UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
.getBytes(java.nio.charset.StandardCharsets.UTF_8));
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
// UUID不是离线格式 → 服务器在线模式的正版玩家
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
"[Sdf1_login] 结论: 正版玩家(非离线UUID)");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
// ★ 离线UUID → 无法通过UUID区分正版/盗版,一律视为非正版
// 正版玩家需通过 /mslogin OAuth验证 或 /login 手动登录
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
"[Sdf1_login] 结论: 离线UUID玩家,需手动登录");
plugin.getLogger().info(
"[Sdf1_login] (正版玩家请使用 /mslogin 或 /正版 命令验证)");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
@@ -104,55 +99,4 @@ public class VerificationManager {
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
# 非法域名后缀列表
# 每行一个后缀,支持以 * 开头表示匹配所有子域名
# 修改此文件后无需重启,插件会在每次检测时重新加载
# 默认包含常见非法注册域名后缀
com
cn
net
org
io
xyz
top
club
site
online
store
tech
info
biz
ru
tk
ml
ga
cf
cc
ws
tv
link
click
date
vip
party
fun
zone
space
+80 -7
View File
@@ -1,5 +1,5 @@
name: Sdf1_login
version: 2.2
version: 2.81
main: Sdf1_login.Main
api-version: '1.21'
commands:
@@ -42,9 +42,9 @@ commands:
签到:
description: 每日签到(中文)
usage: /签到
#oa:
# description: 申请删除账号
# usage: /oa [玩家名]
#oa:
# description: 申请删除账号
# usage: /oa [玩家名]
删除账号:
description: 申请删除账号(中文)
usage: /删除账号 [玩家名]
@@ -97,11 +97,17 @@ commands:
description: 打开商店
usage: /商店
# testorder:
# description: 测试订单中心
# usage: /testorder
# description: 测试订单中心
# usage: /testorder
protect:
description: 区域保护
usage: /protect
landrec:
description: 领地记录系统(回声碎片发放)
usage: /landrec give [玩家]
传送:
description: 传送至领地
usage: /传送 <领地名>
update:
description: 检查所有插件更新(集群控制)
usage: /update
@@ -110,4 +116,71 @@ commands:
usage: /web
控制台:
description: 生成Web端登录令牌(中文版)
usage: /控制台
usage: /控制台
mslogin:
description: 正版登录(Microsoft OAuth)
usage: /mslogin
正版:
description: 正版登录(中文版)
usage: /正版
tpa:
description: tp传送指令
usage: /tpa
tpaccept:
description: 接受传送
usage: /tpaccept
tpdeny:
description: 拒绝传送
usage: /tpdeny
tpauto:
description: 自动接收传送
usage: /tpauto
tpacancel:
description: 取消传送请求
usage: /tpacancel
tpaall:
description: 请求传送所有人
usage: /tpaall
tpahere:
description: 请求传送ta到自己身边
usage: /tpahere
sethome:
description: 设置一个家
usage: /sethome <名称>
delhome:
description: 删除一个家
usage: /delhome <名称>
home:
description: 传送到家
usage: /home [名称]
homes:
description: 查看所有家
usage: /homes
group:
description: 用户组管理
usage: /group <buy|renew|info> [组名]
back:
description: 快速返回最近记录点
usage: /back
tempban:
description: 临时封禁玩家
usage: /tempban <玩家名/IP> <时长> [理由]
tempbanip:
description: 临时封禁IP或玩家
usage: /tempbanip <IP/玩家名> <时长> [理由]
banip:
description: 永久封禁IP或玩家
usage: /banip <IP/玩家名> [永久] [理由]
whois:
description: 查询玩家信息
usage: /whois [玩家名]
who:
description: 查看在线玩家列表
usage: /who
清空队列:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /清空队列
cleartake:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /cleartake