Compare commits

...
205 Commits
Author SHA1 Message Date
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
32 changed files with 21202 additions and 2851 deletions
+13
View File
@@ -45,6 +45,19 @@ txt/
backup/
新建*.txt
群戏归类.txt
upload_sftp.py
# 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
# Windows 保留设备名(无法用普通 rm 删除)
nul
+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兑换→余额同步、扣款方向正确
+47 -5
View File
@@ -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)
-6
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>
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
+82 -36
View File
@@ -5,7 +5,6 @@ import org.bukkit.entity.Player;
import java.io.File;
import java.sql.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class BondManager {
@@ -14,20 +13,20 @@ public class BondManager {
// ===== 交易监听器 =====
public interface TransactionListener {
void onTransactionLogged(String playerName, String type, int amount);
void onTransaction(String playerName, String type, int amount);
}
private final List<TransactionListener> transactionListeners = new CopyOnWriteArrayList<>();
private final List<TransactionListener> transactionListeners = new ArrayList<>();
public void addTransactionListener(TransactionListener listener) {
transactionListeners.add(listener);
}
private void notifyTransactionListeners(String playerName, String type, int amount) {
private void fireTransactionEvent(String playerName, String type, int amount) {
for (TransactionListener listener : transactionListeners) {
try {
listener.onTransactionLogged(playerName, type, amount);
listener.onTransaction(playerName, type, amount);
} catch (Exception e) {
plugin.getLogger().warning("[BondManager] 交易监听器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
@@ -382,11 +381,11 @@ public class BondManager {
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
// 触发交易监听器
fireTransactionEvent(player, type, amount);
} catch (SQLException e) {
e.printStackTrace();
}
// ★ 通知监听器:交易已记录
notifyTransactionListeners(player, type, amount);
}
@@ -721,40 +720,87 @@ public class BondManager {
}
}
// ======================== 同步用:获取指定时间之后的交易 ========================
// ===== 获取玩家最近1小时出售总额 =====
public int getTodaySellTotal(String player) {
return getSellTotalInWindow(player, System.currentTimeMillis() - 3600000L);
}
/**
* 获取指定时间之后的所有交易记录(用于推送到Web端)
* 使用时间而非ID追踪,确保shop_buy合并后的UPDATE记录也能被同步
* @param afterTime 上次同步的最晚时间(毫秒),0表示从头开始
* @return 交易记录列表
*/
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
List<Map<String, Object>> list = new ArrayList<>();
private int getSellTotalInWindow(String player, long since) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE time > ? "
+ "ORDER BY time ASC "
+ "LIMIT 200");
"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 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> row = new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("player_name", rs.getString("player_name"));
row.put("type", rs.getString("type"));
row.put("amount", rs.getInt("amount"));
row.put("target_player", rs.getString("target_player"));
row.put("operator", rs.getString("operator"));
row.put("reason", rs.getString("reason"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("balance_after", rs.getInt("balance_after"));
row.put("time", rs.getLong("time"));
list.add(row);
Map<String, Object> tx = new LinkedHashMap<>();
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 list;
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,
+661 -6
View File
@@ -1,7 +1,11 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
@@ -34,10 +38,484 @@ public class ChatFilterManager {
new ConcurrentHashMap<>();
private final Map<String, String> messages =
new LinkedHashMap<>();
// ★ 广告机检测:记录每个玩家发送链接的频率
private final Map<String, List<Long>> linkSendHistory =
new ConcurrentHashMap<>();
// ★ 挂机检测
private final Map<String, Long> playerLastActiveTime =
new ConcurrentHashMap<>();
// ★ 管理员邮件地址
private String adminEmail = "admin@example.com";
private boolean notifyAdmin = true;
private boolean notifyAll = false;
private int muteDuration = 300;
private boolean enabled = true;
// ★ 非法域名后缀列表(热更新)
private final Set<String> illegalDomainSuffixes = new HashSet<>();
// ============================================================
// ★ 新玩家验证码系统
// ============================================================
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
/** 验证码数据存储(public供Main.java访问清理) */
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
/** 验证码类型对外公开 */
public enum VerificationType {
MATH, // 数学题
GUI // GUI物品选择
}
static class VerificationData {
long createTime;
int a, b, op; // 0=加法, 1=减法, 2=乘法
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
VerificationType type; // 验证码类型
boolean completed;
VerificationData(int a, int b, int op, VerificationType type) {
this.createTime = System.currentTimeMillis();
this.a = a; this.b = b; this.op = op;
this.type = type;
}
boolean isExpired() {
return System.currentTimeMillis() - createTime > 30000;
}
}
/** 验证码结果:通过/失败 */
public enum VerificationResult {
NEED_VERIFICATION, // 需要验证码
PENDING, // 验证码进行中
VERIFIED, // 已验证
FAILED // 验证失败(消息吞噬)
}
/** 验证码答案缓存,供外部判断是否为答案 */
public boolean isPendingVerificationAnswer(String playerName, String msg) {
VerificationData vd = verificationData.get(playerName);
if (vd == null || vd.completed) return false;
// 尝试作为答案检查
VerificationResult result = checkMathAnswer(playerName, msg);
return result != VerificationResult.FAILED; // 通过或进行中都是有效的答案输入
}
/**
* 检查玩家是否需要验证码
*/
public VerificationResult checkNewPlayerVerification(Player p) {
String name = p.getName();
// ★ 永久已验证的玩家 → 直接放行
if (verifiedPlayers.contains(name)) {
return VerificationResult.VERIFIED;
}
// 正在验证中(等待答案)
if (verificationData.containsKey(name)) {
VerificationData vd = verificationData.get(name);
if (vd.completed) {
verifiedPlayers.add(name);
verificationData.remove(name);
return VerificationResult.VERIFIED;
}
// 验证码是否过期 — 过期后自动放行,不无限卡玩家
if (vd.isExpired()) {
verificationData.remove(name);
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
}
return VerificationResult.PENDING;
}
// 检查玩家加入时间是否≤1小时
if (isNewPlayerUnder1Hour(p)) {
return VerificationResult.NEED_VERIFICATION;
}
// 老玩家:直接永久验证通过
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
}
/** 全局共享的Random实例,避免频繁创建产生相同序列 */
private static final ThreadLocal<Random> sharedRandom = ThreadLocal.withInitial(Random::new);
/**
* 生成随机验证码:数学题或GUI随机50%概率
*/
public void generateMathVerification(String playerName) {
Random rand = sharedRandom.get();
// 50% 概率数学题,50% 概率GUI
if (rand.nextBoolean()) {
generateMathChallenge(playerName, rand);
} else {
generateGUIChallenge(playerName, rand);
}
}
/** 生成数学题验证码 */
private void generateMathChallenge(String playerName, Random rand) {
int op = rand.nextInt(3); // 0=加, 1=减, 2=乘
int a, b;
switch (op) {
case 0: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
case 1: a = rand.nextInt(10) + 2; b = rand.nextInt(a) + 1; break;
default: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
}
VerificationData vd = new VerificationData(a, b, op, VerificationType.MATH);
verificationData.put(playerName, vd);
String opSymbol = switch (op) {
case 0 -> "+";
case 1 -> "-";
default -> "×";
};
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
Player p = org.bukkit.Bukkit.getPlayer(playerName);
if (p != null && p.isOnline()) {
p.sendMessage("§e§l[验证码] §f请回答: §e" + a + " " + opSymbol + " " + b + " = ?");
p.sendMessage("§7(30秒内输入答案)");
}
});
}
/** 中文物品名映射表 */
private static final Map<String, String> MATERIAL_CN = new HashMap<>();
static {
MATERIAL_CN.put("EMERALD", "绿宝石");
MATERIAL_CN.put("DIAMOND", "钻石");
MATERIAL_CN.put("IRON_INGOT", "铁锭");
MATERIAL_CN.put("GOLD_INGOT", "金锭");
MATERIAL_CN.put("STONE", "石头");
MATERIAL_CN.put("COBBLESTONE", "圆石");
MATERIAL_CN.put("SAND", "沙子");
MATERIAL_CN.put("GRAVEL", "砂砾");
MATERIAL_CN.put("REDSTONE", "红石粉");
MATERIAL_CN.put("LAPIS_LAZULI", "青金石");
MATERIAL_CN.put("QUARTZ", "石英");
MATERIAL_CN.put("APPLE", "苹果");
MATERIAL_CN.put("BOWL", "碗");
MATERIAL_CN.put("CAKE", "蛋糕");
MATERIAL_CN.put("BOOK", "书");
MATERIAL_CN.put("FLINT", "燧石");
MATERIAL_CN.put("FLINT_AND_STEEL", "打火石");
MATERIAL_CN.put("LEATHER_HELMET", "皮革头盔");
MATERIAL_CN.put("LEATHER_CHESTPLATE", "皮革上衣");
MATERIAL_CN.put("LEATHER_LEGGINGS", "皮革护腿");
MATERIAL_CN.put("LEATHER_BOOTS", "皮革靴子");
MATERIAL_CN.put("GOLDEN_APPLE", "附魔金苹果");
MATERIAL_CN.put("OBSIDIAN", "黑曜石");
MATERIAL_CN.put("DIAMOND_HELMET", "钻石头盔");
MATERIAL_CN.put("DIAMOND_CHESTPLATE", "钻石护甲");
MATERIAL_CN.put("DIAMOND_LEGGINGS", "钻石护腿");
MATERIAL_CN.put("DIAMOND_BOOTS", "钻石靴子");
MATERIAL_CN.put("IRON_HELMET", "铁头盔");
MATERIAL_CN.put("IRON_CHESTPLATE", "铁护甲");
MATERIAL_CN.put("IRON_LEGGINGS", "铁护腿");
MATERIAL_CN.put("IRON_BOOTS", "铁靴子");
MATERIAL_CN.put("WOODEN_SWORD", "木剑");
MATERIAL_CN.put("STONE_SWORD", "石剑");
MATERIAL_CN.put("IRON_SWORD", "铁剑");
MATERIAL_CN.put("GOLDEN_SWORD", "金剑");
MATERIAL_CN.put("DIAMOND_SWORD", "钻石剑");
MATERIAL_CN.put("GREEN_CONCRETE", "绿色混凝土");
MATERIAL_CN.put("LIME_CONCRETE", "淡绿色混凝土");
MATERIAL_CN.put("JUNGLE_PLANKS", "丛林木板");
MATERIAL_CN.put("ACACIA_PLANKS", "金合欢木板");
MATERIAL_CN.put("BLAZE_ROD", "烈焰棒");
MATERIAL_CN.put("MAGMA_CREAM", "熔浆膏");
MATERIAL_CN.put("CLAY_BALL", "黏土球");
MATERIAL_CN.put("MUSIC_DISC_13", "音乐唱片C13");
MATERIAL_CN.put("MUSIC_DISC_PIGSTEP", "音乐唱片PigStep");
MATERIAL_CN.put("END_CRYSTAL", "末影水晶");
MATERIAL_CN.put("GLOWSTONE", "萤石粉");
MATERIAL_CN.put("DRAGON_EGG", "龙蛋");
MATERIAL_CN.put("NETHERITE_SCRAP", "下界合金碎片");
MATERIAL_CN.put("CREEPER_HEAD", "爬行者头颅");
MATERIAL_CN.put("GOLDEN_APPLE", "金苹果");
MATERIAL_CN.put("ENCHANTED_GOLDEN_APPLE", "附魔金苹果");
}
/** 生成中文物品名 */
private String toChineseName(String materialName) {
String upper = materialName.toUpperCase();
return MATERIAL_CN.getOrDefault(upper, materialName.replace("_", " "));
}
/** 生成GUI验证码:N个物品+1个正确答案,30秒超时 */
private void generateGUIChallenge(String playerName, Random rand) {
// 丰富物品池,保证足够多样性
Material[] materials = {Material.STONE, Material.COBBLESTONE, Material.GRAVEL,
Material.SAND, Material.CLAY_BALL, Material.REDSTONE,
Material.LAPIS_LAZULI, Material.QUARTZ, Material.IRON_INGOT,
Material.GOLD_INGOT, Material.DIAMOND, Material.EMERALD,
Material.GREEN_CONCRETE, Material.LIME_CONCRETE,
Material.JUNGLE_PLANKS, Material.ACACIA_PLANKS,
Material.BLAZE_ROD, Material.CREEPER_HEAD, Material.DRAGON_EGG,
Material.NETHERITE_SCRAP, Material.END_CRYSTAL, Material.GLOWSTONE,
Material.MAGMA_CREAM, Material.MUSIC_DISC_13, Material.MUSIC_DISC_PIGSTEP,
Material.BOOK, Material.BOWL, Material.CAKE, Material.FLINT,
Material.FLINT_AND_STEEL, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.APPLE,
Material.GOLDEN_APPLE, Material.ENCHANTED_GOLDEN_APPLE,
Material.OBSIDIAN, Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE,
Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.IRON_HELMET,
Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS,
Material.WOODEN_SWORD, Material.STONE_SWORD, Material.IRON_SWORD,
Material.GOLDEN_SWORD, Material.DIAMOND_SWORD};
// 随机选1个正确答案
Material correctItem = materials[rand.nextInt(materials.length)];
String chineseName = toChineseName(correctItem.name());
// 选N-1个干扰项(5~10个物品,其中1个正确,其余干扰)
int totalItems = 5 + rand.nextInt(6); // 5~10
List<Material> distractorPool = new ArrayList<>();
for (Material m : materials) {
if (m != correctItem) distractorPool.add(m);
}
Collections.shuffle(distractorPool, rand);
List<Material> itemsToPlace = new ArrayList<>();
itemsToPlace.add(correctItem);
for (int i = 0; i < Math.min(totalItems - 1, distractorPool.size()); i++) {
itemsToPlace.add(distractorPool.get(i));
}
// 随机打乱物品位置
Collections.shuffle(itemsToPlace, rand);
// 存储:正确答案的Material名(用英文做校验),显示用中文名
VerificationData vd = new VerificationData(0, 0, 0, VerificationType.GUI);
vd.guiTargets = correctItem.name(); // 校验用英文
vd.type = VerificationType.GUI;
verificationData.put(playerName, vd);
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
Player p = org.bukkit.Bukkit.getPlayer(playerName);
if (p != null && p.isOnline()) {
try {
// 在GUI标题中明确显示中文目标物品名
Inventory inv = Bukkit.createInventory(null, 27, "§e§l点击" + chineseName + "完成验证");
// 填充灰色玻璃半透明方块作为背景
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta gm = glass.getItemMeta();
if (gm != null) { gm.setDisplayName(" "); glass.setItemMeta(gm); }
for (int i = 0; i < 27; i++) inv.setItem(i, glass);
// 随机打乱物品放置位置(避开边框slot 0-8, 18-26, 9,14,19)
List<Integer> availableSlots = Arrays.asList(9, 10, 11, 13, 14, 15, 17, 19, 20, 21);
Collections.shuffle(availableSlots, rand);
int itemIdx = 0;
for (int slot : availableSlots) {
if (itemIdx >= itemsToPlace.size()) break;
Material mat = itemsToPlace.get(itemIdx);
String cnName = toChineseName(mat.name());
inv.setItem(slot, mkItem(mat, "§f" + cnName, ""));
itemIdx++;
}
p.openInventory(inv);
} catch (Exception ex) {
plugin.getLogger().warning("[验证码] GUI挑战生成失败: " + ex.getMessage());
// 降级为数学题
generateMathChallenge(playerName, rand);
}
}
});
}
// 辅助方法:创建带显示名的物品
private ItemStack mkItem(Material mat, String displayName, String lore) {
ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.setDisplayName(displayName);
List<String> lores = new ArrayList<>();
lores.add(lore);
meta.setLore(lores);
stack.setItemMeta(meta);
}
return stack;
}
public VerificationResult checkAnswer(String playerName, String answerStr) {
VerificationData vd = verificationData.get(playerName);
if (vd == null) return VerificationResult.VERIFIED;
if (vd.isExpired()) {
verificationData.remove(playerName);
verifiedPlayers.add(playerName);
return VerificationResult.VERIFIED;
}
if (vd.type == VerificationType.MATH) {
return checkMathAnswer(playerName, answerStr);
}
// GUI验证码 — 聊天输入不作为答案,返回PENDING等待GUI点击
return VerificationResult.PENDING;
}
/**
* 检查玩家回答是否正确
* @deprecated 使用 checkAnswer 代替
*/
@Deprecated
public VerificationResult checkMathAnswer(String playerName, String answerStr) {
VerificationData vd = verificationData.get(playerName);
if (vd == null) return VerificationResult.VERIFIED; // 无验证码 → 放行
if (vd.isExpired()) {
verificationData.remove(playerName);
verifiedPlayers.add(playerName); // 过期后不再要求
return VerificationResult.VERIFIED;
}
if (vd.type == VerificationType.MATH) {
try {
int ans = Integer.parseInt(answerStr.trim());
int expected;
switch (vd.op) {
case 0 -> expected = vd.a + vd.b;
case 1 -> expected = vd.a - vd.b;
default -> expected = vd.a * vd.b;
}
if (ans == expected) {
vd.completed = true;
verificationData.remove(playerName);
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
return VerificationResult.VERIFIED;
} else {
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
return VerificationResult.FAILED;
}
} catch (NumberFormatException e) {
// 非数字答案 → 同样不放行,重新答题
return VerificationResult.FAILED;
}
}
// GUI验证码 — answerStr 为空时表示未回答
if (answerStr == null || answerStr.isEmpty()) {
return VerificationResult.PENDING;
}
return VerificationResult.FAILED;
}
/**
* 处理GUI物品点击验证码
* @param playerName 玩家名
* @param clickedItemName 点击的物品Material名(如"STONE")
* @return 验证结果
*/
public VerificationResult checkGUIClick(String playerName, String clickedItemName) {
String name = playerName;
VerificationData vd = verificationData.get(name);
if (vd == null) return VerificationResult.VERIFIED;
if (!VerificationType.GUI.equals(vd.type)) return VerificationResult.PENDING;
if (vd.isExpired()) {
verificationData.remove(name);
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
}
// ★ 关键修复:点击的物品名必须等于目标物品名才通过
if (clickedItemName != null && clickedItemName.equals(vd.guiTargets)) {
vd.completed = true;
verificationData.remove(name);
verifiedPlayers.add(name);
return VerificationResult.VERIFIED;
}
// 点击错误物品 → 消息拦截
return VerificationResult.FAILED;
}
// ★ 临时缓存玩家未验证的消息(N+1 机制:缓存列表,答对第N题后释放N条)
public final Map<String, List<String>> pendingMessages = new ConcurrentHashMap<>();
/** 缓存玩家消息,等验证通过时代为广播 */
public void cachePendingMessage(String playerName, String message) {
pendingMessages.computeIfAbsent(playerName, k -> new ArrayList<>()).add(message);
}
/** 广播所有缓存的消息(N+1机制:广播缓存的N-1条 + 当前第N条) */
public void broadcastCachedMessages(String playerName) {
List<String> messages = pendingMessages.remove(playerName);
if (messages != null) {
for (String msg : messages) {
Bukkit.broadcastMessage(msg);
}
}
}
/** 清除玩家缓存的消息(验证失败时调用) */
public void clearPendingMessages(String playerName) {
pendingMessages.remove(playerName);
}
/**
* 清理过期的验证码
*/
public void cleanupExpiredVerifications() {
long now = System.currentTimeMillis();
verificationData.entrySet().removeIf(e -> {
if (e.getValue().isExpired() && !e.getValue().completed) {
return true;
}
return false;
});
}
/**
* 检查玩家是否为新玩家(register_time 在1小时内)
*/
public boolean isNewPlayerUnder1Hour(Player p) {
if (p == null || !p.isOnline()) return false;
try {
Main mainPlugin = (Main) plugin;
DatabaseManager db = mainPlugin.getDb();
if (db == null) return false; // 查不到就不拦截,直接放行
// register_time 在数据库存的是秒级时间戳
Object regTimeObj = db.getField(p.getName(), "register_time");
if (regTimeObj == null) return false; // 没注册记录 → 放行
long regTimeSeconds = ((Number) regTimeObj).longValue();
if (regTimeSeconds == 0) return false; // 注册时间为0 → 放行
long regTimeMs = regTimeSeconds * 1000;
long elapsed = System.currentTimeMillis() - regTimeMs;
boolean isNew = elapsed < 3600000;
// 日志调试:打印注册时间差值
if (isNew) {
plugin.getLogger().info("[验证码] 新玩家: " + p.getName() + " 注册" + (elapsed/1000) + "秒前");
}
return isNew;
} catch (Exception e) {
return false; // 出错时默认放行,不卡玩家
}
}
// ==================== 广告机检测 ====================
public boolean isEnabled() {
return enabled;
@@ -50,12 +528,15 @@ public class ChatFilterManager {
private static final Pattern URL_PATTERN =
Pattern.compile(
"(?i)(?:https?://\\S+)"
+ "|(?:(?:[a-zA-Z0-9]"
+ "(?:[a-zA-Z0-9\\-]*"
+ "[a-zA-Z0-9])?\\.)+"
+ "[a-zA-Z]{2,})"
+ "(?:/\\S*)?");
"(?i)"
// 1. 标准URL: http://xxx.com/...
+ "(?:https?://\\S+)"
// 2. 点分隔域名: xxx.com, my.site.cn
+ "|(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,})(?:/\\S*)?"
// 3. 下划线分隔域名: xxx_com, my_site.cn, 123_ypshidifu.cn
+ "|(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)"
+ "(?:[_.][a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*"
+ "[_.][a-zA-Z]{2,}");
private static final Pattern DOT_VARIANTS =
Pattern.compile(
"[\uff0e\u3002\u2025\u2026\u00b7]");
@@ -609,4 +1090,178 @@ public class ChatFilterManager {
|| s.contains("启用")
|| s.contains("是");
}
// ==================== 广告机检测 ====================
/**
* 检查消息是否包含第三方推广内容(邮件、游戏内推广等)
*/
public boolean isAdvertisingContent(String msg) {
String lower = msg.toLowerCase();
// 检测关键词
String[] keywords = {"群", "群号", "QQ", "qq群", "加群", "交流群", "服务器", "开服", "联机", "白嫖", "免费送", "折扣", "打折", "代充", "充值优惠", "外挂", "脚本", "辅助", "破解"};
for (String kw : keywords) {
if (lower.contains(kw)) return true;
}
return false;
}
/**
* 记录玩家发送链接的行为,检测广告机
* @return 如果检测到广告机返回true
*/
public boolean checkAdBotBehavior(String playerName, String msg) {
long now = System.currentTimeMillis();
// 提取URL
List<String> urls = extractUrls(msg);
if (urls.isEmpty()) return false;
// 检查是否全部非白名单
boolean hasNonWhitelisted = false;
for (String url : urls) {
if (!isWhitelisted(url)) {
hasNonWhitelisted = true;
break;
}
}
if (!hasNonWhitelisted) return false;
// 检查是否包含推广关键词
if (!isAdvertisingContent(msg)) return false;
// 记录链接发送时间
linkSendHistory.computeIfAbsent(playerName, k -> new ArrayList<>()).add(now);
// 清理超过10分钟前的记录
List<Long> history = linkSendHistory.get(playerName);
history.removeIf(t -> (now - t) > 600000);
// 10分钟内发送超过3次非白名单链接 → 广告机
if (history.size() >= 3) {
linkSendHistory.remove(playerName);
return true; // 检测到广告机
}
return false;
}
/**
* 标记玩家活跃(用于挂机检测)
*/
public void markPlayerActive(String playerName) {
playerLastActiveTime.put(playerName, System.currentTimeMillis());
}
/**
* 检查玩家是否长时间挂机(超过5分钟)
*/
public boolean isPlayerIdle(String playerName, long idleThresholdMs) {
Long lastActive = playerLastActiveTime.get(playerName);
if (lastActive == null) return true;
return (System.currentTimeMillis() - lastActive) > idleThresholdMs;
}
/**
* 发送管理员通知邮件
*/
public void sendAdminNotification(String subject, String body) {
// 异步发送,不阻塞主线程
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
EmailManager emailMgr = ((Main) plugin).getEmail();
if (emailMgr != null) {
emailMgr.sendBody(adminEmail, subject, body);
plugin.getLogger().info("[广告机检测] 已发送管理员通知: " + subject);
}
} catch (Exception e) {
plugin.getLogger().warning("[广告机检测] 发送管理员通知失败: " + e.getMessage());
}
});
}
// ==================== 非法域名后缀管理 ====================
/**
* 加载非法域名后缀配置文件(支持热更新)
* 配置文件: plugins/Sdf1_login/illegal_domains.txt
* 每行一个后缀,如: com, cn, test 等
*/
public void loadIllegalDomains() {
illegalDomainSuffixes.clear();
File f = new File(plugin.getDataFolder(), "illegal_domains.txt");
if (!f.exists()) {
plugin.getLogger().info("[非法域名] 配置文件不存在,已加载默认内置后缀");
// 内置默认后缀作为兜底
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
illegalDomainSuffixes.add(s.toLowerCase());
}
return;
}
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
// 去掉可能的 * 前缀
String suffix = line.replaceFirst("^\\*", "").toLowerCase();
if (!suffix.isEmpty()) {
illegalDomainSuffixes.add(suffix);
}
}
plugin.getLogger().info("[非法域名] 已加载 " + illegalDomainSuffixes.size() + " 个非法后缀(来自配置文件)");
} catch (Exception e) {
plugin.getLogger().warning("[非法域名] 加载失败,使用内置默认后缀: " + e.getMessage());
// 加载失败时用内置兜底
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
illegalDomainSuffixes.add(s.toLowerCase());
}
}
}
/**
* 检查名称是否包含非法域名格式(如 baidu_com, mysite.cn 等)
*/
public boolean containsIllegalDomain(String name) {
if (name == null || name.isEmpty()) return false;
String lowerName = name.toLowerCase();
// 支持两种格式:1) dot分隔 xxx.com 2) underscore分隔 xxx_com
for (String separator : new String[]{".", "_"}) {
int idx = lowerName.lastIndexOf(separator);
if (idx < 0) continue;
String suffix = lowerName.substring(idx + 1);
// 去掉可能的前缀嵌套
if (suffix.contains(separator)) {
suffix = suffix.substring(suffix.lastIndexOf(separator) + 1);
}
for (String domainSuffix : illegalDomainSuffixes) {
if (suffix.equals(domainSuffix) && !suffix.isEmpty()) {
// 前面必须有主体部分(排除纯后缀如 .com 本身)
String before = lowerName.substring(0, idx);
if (!before.isEmpty() && before.length() >= 2) {
return true;
}
}
}
}
return false;
}
/**
* 定时检查空闲玩家
*/
public void checkIdlePlayers() {
long idleThreshold = 5 * 60 * 1000L; // 5分钟
org.bukkit.Bukkit.getOnlinePlayers().forEach(p -> {
if (isPlayerIdle(p.getName(), idleThreshold)) {
// 不再发送空闲消息,仅刷新计时
playerLastActiveTime.put(p.getName(), System.currentTimeMillis()); // 刷新计时
}
});
}
}
@@ -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";
@@ -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
+157 -56
View File
@@ -88,6 +88,9 @@ public class DatabaseManager {
"TEXT DEFAULT ''");
safeAdd(st, "last_login_ip",
"TEXT DEFAULT ''");
// UI偏好:1=CLI(默认), 0=GUI
safeAdd(st, "ui_mode",
"INTEGER DEFAULT 1");
// ===== 背包备份表:安全迁移 =====
// 检查旧表是否存在(用 inventory_data 列判断)
@@ -255,6 +258,30 @@ public class DatabaseManager {
+ ")");
safeAdd(st, "menu_snowball",
"INTEGER DEFAULT 1");
// Home传送点表 — 只在不存在时创建,不删除已有数据
st.execute("CREATE TABLE IF NOT EXISTS homes ("
+ "player_name TEXT NOT NULL,"
+ "home_name TEXT NOT NULL,"
+ "world TEXT NOT NULL,"
+ "x REAL NOT NULL,"
+ "y REAL NOT NULL,"
+ "z REAL NOT NULL,"
+ "yaw REAL DEFAULT 0,"
+ "pitch REAL DEFAULT 0,"
+ "created_at INTEGER DEFAULT 0,"
+ "PRIMARY KEY(player_name, home_name)"
+ ")");
safeAdd(st, "menu_snowball",
"INTEGER DEFAULT 1");
// 传送请求表
st.execute("CREATE TABLE IF NOT EXISTS teleport_requests ("
+ "sender TEXT NOT NULL,"
+ "receiver TEXT NOT NULL,"
+ "type TEXT NOT NULL DEFAULT 'tpa',"
+ "timestamp INTEGER NOT NULL DEFAULT 0"
+ ")");
st.close();
logger.info("[Sdf1_login] 数据库初始化完成");
} catch (Exception e) {
@@ -274,6 +301,56 @@ public class DatabaseManager {
}
}
// ==================== 自动续费偏好 ====================
/** 获取玩家的自动续费偏好 */
public boolean getPlayerAutoRenew(String player, String group) {
try {
Statement st = db.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
String sql = "SELECT auto_renew FROM player_auto_renew WHERE player_name = ? AND group_name = ?";
PreparedStatement ps = db.prepareStatement(sql);
ps.setString(1, player);
ps.setString(2, group);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getInt("auto_renew") == 1;
} catch (Exception ignored) {}
return false;
}
/** 设置玩家的自动续费偏好 */
public void setPlayerAutoRenew(String player, String group, boolean value) {
try {
Statement st = db.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
String sql = "INSERT OR REPLACE INTO player_auto_renew (player_name, group_name, auto_renew) VALUES (?, ?, ?)";
PreparedStatement ps = db.prepareStatement(sql);
ps.setString(1, player);
ps.setString(2, group);
ps.setInt(3, value ? 1 : 0);
ps.executeUpdate();
} catch (Exception ignored) {}
}
/** 获取玩家所有自动续费开启的组 */
public List<Map<String, Object>> getAutoRenewGroups(String player) {
List<Map<String, Object>> result = new ArrayList<>();
try {
Statement st = db.createStatement();
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
String sql = "SELECT group_name FROM player_auto_renew WHERE player_name = ? AND auto_renew = 1";
PreparedStatement ps = db.prepareStatement(sql);
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
row.put("group_name", rs.getString("group_name"));
result.add(row);
}
} catch (Exception ignored) {}
return result;
}
public void close() {
try {
if (db != null && !db.isClosed())
@@ -281,6 +358,51 @@ public class DatabaseManager {
} catch (Exception ignored) {
}
}
public Connection getConnection() {
return db;
}
// ==================== UI偏好 ====================
/**
* 获取玩家UI偏好
* @return 1=CLI(默认), 0=GUI
*/
public int getUiMode(String playerName) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT ui_mode FROM users WHERE player_name=?");
ps.setString(1, playerName);
ResultSet rs = ps.executeQuery();
int mode = 1; // 默认CLI
if (rs.next()) {
mode = rs.getInt("ui_mode");
}
rs.close();
ps.close();
return mode;
} catch (SQLException e) {
return 1; // 默认CLI
}
}
/**
* 设置玩家UI偏好
* @param mode 1=CLI, 0=GUI
*/
public void setUiMode(String playerName, int mode) {
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE users SET ui_mode=? WHERE player_name=?");
ps.setInt(1, mode);
ps.setString(2, playerName);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
// 静默
}
}
// ==================== 菜单图标 ====================
public void saveMenuIcon(String name,
@@ -438,9 +560,11 @@ public class DatabaseManager {
? String.valueOf(ip) : null;
}
public boolean checkPasswordOrTemp(
String name, String hash) {
// 先检查主密码
/**
* 验证密码(仅主密码)
* @return true if matched
*/
public boolean checkPassword(String name, String hash) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT password_hash "
@@ -453,8 +577,7 @@ public class DatabaseManager {
rs.getString("password_hash");
rs.close();
ps.close();
if (hash.equals(mainHash))
return true;
return hash.equals(mainHash);
} else {
rs.close();
ps.close();
@@ -462,47 +585,51 @@ public class DatabaseManager {
} catch (SQLException e) {
e.printStackTrace();
}
// 再检查临时密码
return false;
}
/**
* 验证密码(主密码优先,不通过再查临时密码)
* 注意: 传入的 hash 必须是主密码的 hash
* @return "main" | "temp" | null
*/
public String checkPasswordWithFallback(String name, String mainHash) {
// 先检查主密码
if (checkPassword(name, mainHash)) {
return "main";
}
// 主密码不匹配,检查临时密码
try {
PreparedStatement ps2 = db.prepareStatement(
PreparedStatement ps = db.prepareStatement(
"SELECT temp_password, "
+ "temp_pw_expire, "
+ "temp_pw_used "
+ "FROM users "
+ "WHERE player_name=?");
ps2.setString(1, name);
ResultSet rs2 = ps2.executeQuery();
if (rs2.next()) {
String tempHash =
rs2.getString(
"temp_password");
long expire =
rs2.getLong(
"temp_pw_expire");
int used =
rs2.getInt(
"temp_pw_used");
rs2.close();
ps2.close();
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String tempHash = rs.getString("temp_password");
long expire = rs.getLong("temp_pw_expire");
int used = rs.getInt("temp_pw_used");
rs.close();
ps.close();
if (tempHash != null
&& !tempHash.isEmpty()
&& hash.equals(tempHash)
&& System.currentTimeMillis()
< expire
&& mainHash.equals(tempHash)
&& System.currentTimeMillis() < expire
&& used != 1) {
return true;
return "temp";
}
} else {
rs2.close();
ps2.close();
rs.close();
ps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
return null;
}
public Map<String, Object> getUser(String name) {
Map<String, Object> r =
new LinkedHashMap<>();
@@ -569,32 +696,6 @@ public class DatabaseManager {
}
}
public boolean checkPassword(String name,
String hash) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT password_hash "
+ "FROM users "
+ "WHERE player_name=?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String stored =
rs.getString("password_hash");
rs.close();
ps.close();
return hash.equals(stored);
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public boolean isUsingTempPassword(String name) {
String h = (String) getField(name,
"temp_password");
+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());
}
}
}
+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);
+462
View File
@@ -0,0 +1,462 @@
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;
}
}
/**
* 获取全局家数量限制
*/
private int getGlobalHomeLimit() {
// 默认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();
}
}
+26 -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;
}
@@ -162,6 +177,10 @@ public class LoginManager {
name, hash);
if (matchMain) {
plugin.getLoggedIn().add(name);
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
if (plugin.webManager != null) {
plugin.webManager.recordJavaLogin(name);
}
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setLoggedIn(
@@ -201,12 +220,16 @@ public class LoginManager {
return true;
}
boolean matchTemp =
String pwdResult =
plugin.getDb()
.checkPasswordOrTemp(
.checkPasswordWithFallback(
name, hash);
if (matchTemp) {
if (pwdResult != null) {
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);
File diff suppressed because it is too large Load Diff
+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),
+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;
+245
View File
@@ -0,0 +1,245 @@
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;
}
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();
}
}
+336 -3
View File
@@ -16,6 +16,7 @@ import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Base64;
import java.text.SimpleDateFormat;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.bukkit.potion.PotionEffect;
@@ -30,6 +31,11 @@ public class ShopManager implements Listener {
private final Main plugin;
private final List<ShopCategory> categories =
new ArrayList<>();
// ===== 出售限额配置 =====
private String maxSellMessage = "";
private boolean maxSellMessageEnabled = false;
private int maxSellLimit = 15000; // 默认1.5万
private final Object maxSellLock = new Object();
private final List<RefundRecord> refundRecords =
new ArrayList<>();
private final Map<UUID, Integer> activeDiscount =
@@ -140,6 +146,7 @@ public class ShopManager implements Listener {
public ShopManager(Main plugin) {
this.plugin = plugin;
loadCategories();
loadMaxSellConfig();
startHourlyTask();
loadItemNbtData();
startCleanupTask();
@@ -1369,9 +1376,65 @@ public class ShopManager implements Listener {
}
public void saveAll() {
for (ShopCategory cat : categories)
saveCategory(cat);
}
for (ShopCategory cat : categories)
saveCategory(cat);
saveMaxSellConfig();
}
// ===== maxSell 配置持久化 =====
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
private void loadMaxSellConfig() {
File cfgFile = new File(plugin.getDataFolder(), "maxsell_config.txt");
try {
if (cfgFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("maxSellLimit=")) {
try {
maxSellLimit = Integer.parseInt(line.substring("maxSellLimit=".length()));
} catch (NumberFormatException ignored) {}
} else if (line.startsWith("maxSellMessageEnabled=")) {
maxSellMessageEnabled = Boolean.parseBoolean(line.substring("maxSellMessageEnabled=".length()));
} else if (line.startsWith("maxSellMessage=")) {
maxSellMessage = line.substring("maxSellMessage=".length());
}
}
br.close();
} else {
// 配置文件不存在,使用默认值并保存
maxSellLimit = 15000;
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
maxSellMessageEnabled = true;
saveMaxSellConfig();
}
} catch (Exception e) {
plugin.getLogger().warning("[Shop] 加载maxsell配置失败: " + e.getMessage());
maxSellLimit = 15000;
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
maxSellMessageEnabled = true;
}
}
/** 保存 maxSell 配置到插件数据文件夹下的 maxsell_config.txt */
private void saveMaxSellConfig() {
synchronized (maxSellLock) {
try {
File dir = new File(plugin.getDataFolder(), "shop");
if (!dir.exists()) dir.mkdirs();
File cfgFile = new File(dir.getParentFile(), "maxsell_config.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(cfgFile, StandardCharsets.UTF_8));
bw.write("maxSellLimit=" + maxSellLimit);
bw.newLine();
bw.write("maxSellMessageEnabled=" + maxSellMessageEnabled);
bw.newLine();
bw.write("maxSellMessage=" + maxSellMessage);
bw.close();
} catch (Exception e) {
plugin.getLogger().warning("[Shop] 保存maxsell配置失败: " + e.getMessage());
}
}
}
public ItemStack getShopStack(
ShopItem item, int amount) {
// 优先级1: 手动捕获的NBT数据
@@ -1555,6 +1618,179 @@ public class ShopManager implements Listener {
// ===== 出售 =====
/**
* HTML解码器,支持:
* - &颜色代码 (如 &c红色, &a绿色)
* - <b>加粗</b>, <u>下划线</u>, <i>斜体</i>
* - <br>换行
* - <p style="...">段落</p> (支持color属性)
*/
private String decodeHtml(String input) {
if (input == null || input.isEmpty()) return input;
String result = input;
// 1. 先处理 &颜色代码 → §颜色代码
result = result.replace("&0", "§0");
result = result.replace("&1", "§1");
result = result.replace("&2", "§2");
result = result.replace("&3", "§3");
result = result.replace("&4", "§4");
result = result.replace("&5", "§5");
result = result.replace("&6", "§6");
result = result.replace("&7", "§7");
result = result.replace("&8", "§8");
result = result.replace("&9", "§9");
result = result.replace("&a", "§a");
result = result.replace("&b", "§b");
result = result.replace("&c", "§c");
result = result.replace("&d", "§d");
result = result.replace("&e", "§e");
result = result.replace("&f", "§f");
// 样式代码
result = result.replace("&k", "§k");
result = result.replace("&l", "§l");
result = result.replace("&m", "§m");
result = result.replace("&n", "§n");
result = result.replace("&o", "§o");
result = result.replace("&r", "§r");
// 2. 处理HTML标签
// <u>标签:§n 开始下划线,§r 切断(只作用于标签内)
// <b>标签:§l 开始加粗,§r 切断
// <i>标签:§o 开始斜体,§r 切断
result = result.replaceAll("<u>(.*?)</u>", "§n$1§r");
result = result.replaceAll("<b>(.*?)</b>", "§l$1§r");
result = result.replaceAll("<i>(.*?)</i>", "§o$1§r");
result = result.replaceAll("<br>", "\n");
result = result.replaceAll("<br/>", "\n");
// 3. 处理 <p style="color:#xxx"> 或 <p style="color:xxx">
java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile(
"<p\\s+style=[\"']color:\\s*(#[0-9a-fA-F]{3,8}|[a-zA-Z_]+|rgb\\([^)]+\\)|rgba\\([^)]+\\)|hsl\\([^)]+\\)|hsla\\([^)]+\\))\\s*;?[\"']\\s*>(.*?)</p>",
java.util.regex.Pattern.CASE_INSENSITIVE);
java.util.regex.Matcher pMatcher = pPattern.matcher(result);
StringBuilder sb = new StringBuilder();
while (pMatcher.find()) {
String color = pMatcher.group(1);
String text = pMatcher.group(2);
String mcColor = convertToMcColor(color);
pMatcher.appendReplacement(sb, mcColor + text + "§r");
}
pMatcher.appendTail(sb);
result = sb.toString();
// 4. 处理 <p>段落</p> (无style) → 换行分隔
result = result.replaceAll("<p>(.*?)</p>", "$1\n");
// 5. 移除残留的style标签属性
result = result.replaceAll("<p[^>]*>", "");
result = result.replaceAll("</p>", "");
return result;
}
/**
* 将颜色值转换为Minecraft颜色代码
* 支持: #RRGGBB 十六进制, 或颜色名称
*/
private String convertToMcColor(String color) {
if (color == null || color.isEmpty()) return "";
// 处理 rgb/rgba 颜色函数
String lowerColor = color.toLowerCase();
if (lowerColor.startsWith("rgb(") || lowerColor.startsWith("rgba(")) {
try {
// 提取括号内的内容
int start = color.indexOf('(') + 1;
int end = color.lastIndexOf(')');
if (start > 0 && end > start) {
String content = color.substring(start, end);
String[] parts = content.split(",");
if (parts.length >= 3) {
int r = Integer.parseInt(parts[0].trim());
int g = Integer.parseInt(parts[1].trim());
int b = Integer.parseInt(parts[2].trim());
// 转换为十六进制
String hex = String.format("#%02x%02x%02x", r, g, b);
// 递归调用,使用十六进制逻辑
return convertToMcColor(hex);
}
}
} catch (Exception e) {
// 解析失败,返回默认白色
return "§f";
}
}
// 处理 hsl/hsla 颜色函数(简单映射到最接近的Minecraft颜色)
if (lowerColor.startsWith("hsl(") || lowerColor.startsWith("hsla(")) {
// 由于HSL解析复杂,暂时返回默认白色
return "§f";
}
// 十六进制颜色
if (color.startsWith("#")) {
String hex = color.substring(1).toLowerCase();
if (hex.equals("ff5555") || hex.equals("ff0000")) return "§c";
if (hex.equals("55ff55") || hex.equals("00ff00")) return "§a";
if (hex.equals("5555ff") || hex.equals("0000ff")) return "§9";
if (hex.equals("ffff55") || hex.equals("ffff00")) return "§e";
if (hex.equals("ffffff")) return "§f";
if (hex.equals("000000")) return "§0";
if (hex.equals("a0a0a0") || hex.equals("808080")) return "§7";
if (hex.equals("ffaa00")) return "§6";
if (hex.equals("55ffff")) return "§b";
if (hex.equals("ff55ff")) return "§d";
if (hex.equals("aa0000")) return "§4";
if (hex.equals("0000aa")) return "§1";
if (hex.equals("00aa00")) return "§2";
if (hex.equals("00aaaa")) return "§3";
if (hex.equals("aa00aa")) return "§5";
if (hex.equals("555555")) return "§8";
return "§f"; // 默认白色
}
// 颜色名称
String name = color.toLowerCase().replace("_", "");
if (name.equals("red") || name.equals("darkred") || name.equals("dark_red") || name.equals("crimson") || name.equals("salmon") || name.equals("tomato") || name.equals("coral")) return "§c";
if (name.equals("green") || name.equals("darkgreen") || name.equals("dark_green") || name.equals("lime") || name.equals("chartreuse") || name.equals("olive") || name.equals("olivegreen")) return "§a";
if (name.equals("blue") || name.equals("darkblue") || name.equals("dark_blue") || name.equals("darkaqua") || name.equals("navy") || name.equals("indigo")) return "§9";
if (name.equals("yellow") || name.equals("gold") || name.equals("goldenrod")) return "§e";
if (name.equals("white") || name.equals("silver") || name.equals("snow") || name.equals("ivory")) return "§f";
if (name.equals("black") || name.equals("onyx") || name.equals("jetblack")) return "§0";
if (name.equals("gray") || name.equals("grey") || name.equals("darkgray") || name.equals("darkgrey") || name.equals("dimgray") || name.equals("dimgrey")) return "§7";
if (name.equals("aqua") || name.equals("lightblue") || name.equals("turquoise") || name.equals("teal")) return "§b";
if (name.equals("lightpurple") || name.equals("purple") || name.equals("magenta") || name.equals("plum") || name.equals("orchid") || name.equals("violet")) return "§d";
if (name.equals("darkred")) return "§4";
if (name.equals("darkblue")) return "§1";
if (name.equals("darkgreen")) return "§2";
if (name.equals("darkaqua") || name.equals("cyan")) return "§3";
if (name.equals("darkpurple")) return "§5";
if (name.equals("darkgray") || name.equals("darkgrey")) return "§8";
if (name.equals("lightgray") || name.equals("lightgrey")) return "§7";
if (name.equals("orange") || name.equals("orangered")) return "§6";
if (name.equals("pink") || name.equals("hotpink") || name.equals("lightpink")) return "§d";
if (name.equals("brown") || name.equals("saddlebrown") || name.equals("sienna") || name.equals("chocolate")) return "§4";
return "§f"; // 默认白色
}
// ===== 占位符替换 =====
private String replacePlaceholders(String msg, String player, int used, int limit, String time) {
if (msg == null || msg.isEmpty()) return msg;
// 支持中英文占位符,任意括号
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]玩家[(\\(\\)\\[\\]\\{\\}【】)]", player);
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]player[(\\(\\)\\[\\]\\{\\}【】)]", player);
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已出售金额[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]used[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已使用额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]剩余额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]limit[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]下次重置时间[(\\(\\)\\[\\]\\{\\}【】)]", time);
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]time[(\\(\\)\\[\\]\\{\\}【】)]", time);
//同时支持%player%等格式
msg = msg.replace("%player%", player);
msg = msg.replace("%used%", String.valueOf(used));
msg = msg.replace("%limit%", String.valueOf(limit));
msg = msg.replace("%time%", time);
return msg;
}
public boolean sellItem(Player p, ShopItem item,
int amount) {
if (item == null) return false;
@@ -1565,6 +1801,31 @@ public class ShopManager implements Listener {
return false;
}
// ★ 出售限额检查
if (maxSellLimit != -1) {
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
int remaining = maxSellLimit - todaySold;
int saleAmount = item.getEffectiveSellPrice() * amount;
if (saleAmount > remaining) {
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
String msg = decodeHtml(maxSellMessage);
// 计算下次重置时间(最早交易 + 1小时)
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
String resetTime;
if (earliestTime > 0) {
resetTime = new java.text.SimpleDateFormat("HH:mm")
.format(new java.util.Date(earliestTime + 3600000L));
} else {
resetTime = "现在";
}
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
p.sendMessage(msg);
}
// 提示消息关闭时不打印任何消息
return false;
}
}
if (!p.getInventory().containsAtLeast(
getShopStack(item, 1), amount)) {
p.sendMessage("§c你没有足够的 §e"
@@ -1782,12 +2043,60 @@ public class ShopManager implements Listener {
if (sub.equals("reload")) {
loadCategories();
loadMaxSellConfig();
s.sendMessage("§a商店已重载,共 "
+ categories.size()
+ " 个分类");
return true;
}
if (sub.equals("setmax")) {
if (a.length < 3) {
s.sendMessage(
"§e用法: shop setmax <金额(-1无限)>");
return true;
}
try {
int limit = Integer.parseInt(a[2]);
maxSellLimit = limit;
saveMaxSellConfig();
if (limit == -1) {
s.sendMessage("§a出售限额已设为无限");
} else {
s.sendMessage("§a出售限额已设为 §e" + limit + " §a枚债券/小时");
}
} catch (NumberFormatException e) {
s.sendMessage("§c无效数字: " + a[2]);
}
return true;
}
if (sub.equals("maxmsg")) {
if (a.length < 3) {
s.sendMessage(
"§e用法: shop maxmsg <内容/关闭/默认>");
return true;
}
// 拼接剩余参数作为消息内容
String msg = String.join(" ", Arrays.copyOfRange(a, 2, a.length));
if (msg.equalsIgnoreCase("关闭") || msg.equalsIgnoreCase("off")) {
maxSellMessageEnabled = false;
saveMaxSellConfig();
s.sendMessage("§a出售限额提示已关闭");
} else if (msg.equalsIgnoreCase("默认") || msg.equalsIgnoreCase("default")) {
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
maxSellMessageEnabled = true;
saveMaxSellConfig();
s.sendMessage("§a出售限额提示已设为默认内容");
} else {
maxSellMessage = msg;
maxSellMessageEnabled = true;
saveMaxSellConfig();
s.sendMessage("§a出售限额提示已设置");
}
return true;
}
if (sub.equals("add")) {
if (a.length < 7) {
s.sendMessage(
@@ -2527,6 +2836,30 @@ public class ShopManager implements Listener {
count++;
}
}
// ★ 出售限额检查
if (maxSellLimit != -1 && total > 0) {
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
int remaining = maxSellLimit - todaySold;
int saleAmount = item.getEffectiveSellPrice() * total;
if (saleAmount > remaining) {
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
String msg = decodeHtml(maxSellMessage);
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
String resetTime;
if (earliestTime > 0) {
resetTime = new java.text.SimpleDateFormat("HH:mm")
.format(new java.util.Date(earliestTime + 3600000L));
} else {
resetTime = "现在";
}
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
p.sendMessage(msg);
}
// 提示消息关闭时不打印任何消息
return;
}
}
if (total <= 0) {
p.sendMessage("§c你没有可出售的 §e"
+ item.getDisplayName());
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
+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
+49 -7
View File
@@ -1,5 +1,5 @@
name: Sdf1_login
version: 2.5
version: 2.6
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,14 @@ commands:
description: 打开商店
usage: /商店
# testorder:
# description: 测试订单中心
# usage: /testorder
# description: 测试订单中心
# usage: /testorder
protect:
description: 区域保护
usage: /protect
传送:
description: 传送至领地
usage: /传送 <领地名>
update:
description: 检查所有插件更新(集群控制)
usage: /update
@@ -110,4 +113,43 @@ commands:
usage: /web
控制台:
description: 生成Web端登录令牌(中文版)
usage: /控制台
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