Compare commits

...
205 Commits
Author SHA1 Message Date
youpaishidifu d9f7f2de71 版号切回3.0(正式版):3.0收尾,补AGPL-3.0协议后同步公开三仓
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m9s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-09 11:33:02 +08:00
youpaishidifu 84e6d3bbc0 添加 AGPL-3.0 开源协议:LICENSE 为 gnu.org 官方原文,README 补充协议说明与网络服务开源义务 2026-10-09 11:29:11 +08:00
youpaishidifu 34a344f39c 版号切回3.01(内测版):3.0正式版已圆满收官
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m10s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-09 00:10:51 +08:00
youpaishidifu 84d351b116 未登录白名单加白:正版验证mslogin/正版、找回密码、web/控制台(登录链路相关命令放行)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m3s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 23:27:59 +08:00
youpaishidifu ad11a2bcd3 收紧未登录命令白名单:仅放行 login/l/reg/sdf1_login reset 及中文登录注册
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-08 22:48:39 +08:00
youpaishidifu dea0370084 修复email未登录越权漏洞:从未登录放行名单移除email/绑定/绑定邮箱,三个绑定入口加登录硬校验,版号切回3.0
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m23s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 22:18:17 +08:00
youpaishidifu d6af2f69e0 版号切 3.01(内测版)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m17s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-08 20:40:47 +08:00
youpaishidifu 70f6112ad8 版号切 3.0(正式版)
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-08 20:16:18 +08:00
youpaishidifu cbfdfb016b 三库归位登录凭证/用户组/Php,处罚预告按规则档位修正,解禁与重置不再动人机验证
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
1) login.db 创建即入 登录凭证/、usergroup.db 入 用户组/、web_sync.db 入 Php/(WebManager 5处统一走 webSyncDbFile()),根目录存量由 migrateRootDataFiles 启动最早期整组迁移(.db+-shm+-wal 同进同退,失败整组回滚);2) nextPunishHint 改为按 applyPunish同款判据(violation>=阈值取最高档)计算下一次违规真正命中的档位与配置时长,修复旧实现取 k>违规数 导致第3次违规就预告第6档15分钟、预告与实际处罚长期对不上的问题;3) unmutePlayer/resetPlayer 移除 markReverify 调用,解禁与重置发言违规不再重置人机验证(resetPlayer 顺带清 DB muted_until,避免重置后重启禁言复活)
2026-10-08 18:57:51 +08:00
youpaishidifu 47301edaed 归位迁移改为sqlite三件套原子搬移,失败整组回滚
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
quest_data.db/bond.db/garbage.db/pvp.db 都是 WAL 模式 sqlite,运行时
-shm/-wal 与主库必须同进同退:单搬主库会丢未checkpoint事务导致数据回退,
单搬wal缺主库直接损坏。原实现逐文件rename,崩在中间会留下破损三件套。

改为每个 db 一组,主库/-shm/-wal 整体搬,任一失败立即把已搬的搬回原位;
目标端已存在则整组跳过不合并。创建时路径直接指向子目录(QuestStorage/
PlayerStorage 已改),迁移只兜存量。
2026-10-08 17:49:02 +08:00
youpaishidifu 0ab7c7052c 修复违规计数文案歧义、验证码重验回归、quest_data.db归位
1. 违规计数订正:实际计数一直是1→2→3(incrementViolation是+1),截图显示的
   1/3/6三角数是文案歧义——nextPunishHint返回的第N次违规预告与命中提示的第N次
   违规两行并列,被读成跳号。现改为预告只报处罚档位不报次数,命中提示改为
   「本次为第N次违规」,次数全局只出现一次。

2. 验证码回归修复:isMuted()禁言到期自动解禁时调用markReverify(),把已通过
   人机验证的玩家踢出verifiedPlayers,导致「验证码通过→被禁言→到期解禁→又要
   重验」的死循环。按用户裁定改为:到期自动解禁不再吊销验证状态,仅显式
   /chat unmute 与 /chat reset 才吊销。

3. quest_data.db 归位任务面板/:QuestStorage与PlayerStorage两处硬编码路径
   改为子目录并自动建目录,migrateRootDataFiles() 增加 db/-shm/-wal 三件套
   幂等迁移(启动最早期执行)。
2026-10-08 17:44:15 +08:00
youpaishidifu 19e03c5fb4 修复三bug并归位根目录5类文件:①禁言解除后强制重新人机验证(解禁≠解除人机验证,到期/命令解禁/重置均打reverify标记,答对清除) ②OP白名单过期条目不再补授(修复26-10-08到期后每分钟撤销↔授予无限横跳) ③敏感词命中改为明确反馈(控制台日志+玩家提示+下一档处罚预告,修复启用过滤配置解析失效) ④bond.db/chat.txt/garbage.db/maxsell_config.txt/pvp.db启动最早期自动迁入对应子文件夹
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m31s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 17:02:02 +08:00
youpaishidifu 58f9a756e9 调整.gitignore拉黑规则
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m46s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-08 16:20:58 +08:00
youpaishidifu bdd55cc9bf 将opwl.json移入resources目录,随jar一起释放。移除根目录的opwl.json暂存
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m10s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-08 16:15:06 +08:00
youpaishidifu 3217e53cda 补充opwl.json使用说明;注释停用/menu命令注册
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m27s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-08 15:41:49 +08:00
youpaishidifu 881fb85408 修复三处bug:时间解析支持26-10-08 14:43短横线格式、禁言期间屏蔽一切私信收发、URL拦截baidu . com空格变形绕过 2026-10-08 15:41:46 +08:00
youpaishidifu 581475e760 opwl事故修复三连: ①ops.json真实字段是uuid(旧代码只认id致复原全靠猜名字) ②复原的OP自动写回白名单(否则同一轮巡检把刚复原的11人又当非法持有全撤+报警) ③配置丢失/解码失败自动隔离坏文件(改名不删)+从jar释放默认配置+复原OP备份(热重载与/opwl reload同生效) ④补授从仅在线扩展到在线+离线(白名单11人但ops.json为空的根因)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m7s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-08 14:01:34 +08:00
youpaishidifu 32fe81f908 补单提速第二轮+控台指令最小化信息透露
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
问题1 补单10秒才抓到单(根因两条):
- 线程错位: TimerA-周期批处理跑在sdf1-db-worker上, triggerFastPoller在该线程同步doGet,
  撞sendHard的10秒硬超时 -> 每次补单固定10秒且返回null, 还吃光DB队列整轮预算;
- 节奏不符设计: 定时器是固定5秒而非0~5秒随机, 4秒限流又把随机节奏里<4秒的轮次整轮吞掉。
修法: 定时器改0~100tick随机(nextPollerDelayTicks); 限流4000ms->500ms;
HTTP挪到补单专用单线程pollerExecutor(fire-and-forget, 队列容量1+丢弃);
补单改8秒短超时POLLER_TIMEOUT_S; 本轮processed>0立刻pullPendingTransactions入账;
shutdown补关pollerExecutor。

问题2 conmsg/opwl对玩家泄露"仅控制台可用/需要控台权限":
从plugin.yml摘掉注册 -> 玩家敲它必然落进UnknownCommandEvent, 与"不存在的指令"
走完全相同路径(压制原生unknown报错+相似建议候选里永远没有这俩);
控制台由onUnknownCommand里的dispatchConsoleOnlyCommand就地接管执行,
onCommand里的opwl/conmsg分支改为兜底且玩家侧一律静默。
2026-10-08 12:45:36 +08:00
youpaishidifu 31f2a60096 opwl扫描点订正: 心跳日志改为无更新就闭嘴(仅重载发生或统计快照变化才打印) + 修正扫描周期9000tick(实为7.5分钟)→18000tick真正15分钟,消除非整点多余冒泡
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m27s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 11:16:16 +08:00
youpaishidifu cd0a45d284 opwl数据安全+补单提速: 解析失败卸载OP前先备份原版ops.json(备份失败则不卸载,恢复时复原,pickJson排除ops.json*) + 补单触发限流15s→4s并新增独立5秒定时器(不受锁库退避/队列积压影响)
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-08 11:04:53 +08:00
youpaishidifu e2447f0b95 opwl第四轮: 修临时OP自动续期(存exp:绝对到期时间)+上下任提示当事人(在线直发/离线补发)+/op按启用状态分支(true全拦/false仅注册校验)+扫描点心跳日志+新增opwl check子命令
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m59s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 00:45:30 +08:00
youpaishidifu ca1fe6f252 opwl第二轮: 整点扫描(:15/:30/:45)替代纯15秒应用 + 15秒文件热重载保留 + 控制台/op拦截引导opwl add + add/addtime注册检查(非注册引导改json强制授权)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-08 00:18:21 +08:00
youpaishidifu 84285e5009 opwl改回独立目录OP白名单/(目录隔离,任意json都当配置;模板resources/OP白名单/opwl.json)
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m15s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-07 23:58:12 +08:00
youpaishidifu a39e26caf7 opwl接管原生OP名单:加载时导入原生OP,非法持有一律撤销+报警(新增非法持有报警信息模板,支持user/time变量中英文),改为单文件opwl.json
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 23:05:29 +08:00
youpaishidifu d19ecc5ce0 新增OP白名单功能(opwl):读OP白名单目录json授OP,读失败一次性卸载全部OP,15秒增量热重载,命令仅控台
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 22:09:01 +08:00
youpaishidifu e745010220 修复pushShopCatalog对账后回退本地hash翻案:对账needPush=false直接回,不再整包推975项
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m1s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 18:02:31 +08:00
youpaishidifu a580fbbcb7 领地/商城目录接入alignGate差集对账,重启不再整包推,多退少补
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m18s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 17:32:15 +08:00
youpaishidifu dfdb951c8a 修复bans收回死循环:已过期临时封禁不再拉回Bukkit,避免每轮extra=1无限收回1条
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m8s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 16:21:40 +08:00
youpaishidifu f3a987dde9 修复setField对null写成字面量null导致credentials每轮恒定补推1:改setNull并启动自愈清理存量脏数据
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m17s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-07 13:47:56 +08:00
youpaishidifu ed558517ba 修复credentials对账恒changed全量:凭证行补指纹键password_salt/temp_password,推送改对账差集不再整包硬推611条
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m56s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-07 12:07:25 +08:00
youpaishidifu 9f8a881d62 IP同步改为只推对账差集,删除全量硬推;补上users/bonds/shop三处补推守卫
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m15s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
- syncAllPlayerIps 去掉 forceFullIpPush:以前只要对账发现缺口就跳过 ipCache
  过滤、把本机全部IP整包推给PHP(生产实测 missing=0/changed=110 却推了609条),
  现在只推 alignGate 裁决出的 pushKeys(missing + 本机改过的 changed);
  对账没做成时本轮不推不收,绝不回退全量硬推。
- 日志改为「对账补推 N/M 个玩家IP(PHP 侧共 X 条)」,与实际发送量一致。
- shop/bonds/users 三处补上 alignNeedsPush 守卫:PHP 库被清空但本地数据没变时,
  本地 hash 会永久短路补推(与 10-06 修过的 admins/providers/bans/credentials 同款 bug)。
2026-10-07 11:17:09 +08:00
youpaishidifu d0d3b48000 临时屏蔽一下没做完的功能和配置文件
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m1s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-07 00:21:41 +08:00
youpaishidifu d8d8c18d53 修复配置类对账推送在404后永久卡死:5处hash改为推送成功后才提交
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
灾备演练(删PHP库+404+空白库重建)实测发现:
- admins/providers/credentials/bans/lands 5处 lastXxxHash 在HTTP推送前就提交
- 404期间唯一一次推送失败,hash却已写脏
- 之后 alignGate 每轮判「PHP缺N条要补推」,全被本地hash短路挡回
- 表现为日志反复打「补推N」却永远发不出去,PHP端表永久0行
  (web_plugin_admins/web_service_providers/weblogin_credentials/web_player_bans/web_area_lands)

修复:
1. 对账明确 needPush 时本地hash不得拦截(alignNeedsPush 绕过短路)
2. hash 一律改为 PHP 明确返回 success=true 后才提交
   与 syncUserRegistrations(2026-10-04)/syncShopData/syncBonds 的既有规矩对齐

交易流水多退少补在本次演练中表现正常:空白库→443/443全量补推成功
2026-10-06 23:00:27 +08:00
youpaishidifu c118e8e708 周期对账通过时也打印两边笔数,让对齐结论随时可验证
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m17s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-06 21:50:01 +08:00
youpaishidifu 07d0a5a192 交易流水补运行期周期对账,修复PHP库被重载清空后的假对齐
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m34s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-06 21:25:13 +08:00
youpaishidifu 9c2d5db41e 充值回执对账排进首次全量同步队列:无人在线时启动即点差集,不再只依赖玩家上线的周期批次
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-06 19:58:08 +08:00
youpaishidifu 64cb501321 充值回执一式两份对账:Java持web_tx_receipts镜像,随全量同步多退少补热回补PHP缺失流水
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m17s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-06 18:18:00 +08:00
youpaishidifu e5e460ce20 WebManager:超时日志节流降噪、连接4秒判死换连接重试、对账无答复一律不推不收、system/sync token软硬双到期异步续期
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m23s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-06 15:14:29 +08:00
youpaishidifu 19aecebc90 WebManager:修正DB队列硬超时线程标记错位,对账硬超时不回退全量硬推,加队列节奏与商品目录hash静默
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-06 13:12:15 +08:00
youpaishidifu 53cadf2282 WebManager:DB队列HTTP改10秒硬超时并移交旁路线程放行,任务加10秒预算防堵门
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m5s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-06 12:06:31 +08:00
youpaishidifu ec4bd415d2 双向对账改造:全量/周期同步与PHP指纹对账多退少补,null整数列归一防每轮changed
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m21s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-05 22:29:50 +08:00
youpaishidifu 6a2bfaf1ff 修复开关关闭时插件设置.txt热重载永不触发:start()不再因未启用而跳过启动合并定时器
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-05 18:58:23 +08:00
youpaishidifu 5d89ff54c9 修复交易推流死等:批调度毫秒误当tick(15秒被排成12.5分钟);改单次15秒必答复三态日志;轮次卡死自愈不需手动reload;双向对账多退少补(PHP缺的补推、PHP多的收回导入本地)
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m10s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-05 17:46:34 +08:00
youpaishidifu ebb17d69bf 交易推流加15秒回执窗口:收到PHP回执才推下一批,未收到则等15秒重试同一批(带20次上限),批调度纳入锁库退避
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m30s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-05 17:00:25 +08:00
youpaishidifu 478aa97ca8 修复两处同步缺陷:parseJson漏嵌套对象分支致流水对齐恒判PHP无响应;热重载/reload后排全量同步
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m36s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-05 15:44:02 +08:00
youpaishidifu 88db479ede 交易流水同步改为节流+两边对账:开服先点验,一致则跳过补推;逐批只补PHP缺失序列号;推进异步化不阻塞DB队列
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m13s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-04 23:52:17 +08:00
youpaishidifu 2822ba9c9a 负面效果从增益里拆出独立子菜单 + 药水成就防作弊
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m15s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
用户要求:增益只归纳增益,负面另开子菜单;移除指定效果那套不动。
插件给的效果必须打标记,靠插件凑齐原版药水成就的立刻撤销。

效果拆分
- AreaConfig 新增 badEffects 字段,area_lands 加 bad_effects 列(73 列)
- GUI:效果管理主菜单 11~15 连排五格(清负面/禁效果/单清/增益/负面),
  新增 subPage 4 负面效果列表;选择页按 clear/give/bad 分别只列对应档位
- CLI:新增 effectsmgmt 6/7 子页与 effectsbadadd/effectsbadremove/effectsbadedit
  指令、editBad_ 回写分支,与 GUI 同一份效果口径
- 施加点全部合流 giveRegionEffects/applyEffectList,并修正 handleEnter
  的先清后给顺序(否则负面配置会被 clearBadEffects 清掉)
- 数量上限改为增益+负面合计(用户组 max_effects 是同一份预算)

成就防作弊
- 新增 AchievementEffectGuard:接管 nether/all_potions(17种) 与
  nether/all_effects(34种),清单抄自服务端 advancement json
- EntityPotionEffectEvent(Cause.PLUGIN) + PlayerAdvancementCriterionGrantEvent
  双钩子,revokeCriteria 撤销,进服复核一次,离场清标记
2026-10-04 22:57:14 +08:00
youpaishidifu 0c18137401 修正药效生存上限表:村庄英雄5级、挖掘疲劳3级、伤害吸收4级、不祥类5级
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m21s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-04 20:32:03 +08:00
youpaishidifu b64e7e336c 修正药效等级上限口径:按各效果的生存可获取上限封顶
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m15s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
用户纠正:上一版把 /effect 命令的作弊上限(amplifier 255 = 强度 256)
当成了「原版最大上限」,口径错了。真实含义是**生存模式下正常探索、
合成、建造能拿到的最大等级**:
  · 抗性提升 —— 信标最高 2 级(4级金字塔选「提升至II」)
  · 海豚的恩惠 —— 只有潮涌能量块一个来源,最高 1 级(截图里的 Lv255 即此)
所以「统一按 256 封顶」仍然等于没封。

改为按效果逐个封顶,两层上限同时生效:
  1) 生存可获取上限(硬顶):getSurvivalMaxLevel() 逐项查表
     · 可升 II 的:迅捷/急迫/抗性提升/跳跃提升/力量(信标)、
       瞬间治疗/瞬间伤害/缓慢/挖掘疲劳/中毒/生命恢复(发光石粉)
     · 生存单一来源恒为 1:海豚的恩惠、潮涌能量、抗火、水下呼吸、
       隐身、夜视、缓降、虚弱、反胃、饥饿、凋零、失明、黑暗、
       发光、飘浮、各类事件效果等
     · 生存无来源:生命提升/伤害吸收/饱和/幸运/霉运 → 1
     · 未知效果兜底 1(宁可少给,绝不超发)
  2) 用户组配置 max_effect_level(软顶):组里配多少就是多少
  最终 cap = min(两者),getEffectLevelCap() 统一出口

表项与 paper-api 26.1.2 的 39 个 PotionEffectType 完全对齐(已核对无遗漏)。
注意:生命恢复取 2 而非旧版附魔金苹果的 V —— 那是 1.6.1 前的历史数据,
      附魔金苹果早已改为给伤害吸收。

应用点全部改走 clampEffectLevel(raw, effectName, owner)(5 处):
首次给予、热更新、handleEnter、applyEffects、PHP 推送字段。
CLI 添加/编辑的报错信息改为报该效果的真实上限,并说明生存上限是多少。

PHP 侧 land_api.php 同步加入同一张表(134 个 case 标签与 Java 端
逐项对账一致),web 端写入时即按效果封顶,不再等 Java 兜底。
2026-10-04 19:56:45 +08:00
youpaishidifu 6f9202859d 修复领地增益药效等级无上限,新增用户组药效等级上限配置
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m32s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
问题:领地增益效果的等级完全没校验,管理员配了 255 级就会原样
生效(截图里出现的 Lv255 即源于此)。原版 /effect 的 amplifier 硬
上限是 255(对应药效强度 1~256),插件此前允许写入并施加远超原版
范围的等级。

修复(2026-10-04 任务4):
- UserGroupManager 新增 maxEffectLevel 字段,默认 256
  · 常量 DEFAULT_MAX_EFFECT_LEVEL = 256 集中定义原版上限
  · user_group_config 建表补列 + ALTER 迁移(老库自动升级)
  · loadGroupConfigs 读取、saveGroupConfigToDB 加为第 14 列
  · 新增 getPlayerMaxEffectLevel:普通玩家走原版 256,用户组按组配置
- AreaProtection 新增 getMaxGiveEffectLevel / clampGiveEffectLevel,
  在全部 4 个应用点封顶:首次给予(applyRegionEffects)、热更新、
  handleEnter、applyEffects;另在 PHP 字段推送(applyFieldToAreaConfig)
  与 CLI 添加/编辑两处入口按同一上限做输入校验与提示
- WebManager 用户组同步双向补 max_effect_level(推 PHP + 回读)

规则:普通玩家(未加入任何用户组)严格按原版 256 封顶,严禁超限;
用户组玩家按组配置封顶,组里配多少就是多少,但任何情况下都不得
突破原版 256。原先散落各处的硬编码 255 已统一为该口径。
2026-10-04 19:25:39 +08:00
youpaishidifu f2808a5923 修复 denyMove 对访客/路人失效:GUI显示已禁用但实际畅通无阻
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
现象:领地访客授权页显示「移动 已禁用」,玩家在领地内却能自由走动。

根因:getEffectiveDeny 是所有 deny* 权限的共用入口,其第3步
    if (level == PermissionLevel.VISITOR) return false;
  对全部权限无条件豁免。denyMove 走同一入口,访客在此被放行,
  永远到不了第4步读 ac.denyMove。而 GUI(AreaGUIManager:586)
  显示读的正是 land.denyMove=true —— 显示与实际判定脱节。

判定链(修复前,访客):
  第1步 非ADMIN/OWNER → 不豁免
  第2步 playerPerms 无 denyMove 键 → containsKey=false,不命中
  第3步 level==VISITOR → return false ← 绕过点
  第4步 getLandDefaultDeny(ac,"denyMove")  永远到不了

修复(按用户决策:仅 denyMove 改,其余权限保持现状):
  第3步增加 denyMove 特例 —— 访客若无 per-player 显式 denyMove,
  不再豁免,继续落到第4步按领地默认 denyMove 判定。
  level==null(纯路人)本就落在第4步,行为正确,未改动。

顺带恢复 logDenyExempt 诊断输出(此前整段被注释,
是当初现场无法定位该问题的直接原因)。

验证:离线回归测试 13/13 通过,覆盖 ADMIN/OWNER 豁免、
per-player 显式放行与禁止、访客回落、路人回落、白名单访客,
并断言 denyBlockBreak/denyContainer/denyBlockPlace/denyWax
的访客豁免语义未被破坏。mvn -o clean package 编译通过。
2026-10-04 18:47:05 +08:00
youpaishidifu 494ab22227 修复首次全量同步的三处根因(假成功+流水永久丢失+同步过慢)
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m58s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
【BUG-1 假成功(核心)】hash 在请求发出前就提交,导致同步失败后永不重试
  位置:syncUserRegistrations / syncBondBalances / syncShopData
  现象:日志打印"已同步 3人",PHP 端 users 表实测 0 条
  原因:lastXxxHash = currentHash 写在 httpPostWithToken 之前,密钥错/网络断/500
        时 hash 已被写脏,这份数据再也不会重推
  修复:hash 仅在 PHP 明确返回 success=true 后提交;失败不提交,下轮周期自动重试

【BUG-2 历史流水永久丢失(不可恢复)】
  实测:bond.db 419 笔(最早 2026-05-21),web_sync.db 水位线已推到 2026-10-04
        17:18:02,419 笔全部 <= 水位线,晚于水位 0 笔;PHP 端 game_transactions 仅 3 笔
  原因:水位线语义是"我以为 PHP 已经有了"。Web 端被清空(全新部署/隔离目录重置/
        低版本插件首升 PHP)后两者失联且无法自愈,只推了水位线之后的 1 笔
  修复:
   1) 新增 resetTxWatermarkForFullSync(),首次全量同步前强制归零水位线重推全部历史
   2) 新增 pushTxBatch(),每批最多 300 笔(TX_SYNC_BATCH_SIZE),失败即中止且
      不推进水位线 → 下轮从同一位置重试,不丢数据
   3) 分批后打"第 N/M 批 已同步 X 笔"与总计日志

【BUG-3 全量同步串行 sleep 过长 + 无进度可见】
  原因:12 项任务之间各 sleep 6~14 秒,总计 72~168 秒串行阻塞(用户"等 3 分钟没数据")
  修复:间隔缩短为 1.5~3.5 秒;开头判 isEnabled() 跳过;开头打地址+密钥长度;
       每项提交打一行"已排队";结尾打 12 项汇总与耗时

兼容性:纯 CRLF 行尾已保持(0 裸 LF);mvn -o clean package 编译通过
2026-10-04 17:53:59 +08:00
youpaishidifu ba613ff953 修复:Web通信关闭后仍在出网(热重载暴露的可见性 bug)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
现象(用户日志实证):
  16:41:36 配置已热重载: 启用=false 密钥长度=30 [开关已关闭]
  16:41:45 syncOnlinePlayers: enabled=false, 跳过     ← 有守卫,正常
  16:42:12 推送游戏内商品目录 ... 已同步 975 项         ← ★ 无守卫,仍在出网
  16:42:46 服务商数据变更,已同步: 1人                 ← ★ 无守卫,仍在出网

根因两层:
1) 约 22 个同步方法(pushShopCatalog/syncServiceProviders/syncPermissions/
   pullShopStock/pullShopPrices/pullBondChanges 等)压根没有 !enabled 守卫;
2) 即使有守卫也拦不住 —— 任务是在「开关还是 true 时」就已 submitDbTask 入队的,
   db-worker 单线程排队轮到它时开关早已关闭,方法开头的守卫已经执行完了。

修法:出网最后一公里统一加闸,三道防线
1) 7 个 HTTP 收口方法开头加 isEnabled() 闸。doGet 是 46 处调用的唯一总出口,
   一处改动覆盖全部 GET 路径,零遗漏;两个 HTTP 降级方法一并覆盖。
2) submitDbTask / submitWebTask 入队前拦一道(省掉入队开销)。
3) startDbWorker 执行 task.run() 之前再查一次(治标兜底,拦存量任务),
   静默 continue 不打 WARN —— 关闭后每轮几十个任务全打日志会刷爆控制台,
   真正的开关变化已由 hotReloadIfChanged 打了一行「[开关已关闭]」,信息不丢。
另加 noteWebDisabledOnce():关闭状态下每 60 秒一条 INFO,说明「为什么没发」。

验证:mvn package 零错误;离线单测 18/18(闸的位置与结构均从真实源码抽取),
含端到端复现「关闭前入队、关闭后执行」这一用户日志场景。
2026-10-04 17:01:21 +08:00
youpaishidifu 932623ed88 Web通信配置热重载+密钥自愈:不再依赖 reload
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
背景:密钥错配导致 708 次 403。根因是运维改了服务器上的 插件设置.txt
却没重启也没 reload,插件一直握着旧密钥死循环重试。

改动(WebManager.java,+233/-3):
1. hotReloadIfChanged():5 个合并定时器每轮开头比对 插件设置.txt 指纹
   (lastModified+length),变了就热重载「启用开关/地址/密钥/三个数值项」。
   放在 enabled 守卫【之前】,开关关着时也能感知「改回 true」。
   3 秒节流 + 指纹闸,文件没动时连磁盘都不读。
2. 密钥自愈:doGet / doPostWithoutFallback / doPostWithSslFallback /
   doPostWithRetry 四个出网收口接入 withSecretAutoHeal(),
   命中「密钥验证失败/认证失败/invalid_secret」时重读配置的密钥并重试一次。
   30 秒冷却防刷日志;文件里就是当前值则不自愈(那是 PHP 侧问题,不死循环)。
3. isEnabled() 改为实时读文件,改开关下一毫秒生效。
4. reloadWebConfig() 保留为手动方式,强制清文件指纹确保真正重新解析。
5. 日志不再打印明文密钥,只打长度。

验证:mvn package 零错误;离线单测 32/32(判定串与解析规则均从源码抽取,
非另写等价实现),含端到端自愈、PHP 真拒不死循环、开关自动恢复、指纹节流。
2026-10-04 16:32:07 +08:00
youpaishidifu dc5633c740 新增4项功能:tempban参数可选+债券冻结时长+领地增益上限+用户组max_effects同步
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m52s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-04 14:48:26 +08:00
youpaishidifu 24897c2b97 命令语法报错补充标准示例与一句话用法,示例仅对控制台与OP玩家展示
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m5s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-04 13:06:22 +08:00
youpaishidifu ab0f15f248 新增命令参数不全/语法错误的中文友好报错(Brigadier 重解析 + 出错指针 + 下一步可填提示)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m39s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-04 12:43:38 +08:00
youpaishidifu 77635b8db2 保留原生JSON解析:控制台未命中分流增加JSON快速通道,与&色码写法并存
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m9s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-04 11:40:36 +08:00
youpaishidifu 8486adb53c 修复:控制台未命中原生指令被当聊天全服广播导致刷屏
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-04 11:19:07 +08:00
youpaishidifu c07c97b7ea conmsg: clear 作为保留子参数,恢复默认署名(不再当名字设置出去)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m22s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 23:26:51 +08:00
youpaishidifu c17c606ff2 转发聊天: 正文支持JSON组件+&色码+换行并存, 署名仅&色码且强制单行
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m39s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 23:06:28 +08:00
youpaishidifu f8756a24df 无感切换: 新增 conmsg 控制台署名 + 按权限分层提示 + 找不到时温和告知
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 22:17:37 +08:00
youpaishidifu be8d0cb615 无感切换分流: 玩家未命中出相似建议, 控制台未命中转发聊天
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m58s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 21:31:49 +08:00
youpaishidifu 2e0fdea5be 无感切换修复控制台接管 + 压制方式改为 message(null)
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m47s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 20:48:01 +08:00
youpaishidifu 21e23bbc89 无感切换限定为 sdf1 系列 4 插件,未命中时回指令清单
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m21s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
按用户口径收窄管辖范围:
- 只接管 CY_beibao / sdf1 / Sdf1_game / Sdf1_login 四个插件的指令
- 命中我们 4 插件的指令(大小写不敏感)-> 直接执行
- 命中别人插件或原版指令 -> 完全不干预,不破坏别人的判断链
- 确实啥都没命中 -> 压制原生 unknown,把我们 4 插件的指令清单分组回给玩家手动执行
- 控制台不接管(运维输错保留原声报错)

技术要点(Paper 26.2 实测签名):
- 拦截点 UnknownCommandEvent.setMessage(String)(父类非 Cancellable)
- 归属判定靠 PluginCommand.getPlugin(),名单 OWN_PLUGIN_NAMES
- 遍历用 CommandMap.getKnownCommands() 返回 Map<String,Command>
2026-10-03 19:39:05 +08:00
youpaishidifu 1e15bfb523 无感切换改用 Paper 26.x 正确拦截点 + 编译目标切到 26.2
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m56s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
根因:Paper 26.x 命令分发改走 Brigadier,PlayerCommandPreprocessEvent
已 @Deprecated 且只在 ServerGamePacketListenerImpl 作为遗留兼容路径保留,
实测事件从未被调用(日志一行都打不出来),改优先级无效。

改动:
- pom.xml: paper-api 1.21.4-R0.1-SNAPSHOT -> 26.2.build.129-stable(与运行时一致)
- 删除失效的 onSeamlessCommandSwitch / getCommandMapSafe
- 新增 UnknownCommandEvent 监听:setMessage("") 压制原生 unknown
- 新增 dispatchCaseInsensitive:输入为大写时小写复核并补执行
- forwardAsChat 改用 26.2 真实签名 AsyncChatEvent + AbstractChatEvent.message()/viewers()
2026-10-03 19:01:53 +08:00
youpaishidifu a1007a19f9 无感切换: 使用 Paper API 获取 CommandMap + 诊断日志
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 1m36s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
- getCommandMapSafe 优先使用 Bukkit.getCommandMap() Paper API
- 添加详细诊断日志(事件触发、label、hit 结果)
- 便于排查大写指令返回 unknown 问题
2026-10-03 18:07:10 +08:00
youpaishidifu dd4bb7baba 修复无感切换优先级 + 改判系统兼容反向操作
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m14s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
- onSeamlessCommandSwitch: MONITOR -> LOWEST(在服务端处理命令前拦截)
- doTempBan: 新增反向改判(永久封禁可被 tempban 改判有期徒刑)
2026-10-03 16:49:19 +08:00
youpaishidifu 31c7077a64 屏蔽除了readme.md以外的md文件
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m59s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 16:12:00 +08:00
youpaishidifu 9656ff4e8f 再次删除脚本部署的凭据文件
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m46s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 15:56:02 +08:00
youpaishidifu 14cc943068 屏蔽部署所需要的凭据文件
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m5s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 15:54:07 +08:00
youpaishidifu 96aef279cf 批次B3异步化 + 任务48改判系统/注册校验 + 任务50无感切换
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m6s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
批次B3(混合型定时器拆分):
- Main: quest PlayTime 计时器 DB 下沉异步
- Main: 工单自动处理定时器改 runTaskTimerAsynchronously
- TicketManager: 新增 sync() 辅助,autoProcessCompleted/settleTicket/processDeleteAccount Bukkit 操作回主线程
- GarbageManager: doCleanup 两段式(DB删旧异步+收集回主线程),collectItems 落库异步
- SnManager: startAutoSweepTask 分片(每 tick 只清点 1 人)
- AreaProtection: sweepBannedOwnerLands 两阶段(Bukkit主线程判定 + DB/HTTP异步删除)

任务48(改判系统 + 注册校验):
- DatabaseManager: 新增 userExistsIgnoreCase(COLLATE NOCASE)
- Main: 新增 ensureRegistered 辅助,doTempBan/doBanIp 入口加注册校验
- Main: 新增 guardVanillaBan 守卫(HIGH优先级双事件),未注册拒绝+临时封禁改判永久

任务50(无感切换):
- Main: 新增 onSeamlessCommandSwitch(MONITOR),命中指令直接执行/大小写不敏感/未命中转公屏
- Main: 新增 getCommandMapSafe + forwardAsChat
2026-10-03 15:36:31 +08:00
youpaishidifu efe00d92af 圈地选区SN校验收紧为本人名下+有效状态;转移物品不再锁SN;管理员强制可处置非法SN
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-03 11:11:52 +08:00
youpaishidifu 643f2a81cf 领地扩建按创建单价扣费、收缩按比例退款、圈地棒SN归属校验;发射器/投掷器消耗SN永久锁定不支持注销
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m9s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-03 10:21:11 +08:00
youpaishidifu e13869923f 管理员GUI配置面板补单次扩建免审面积与免审边长上限两个配置项
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m51s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-02 19:45:06 +08:00
youpaishidifu 20052d660b 领地面积限制与现场审批流:单次扩建3500㎡/边长7000格超限转管理员审批,denyWax改回普通权限判定
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-10-02 19:31:46 +08:00
youpaishidifu 9cf9fd745c 修复垃圾站取回后SN lore丢失:取回改用DB原始存档还原,补挂SN行,背包满掉落防吞
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-02 11:45:01 +08:00
youpaishidifu 4d34606d8e SN脱离管控两档计时:进垃圾箱/落地10分钟即起算,12小时超时放行注销与补发
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m7s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-01 22:47:20 +08:00
youpaishidifu 55ec15bb5b SN以旧换新改判:名下已有登记直接强制收回,并新增开箱扫描回收箱内无SN物品
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m49s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 22:06:42 +08:00
youpaishidifu fbc0e94fc2 2FA解绑报警改发玩家本人邮箱;未绑定2FA时不再提示验证码错误
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m48s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-01 21:00:05 +08:00
youpaishidifu 81e8323d56 2FA增加控制台强制解绑(30秒冷静期可-c取消)与三场景解绑报警邮件,玩家侧最小化信息透露
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m0s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 20:15:44 +08:00
youpaishidifu 393ef114e7 IP风控:检查点1/2接入双验证,IP变更且已绑2FA强制密码+2FA两关
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m23s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-10-01 19:00:34 +08:00
youpaishidifu 7a5fb5f171 回退正版验证落库:改为仅本次运行期内免登录,写入不看玩家在线状态
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m2s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 18:04:19 +08:00
youpaishidifu 9884f79cf6 订正双验证触发点注释
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m23s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-10-01 17:43:48 +08:00
youpaishidifu b7f2d0da57 登录风控:IP变更强制密码+2FA顺序两关,正版验证标记永久落库
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 1m50s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-01 17:35:21 +08:00
youpaishidifu 82524dc9e9 删了最后一个测试文件
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m47s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 14:27:41 +08:00
youpaishidifu 66e28db552 删了一堆测试文件
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m48s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 14:26:34 +08:00
youpaishidifu 919382c377 2FA恢复代码与登录风控联控:绑定后发8组6位恢复代码到邮箱(库存SHA-256一次性消费),/2fa code统一绑定与验证端口,IP变更时提示完成二次验证,恢复代码验证即解绑;正版OAuth验证玩家豁免游戏内验证码;/2fa子命令tab补全与冻结白名单放行
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 13:38:29 +08:00
Test User 2c138b6608 test fix action queue 1790824042
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m23s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-10-01 11:07:22 +08:00
youpaishidifu 7a2eb8e5d8 docs: 修正2FA token的WAF结论注释(实为token查询参数名+≥32位hex被拦,非t=)
Check Source / check (push) Successful in 1s
Auto Release / build (push) Failing after 43s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 10:59:38 +08:00
youpaishidifu 451d088697 fix: 2FA二维码token回退为裸hex64,规避线上WAF拦截(v1_前缀与t=长hex均被CF层404)
Check Source / check (push) Successful in 2s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
Auto Release / build (push) Failing after 13m30s
2026-10-01 10:46:34 +08:00
youpaishidifu d56fcbd3cd 2FA二维码token加v1_前缀:线上WAF拦截「t=32位以上纯hex」形态,前缀破坏纯hex特征(实测可过)
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 44s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 10:37:21 +08:00
youpaishidifu 32aa2bab05 安全加固层B:玩家端2FA(TOTP)绑定——/2fa add生成二维码申请、/2fa code动态码完成绑定、/2fa remove+30秒聊天confirm解绑;Base32/TOTP本地实现,users表加4列,PHP只做二维码展示
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 48s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-10-01 10:16:38 +08:00
youpaishidifu e941365a41 安全加固层A:未绑定邮箱玩家登录/注册后聊天框+actionbar15秒提示(与保存密码提示错峰)
Check Source / check (push) Successful in 2s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
Auto Release / build (push) Failing after 10m47s
2026-10-01 09:55:50 +08:00
youpaishidifu 606728d09a SN管理:新增顶层办理签发命令 issue_new(按玩家ID+设备类型直发)
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m5s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-30 17:32:08 +08:00
youpaishidifu fe6f6d8f80 SN日志actionCn补映射:issue_admin办理签发、cancel_force强制注销
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m52s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-30 12:34:06 +08:00
youpaishidifu 0b77d1c211 SN办理签发入口(Java端):execCommand新增issue分支+doIssue方法——兜底复核旧SN已注销/销毁/补发了结与本地冷静期,玩家在线后登记新SN并发放,回执issue_admin日志
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m38s
Simple Test / test (push) Successful in 3s
Test Env / test (push) Successful in 1s
2026-09-30 12:13:57 +08:00
youpaishidifu 383639a1b5 SN强制注销跳过1小时冷静期,代办注销仍走三条件门槛与冷静期
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m44s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 23:57:30 +08:00
youpaishidifu 374cbdbc11 SN防刷:注销/报失/补办统一三条件门槛(不在本人身上、不在本人领地箱子、脱离自身管控超12小时),未达标统一回通用提示,管理端可见逐条判定;修正拾取事件 loc_type 误记为物品类型
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m3s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-29 20:21:45 +08:00
youpaishidifu 841fd928a5 fix: SN命令回执改同轮返回且失败不丢,全量批处理移出Timer C消除2分钟同步周期
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m10s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 18:03:32 +08:00
youpaishidifu ccb67a0bf5 fix: 修复全员下线后SN同步定时器永久停摆与冷静期提示误导
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 1m53s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
1. WebManager: Timer B/C/E 全员下线时改为保活调度(不再 return 停死),
   Timer C 兼任看门狗在检测到玩家上线时恢复;doActiveSyncBatch 移除
   重复 schedule 调用避免产生双链。
2. SnManager.setStatus 写入 cancel_time,新增 cooldownMinutesLeft 辅助方法。
3. AreaProtection/AreaGUIManager/LandRecordManager/PVPManager 申领入口按
   真实原因分流提示(冷静期剩余分钟数 vs 旧SN未注销),副作用与 return 不变。
2026-09-29 16:38:31 +08:00
youpaishidifu 68bbb52ae6 SN防刷:管理员强制补发仅本次绕过冷静期,修复补发双重登记导致物品发不出去;注销回收容器与他人背包实物,新增死SN拦截(拾取/使用)与无SN旧品自动回收
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 13:37:45 +08:00
youpaishidifu 176e64187c 插入草原探险宣传广告
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m57s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 13:10:06 +08:00
youpaishidifu 7aad66f219 feat: 新增SN防伪系统,接入四类道具签发、拾取/合成/熔炼探查、销毁解绑与PHP目录同步
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m2s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 11:36:53 +08:00
Test User 62ebf5c771 test action trigger 1790651660
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m49s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 11:14:21 +08:00
Test User cef088396f test action trigger 1790651387
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 1m1s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-29 11:09:48 +08:00
Test User 95a8dba554 test action trigger 1790651350 2026-09-29 11:09:10 +08:00
youpaishidifu b62b5ebd83 5测audo release
Check Source / check (push) Successful in 1s
Auto Release / build (push) Failing after 45s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-29 10:37:02 +08:00
youpaishidifu 52dd5c54e9 维护消息占位符防双重替换:标签+定界符混合写法只换占位符
Check Source / check (push) Successful in 1s
Auto Release / build (push) Failing after 1m2s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
问题:配置「开始时间[starttime],结束时间[endtime]」时,定界符占位符先替换成时间,
中文标签「开始时间/结束时间」又被裸替换二次吃掉,渲染成两份时间连排。

修复:replaceVar 先扫描本组 key 是否已有定界符形式出现(delimHit),
命中则整组跳过裸子串替换、保留标签原文;裸写「开始时间」无定界符时仍正常替换。
新增 T3 用例 J 组 9 例,83/83 通过,白名单回归 27/0。
2026-09-29 00:28:40 +08:00
youpaishidifu f3306589e0 维护消息防御性编程:色码包JSON识别、残缺JSON修补、占位符任意中英文括号
Check Source / check (push) Successful in 1s
Auto Release / build (push) Failing after 46s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-28 23:59:07 +08:00
youpaishidifu 61d53d7689 修复维护模式两个线上问题:
Check Source / check (push) Successful in 6s
Auto Release / build (push) Failing after 1m5s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
1) JSON 消息内的字面 <br>/<br/>/</br> 不被摘除(JSON 分支绕过了换行预处理)
   -> deserialize 前先替换成 JSON 转义换行,标签不再漏给玩家
2) [白名单] 段标题被删(只剩「# ===== 白名单段 =====」注释)时玩家名被静默丢弃,
   白名单读成 0 人且 OP可绕过=flase 拼错也无提示 -> 所有人被拦
   -> 注释式段标题也识别 + 段外无键值行按玩家名兜底收录
   -> 条目清洗(零宽/全角空格/行内注释/列表前缀)+ 布尔拼写告警
   -> 维护开启时打印白名单条目与控制段键,便于对账

测试:T3 真实源码抽取 46/46(含用户真实白名单.txt 解析)、maint 33/0、whitelist 27/0
2026-09-28 22:50:43 +08:00
youpaishidifu fb297b34fb 修复维护消息JSON解析:顶层数组[{...}]此前不被识别、整段按纯文本打印成源代码,现与对象{...}同走Gson组件解析;JSON内占位符只替换{xxx}/%xxx%/<xxx>不再裸替换正文英文词;配置模板注明支持数组 2026-09-28 21:35:11 +08:00
youpaishidifu 5e778a1a6d fix: 白名单矫正为严格一行一个名字,段落头加「剥装饰后含=即非标题」硬门槛(修复控制键被误判成标题导致开关失效、全角【白名单】读空致全员被踢),并加空白名单告警 2026-09-28 21:00:54 +08:00
youpaishidifu 50498ffb41 新增维护模式(白名单):白名单.txt双段配置、24小时硬封顶、多格式时间解析、自定义消息与变量、登录/加入/退出热重载、异步预登录无感拦截 2026-09-28 20:01:25 +08:00
youpaishidifu 441569e758 修复sdf1兑换码远程验证:JSON解析支持布尔/数字/转义,已消耗回already_used不再误判未匹配,瞬时故障自动重排队 2026-09-28 19:39:43 +08:00
youpaishidifu 42c51e06a8 修复home床传送点:床被破坏自动删除、换床自动更新、复活抓取精确床坐标、回床走原版复活落点算法 2026-09-28 19:30:20 +08:00
youpaishidifu e2742d8a6e test audo release
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m24s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-28 18:46:04 +08:00
youpaishidifu 19eebf1358 Update GITEA_TOKEN for workflow
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-28 14:11:21 +08:00
youpaishidifu 78d3e11c21 修改.gitignore以屏蔽日志和开发过程中不必要的文件
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-28 13:38:26 +08:00
youpaishidifu 002b0e47a9 feat: /pvp see 重构为裁判⇄幽灵双形态切换(不再切回无保护普通玩家)、/pvp leave see 退出观赛时重新选装并补发PVP装备、断线重连补裁判身份清理
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-28 12:39:10 +08:00
youpaishidifu adae149791 fix: 空场判定延后1tick并列出剩余玩家(修复全员下线仍提示驻场)、断线未登记也复核、死亡时打印双方观赛状态、退出观赛加醒目警告
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m10s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-28 12:03:37 +08:00
youpaishidifu 668a5f3d38 fix: 观赛自动补登记时向玩家提示/pvp leave see并完善日志指引
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m28s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-28 11:38:43 +08:00
youpaishidifu 27ac366046 fix: 观赛战斗保护二次加固——身份判定不限世界、PVP世界野生观察者自动补登记、每次拦截留痕日志、MONITOR安全网防解取消
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m16s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-28 11:25:39 +08:00
youpaishidifu dea31b8222 fix: 观赛者以登记身份双向拦截战斗并强制恢复观察者、PVP世界删除全链路日志与磁盘复核重试
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-28 10:15:15 +08:00
youpaishidifu 233060e7bd fix: 观赛退出绝不回创造模式、正版验证轮询fail-closed、验证码改为99以内小学加减乘除
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m44s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
- PVPArenaManager: 进观赛时创造/观察者一律归一为生存再备份,exitSpectator/forceLeaveArena 兜底还原绝不给 CREATIVE(防退出观赛变创造作弊)
- WebManager: 正版验证轮询fail-closed——非JSON/缺字段视为无效响应仅记日志绝不放行,PHP明确失败即终止轮询,verified须凭证齐全且会话归属玩家一致才 autoLogin
- ChatFilterManager: 验证码算术题改为小学水平99以内加减乘除(和/积≤99、减法不为负、除法整除)
2026-09-27 23:57:32 +08:00
youpaishidifu 8388d68cb7 chore: gitignore忽略编译日志build.log
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m29s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 21:47:26 +08:00
youpaishidifu 2bcc449bc4 fix: PVP观察者改纯命令切换+双向免伤、清空队列联动PHP清理+回写lambda化、封禁IP推送Web黑名单
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-27 21:24:00 +08:00
youpaishidifu d8ff4b0c92 fix: 刮蜡独立拦截+安全权限化、新增PVP观察者观赛(/pvp see)、修复新布局PVP世界清理失效
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m20s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
- 涂蜡/刮蜡:从 onInteract 拆出独立处理器 onWaxInteract(HIGHEST),改用 e.getItem()/getHand() 覆盖副手,
  同时 DENY useInteractedBlock+useItemInHand;denyWax 改为安全权限(不看上级破坏/放置开关、
  不豁免所有者/管理员/访客),并加放行原因诊断日志,解决"开了开关却拦不住"。
- PVP观赛:/pvp see 仅PVP世界生效,自动进原版观察者,回收PVP装备但不发还玩家自有装备,
  发放2个不同雪球右键在观察者⇄普通生存间切换;离场/断线/重连自动还原游戏模式。
- PVP世界清理:新增 resolveWorldFolder/listWorldFolders 兼容 1.20.4+ 新布局
  world/dimensions/minecraft/<name>,替换全部 getWorldContainer 拼路径,并加启动残留清扫,
  修复磁盘堆出20+个 pvp_arena_* 残留目录的问题。
2026-09-27 18:56:22 +08:00
youpaishidifu c65b64030f feat: 领地新增生物蛋/涂蜡权限、基岩版固定算术验证码、-f非管理员静默回退
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
- protect delete <名> -f:非管理员不再报错拒绝,静默回退到普通60秒倒计时删除流程(含退费)
- 基岩版(Geyser/Floodgate)玩家无法用聊天框唤醒GUI验证码,改为固定出算术题
- 新增领地权限「生物蛋」denySpawnEgg,上级为放置方块,生效条件 denyBlockPlace && denySpawnEgg
- 新增领地权限「涂蜡/刮蜡」denyWax,上级为破坏方块+放置方块,生效条件 denyWax && (denyBlockBreak || denyBlockPlace)
- CLI/GUI/Web同步/DB(70->72列)/tab补全全链路接入两个新权限
- 修复GUI成员权限页历史缺项:耕地破坏/末影人行为点击数组缺失导致条目点不动
2026-09-27 16:09:25 +08:00
test 448f23ac53 fix: use release ID for asset upload
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m54s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 15:45:21 +08:00
test fb8b06c4e5 fix: use python3 to construct JSON payload
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m16s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 15:40:07 +08:00
test 4ab2b2dfd8 feat: use heredoc to generate release notes
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m45s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 15:33:28 +08:00
test baba10b84d feat: generate release notes from commits
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m11s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 15:27:58 +08:00
test 5e98830829 feat: generate release notes from commits
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m47s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 15:20:42 +08:00
test 8e84c58bcc feat: generate release notes from commits
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m15s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 15:15:10 +08:00
test b6ad95171d fix: use syntax to avoid YAML template expansion
Check Source / check (push) Successful in 4s
Auto Release / build (push) Failing after 54s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 15:09:17 +08:00
test c1bad334b7 fix: simplify workflow and pass env vars explicitly
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m1s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 15:02:59 +08:00
test 1e5576432d feat: generate release notes from git log with proper JSON escaping
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m1s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 14:56:19 +08:00
test fbdcca4089 feat: generate release notes from git log
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 2m2s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 14:50:30 +08:00
test fd2728fe42 feat: generate release notes from commits
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 1m56s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-27 14:45:00 +08:00
youpaishidifu 385301432a 领地删除退费、-f强制删除与封禁领主领地自动清理;修复领地外开箱残留与tpaall撤回入口
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 1m48s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
1) 退费:AreaConfig新增createCost并落库create_cost列,创建扣款成功后记录实际金额;新增area_config.refund_ratio_per_cent(0=关闭退费、100=全额、其他=原价×比例),60秒到期删除时按比例经BondManager退还债券(流水land_refund)。配置可通过 /protect config refund_ratio、CLI配置页与管理员GUI(位置16)调整,输入自动钳制0~100。

2) 强制删除:/protect delete <名> -f(或--force)仅插件管理员可用,跳过60秒倒计时与退费流程立即删除,并向在线插件管理员&OP上报领地坐标。

3) 自动清理:每5分钟扫描领地领主,命中永久封禁/剩余9年以上(即10年)封禁/账户已注销者,自动强制删除其领地(不退款、不注销账号),日志+在线管理员&OP上报坐标。

4) 修复领地外开箱残留漏洞(双箱四向同材质多候选逐侧判定、InventoryOpen准星回退定位、isUIBlock补CRAFTER/BOOKSHELF/COMPOSTER/CAULDRON、DoubleChest递归展开);tpaall发送后补充可点击撤回按钮并重开传送面板。
2026-09-27 14:26:06 +08:00
youpaishidifu 989b86f6e6 4测audo release
Check Source / check (push) Successful in 1s
Auto Release / build (push) Successful in 1m52s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 14:25:48 +08:00
test e848746800 fix: use release ID for asset upload
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 1m54s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 14:20:01 +08:00
test 915d7d1143 fix: use GITEA_TOKEN instead of ACTIONS_RUNTIME_TOKEN
Check Source / check (push) Successful in 2s
Auto Release / build (push) Successful in 2m9s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 14:14:29 +08:00
test e00a2f6e4b fix: use instead of to avoid YAML template expansion
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m7s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 14:06:03 +08:00
test 720757463b fix: use single quotes for JSON in curl
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m4s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 13:58:06 +08:00
test 6260ee11f9 fix: add Maven installation step
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 2m11s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 13:49:44 +08:00
test e4ff388281 fix: install JDK 25 from Adoptium
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 41s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 13:45:47 +08:00
test 12c6d14916 fix: use HTTP instead of HTTPS for git clone
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 1m32s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s
2026-09-27 13:38:50 +08:00
youpaishidifu 39b74f6c4e 删除test相关的全部内容
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 35s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 13:31:28 +08:00
test eac0da081e test: trigger workflow
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 33s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 13:25:38 +08:00
test b11beb7126 fix: strip protocol from GITHUB_SERVER_URL for git clone
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 34s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 13:12:11 +08:00
test eeecb551bc fix: explicit checkout and correct env vars
Check Source / check (push) Successful in 2s
Auto Release / build (push) Failing after 35s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 2s
2026-09-27 13:04:39 +08:00
test 7e9be87e7d test: check source code availability
Check Source / check (push) Successful in 3s
Auto Release / build (push) Failing after 34s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 13:00:15 +08:00
test 5dcf887236 fix: use correct Gitea Actions env vars
Auto Release / build (push) Failing after 36s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 12:55:44 +08:00
test 7e1f082b74 fix: proper release workflow with Java build
Auto Release / build (push) Failing after 1s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s
2026-09-27 12:46:55 +08:00
test 9a67a4a8bb test: add env test workflow
Auto Release / build (push) Successful in 38s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s
2026-09-27 12:46:30 +08:00
youpaishidifu 1e7b1a21b2 3测audo release
Auto Release / build (push) Successful in 36s
Simple Test / test (push) Successful in 1s
2026-09-27 12:34:53 +08:00
test 15e7ab62f9 fix: simplify workflow
Auto Release / build (push) Successful in 39s
Simple Test / test (push) Successful in 2s
2026-09-27 12:31:22 +08:00
youpaishidifu c249355e27 Merge branch 'main' of https://git.ypshidifu.cn/youpaishidifu/Sdf1_login
Auto Release / build (push) Failing after 43s
Simple Test / test (push) Successful in 1s
2026-09-27 12:26:12 +08:00
test c50b06361e fix: add Java setup step
Auto Release / build (push) Failing after 3m17s
Simple Test / test (push) Successful in 1s
2026-09-27 12:25:45 +08:00
test a521b4c59a test
Auto Release / build (push) Failing after 34s
Simple Test / test (push) Successful in 1s
2026-09-27 12:18:20 +08:00
test 1dba2690a1 final-test
Auto Release / build (push) Failing after 32s
Simple Test / test (push) Successful in 1s
2026-09-27 12:16:42 +08:00
test 2b7daa1928 debug-push
Auto Release / build (push) Failing after 55s
Simple Test / test (push) Successful in 2s
2026-09-27 12:12:48 +08:00
youpaishidifu f008c49e84 修复三项问题:1)大箱子跨领地边界绕过容器权限(双箱改为左右两侧均收集定位并逐侧判定denyContainer,交互事件改用被点击方块定位并补查相邻半箱)2)tpaall语义文案修正(通知/按钮/GUI统一为全服玩家一起传送到发起者身边)3)新人验证码超时不放行改为重新出题,题库引入九九乘法表加减乘除+GUI五选一随机抽题;另注释移动豁免诊断日志 2026-09-27 12:11:41 +08:00
test 16214872aa test-push
Auto Release / build (push) Failing after 51s
Simple Test / test (push) Successful in 2s
2026-09-27 12:05:58 +08:00
test d8a4c820fb test: add simple workflow
Auto Release / build (push) Failing after 34s
Simple Test / test (push) Successful in 1s
2026-09-27 12:03:00 +08:00
test 31b4594c2e test: verify docker access
Auto Release / build (push) Failing after 1m7s
2026-09-27 12:00:41 +08:00
test 4f6f0d6252 debug: trigger workflow
Auto Release / build (push) Failing after 44s
2026-09-27 11:53:51 +08:00
test 499cf10aba test: verify runner works
Auto Release / build (push) Failing after 47s
2026-09-27 11:50:14 +08:00
test df86721562 fix: use ubuntu-latest for runner label compatibility
Auto Release / build (push) Failing after 13m56s
2026-09-27 11:41:21 +08:00
test 28a33e7c02 test: trigger action with restored runner
Auto Release / build (push) Canceled after 0s
2026-09-27 11:31:14 +08:00
test 5ad1843200 fix: restore runner daemon and trigger action 2026-09-27 11:31:14 +08:00
youpaishidifu 6f844b5714 修复领地主判定大小写不敏感导致越权绕过:身份比对统一改为 samePlayer 精确匹配(区分大小写),领地上限 DB 统计改精确匹配
Auto Release / build (push) Canceled after 0s
2026-09-27 10:29:33 +08:00
youpaishidifu 2429d2b614 删掉测试用的文件
Auto Release / build (push) Failing after 5s
2026-09-26 20:28:25 +08:00
test e6e7664244 fix: repair malformed release.yml YAML
Auto Release / build (push) Failing after 5s
2026-09-26 19:53:23 +08:00
test 005b8db0d4 test: verify push and hooks after packed-refs fix 2026-09-26 19:48:39 +08:00
CI 477dbab970 Test trigger new workflow 2026-09-26 10:36:04 +00:00
CI b91814d837 Trigger: update workflow for system JDK 2026-09-26 10:28:29 +00:00
CI 43df7808cd Fix-use-system-JDK 2026-09-26 10:15:59 +00:00
CI 3b8fbdf1b7 Fix: use system JDK 2026-09-26 10:07:22 +00:00
youpaishidifu 920cf79641 删除垃圾文件
Auto Release / build (push) Failing after 28s
2026-09-26 15:35:27 +08:00
youpaishidifu 1a83fe7c55 二测audo release
Auto Release / build (push) Failing after 2m1s
2026-09-26 15:15:30 +08:00
youpaishidifu bcaac7ee71 测试release 2026-09-26 15:04:43 +08:00
youpaishidifu d489c01000 测试action运行 2026-09-26 14:36:38 +08:00
youpaishidifu 411210c341 删掉垃圾文件 2026-09-26 14:33:33 +08:00
youpaishidifu b78f87db62 Merge branch 'main' of https://git.ypshidifu.cn/youpaishidifu/Sdf1_login
# Conflicts:
#	.gitea/workflows/release.yml
#	.github/workflows/release.yml
#	.gitignore
#	src/main/java/Sdf1_login/ChatInputManager.java
#	src/main/java/Sdf1_login/Main.java
2026-09-26 14:24:43 +08:00
youpaishidifu 079ff0587f 删除 test_trigger.txt 2026-09-26 05:38:46 +00:00
youpaishidifu e7c862f4e8 修复大小写变种字符玩家绕过移动限制:统一 normName 规范化匹配,per-player 权限优先于访客豁免,新增移动豁免诊断日志 2026-09-26 13:30:54 +08:00
youpaishidifu 51e56ec85e 注册/聊天改密成功后弹出60秒actionbar凭据保存提醒 2026-09-26 12:23:34 +08:00
youpaishidifu 8069070219 删除不必要的备份文件,修复几个严重bug:
问题1. 领地系统错误的把玩家名归一化引起的大小写名字、特殊字符的玩家绕过限制
修复1:取消归一化,按人头配置权限
问题2:临时密码不作为改密码的验证凭据,导致忘记真实密码后无法改密码
修复2:临时密码也作为改密码的验证凭据
2026-09-26 12:19:32 +08:00
Admin bf421cbd18 fix: move workflow to .github/workflows for Gitea Actions 2026-09-26 12:07:38 +08:00
test a517d12feb test: trigger workflow after runner fix 2026-09-26 12:05:35 +08:00
test 5cca422ba9 fix: use ubuntu-latest docker runner for JDK 25 2026-09-26 09:51:44 +08:00
test 307db659bc trigger workflow test 2026-09-26 09:42:09 +08:00
Test fe89f9649e trigger workflow test 2026-09-26 09:34:00 +08:00
Gitea Runner 6609b29fab fix: use Ubuntu container for JDK 25 compatibility 2026-09-26 09:31:33 +08:00
Gitea Runner ef03f1b90b fix: use host JDK 25 instead of actions/setup-java 2026-09-25 23:43:07 +08:00
test 1d56705365 trigger action test 1790347157 2026-09-25 22:39:31 +08:00
test 8d3442c966 trigger action 2026-09-25 22:31:56 +08:00
CI 89b876754c fix: workflow改用self-hosted runner 2026-09-25 22:24:12 +08:00
youpaishidifu 47678a4c3d 修复领地名字归一化越权 + 改密不校验临时密码
Auto Release / build (push) Failing after 2m1s
问题1(严重):领地写命令在 resolveAreaName 模糊归一化后缺少归属校验,任何玩家可用大小写/部分名/特殊字符命中他人领地执行操作。

- 新增 requireLandOwnership / requireGlobalAdmin 权限闸门,补齐 add/remove/addname/removename/addwhite/removewhite/additem/removeitem/expand/contraction/canceldelete 的领主或管理员校验;global 级操作改为仅管理员

- parseAreaAndTarget、delete、canceladminchange 改用新增的 resolveAreaNameStrict(只做完全一致匹配,不再做包含/编辑距离模糊匹配)

- 新增 validateAreaName 领地名格式校验与 areaNameExists 大小写不敏感重名检查,创建/确认创建/改名统一接入,防止特殊字符污染白名单文件名与大小写互覆盖

- 白名单增删统一按小写存取,修正 add 存小写而 remove 用原样比较导致移除失效的问题

问题2:修改密码改为同时校验真实密码与临时密码(新增 DatabaseManager.checkPasswordForChange),临时密码不再受登录用的5分钟有效期限制,避免临时密码登录后忘记真实密码被永久锁死;三条改密路径(命令两步/聊天输入两步)全部接入,改密成功后作废旧临时密码并同步Web端。

版本号 2.9 -> 2.91;PHP 后端未改动。
2026-09-25 21:25:26 +08:00
youpaishidifu f92e251bdc fix release.yml: 第18行 YAML 冒号误解析->改用 block scalar | 2026-07-24 17:39:29 +08:00
youpaishidifu 383e1bc7df bump v2.9; release workflow 从 plugin.yml 读版本号 2026-07-24 17:33:16 +08:00
youpaishidifu ca8070e31b 2.9正式版
Auto Release / build (push) Successful in 30s
2026-07-24 17:31:16 +08:00
227 changed files with 19402 additions and 138098 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Check Source
on:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check workspace
run: |
echo "=== PWD ==="
pwd
echo "=== LS ==="
ls -la
echo "=== FIND POM ==="
find / -name "pom.xml" 2>/dev/null
echo "=== ENV ==="
env | grep -iE "workspace|repo|git" | sort
echo "=== DONE ==="
+122 -99
View File
@@ -1,100 +1,123 @@
name: Auto Release
name: Auto Release
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Setup JDK 25
run: |
echo "Setting up JDK 25..."
apt-get update -qq
apt-get install -y -qq wget unzip
wget -q https://api.adoptium.net/v3/binary/latest/25/ga/linux/x64/jdk/hotspot/normal/eclipse?project=jdk -O /tmp/jdk25.tar.gz
tar -xzf /tmp/jdk25.tar.gz -C /opt
export JAVA_HOME=$(ls -d /opt/jdk-25* | head -1)
echo "JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV
echo "$JAVA_HOME/bin" >> $GITHUB_PATH
echo "JDK 25 installed: $($JAVA_HOME/bin/java -version 2>&1 | head -1)"
echo "Setup completed"
- name: Setup Maven
run: |
echo "Setting up Maven..."
apt-get install -y -qq maven
echo "Maven version: $(mvn -version 2>&1 | head -1)"
echo "Setup completed"
- name: Checkout code
run: |
echo "Checking out code..."
GIT_HOST=${GITHUB_SERVER_URL#http://}
GIT_HOST=${GIT_HOST#https://}
rm -rf /workspace/youpaishidifu/Sdf1_login/*
rm -rf /workspace/youpaishidifu/Sdf1_login/.[!.]*
git clone "http://${GIT_HOST}/$(echo ${GITHUB_REPOSITORY} | tr '[:upper:]' '[:lower:]').git" .
echo "Checkout completed"
- name: Build with Maven
run: |
echo "Building with Maven..."
mvn -B clean package -DskipTests
echo "Build completed"
- name: Verify JAR
run: |
echo "Verifying JAR..."
ls -la target/*.jar
echo "Verification completed"
- name: Generate Release Notes
run: |
echo "Generating release notes..."
VERSION=$(grep "^version:" src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
TAG="v$VERSION"
# Generate release notes from last 10 commits
echo "## Changes in $TAG" > /tmp/release_body.md
echo "" >> /tmp/release_body.md
git log --format="- **%h** %s (%an, %ad)" --date=short -10 >> /tmp/release_body.md
echo "" >> /tmp/release_body.md
echo "### Full changelog" >> /tmp/release_body.md
echo "" >> /tmp/release_body.md
echo "[View all commits](/youpaishidifu/Sdf1_login/commits/main)" >> /tmp/release_body.md
echo "Release notes generated for $TAG"
cat /tmp/release_body.md
- name: Create Release
env:
GITEA_TOKEN: 194187cbdc8e1f1d3bc810b1b9e07ddc904a1f73
run: |
set -e
VERSION=$(grep "^version:" src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
TAG="v$VERSION"
BASE="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY"
echo "Version: $VERSION, Tag: $TAG"
echo "=== Checking existing tag $TAG ==="
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/tags/$TAG")
echo "Check tag HTTP: $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
OLD_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/tags/$TAG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
echo "Tag $TAG exists (release #$OLD_ID), deleting..."
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/$OLD_ID"
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$BASE/tags/$TAG"
sleep 1
fi
echo "=== Creating release ==="
# Use python3 to construct proper JSON
python3 -c "
import json, sys
body = open('/tmp/release_body.md').read()
data = {'tag_name': '$TAG', 'name': '$TAG', 'body': body, 'draft': False, 'prerelease': False}
print(json.dumps(data))
" > /tmp/release_payload.json
RESP=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$BASE/releases" \
-d @/tmp/release_payload.json)
RELEASE_ID=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
echo "Created release #$RELEASE_ID"
echo "=== Uploading artifact ==="
JAR=$(ls target/*.jar | head -1)
UPLOAD_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/jar" \
"$BASE/releases/$RELEASE_ID/assets?name=$(basename $JAR)" \
--data-binary "@$JAR")
echo "Upload HTTP: $UPLOAD_CODE"
echo "=== Release $TAG created successfully ==="
on:
push:
branches: [main]
jobs:
build:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- name: Verify Java
run: |
echo "JAVA_HOME=$JAVA_HOME"
java -version
- name: Build with Maven
run: mvn -B clean package -DskipTests
- name: Verify JAR
run: ls -la target/*.jar
- name: Create Release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -e
# 从 plugin.yml 提取版本号作为标签
VERSION=$(grep '^version:' src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
TAG="v${VERSION}"
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
echo "Version: ${VERSION}, Tag: ${TAG}"
echo "Token prefix: ${GITEA_TOKEN:0:8}..."
# 测试 token 权限
echo "=== Testing API access ==="
curl -s -w "\nHTTP_CODE:%{http_code}\n" \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}" | tail -5
# 如果同版本标签已存在,先删除旧 release
echo "=== Checking existing tag ${TAG} ==="
RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/releases/tags/${TAG}")
HTTP_CODE=$(echo "$RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
echo "Check tag HTTP: ${HTTP_CODE}"
if [ "$HTTP_CODE" = "200" ]; then
OLD_ID=$(echo "$RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
echo "Tag ${TAG} exists (release #${OLD_ID}), deleting..."
curl -s -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/releases/${OLD_ID}"
curl -s -X DELETE \
-H "Authorization: token $GITEA_TOKEN" \
"${BASE}/tags/${TAG}"
sleep 1
elif [ "$HTTP_CODE" = "404" ]; then
echo "Tag ${TAG} not found, will create new"
else
echo "Warning: unexpected response checking tag"
fi
# 创建新 release
echo "=== Creating release ${TAG} ==="
RELEASE_RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Sdf1_login ${TAG}\",\"body\":\"Version: ${VERSION}\\nCommit: ${{ gitea.sha }}\\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
"${BASE}/releases")
RELEASE_HTTP=$(echo "$RELEASE_RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
echo "Create release HTTP: ${RELEASE_HTTP}"
if [ "$RELEASE_HTTP" != "201" ]; then
echo "=== Release creation failed! ==="
echo "$RELEASE_RESP" | head -1
exit 1
fi
# 上传 JAR 资产
RELEASE_ID=$(echo "$RELEASE_RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -n "$RELEASE_ID" ]; then
echo "=== Uploading JAR to release #${RELEASE_ID} ==="
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@target/Sdf1_login.jar" \
"${BASE}/releases/${RELEASE_ID}/assets"
echo "✅ Release ${TAG} created with JAR"
else
echo "⚠️ Release created but could not get release ID for JAR upload"
fi
+12
View File
@@ -0,0 +1,12 @@
name: Simple Test
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Hello
run: echo "Hello from Gitea Actions!"
+15
View File
@@ -0,0 +1,15 @@
name: Test Env
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Show env
run: |
echo "=== ENV ==="
env | sort
echo "=== DONE ==="
+8 -3
View File
@@ -13,6 +13,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from plugin.yml
id: version
run: |
echo "version=$(grep '^version: ' src/main/resources/plugin.yml | sed 's/version: //')" >> $GITHUB_OUTPUT
- name: Set up JDK 26
uses: actions/setup-java@v4
with:
@@ -28,7 +33,7 @@ jobs:
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: build-${{ github.run_number }}
name: Build #${{ github.run_number }}
tag_name: v${{ steps.version.outputs.version }}
name: v${{ steps.version.outputs.version }}
files: target/Sdf1_login.jar
generate_release_notes: true
generate_release_notes: true
+29
View File
@@ -39,6 +39,7 @@ build/
.DS_Store
# 开发辅助文件(本地保留,不推送到仓库)
/代码关联关系.md
txt/
*.xlsx
*.patch
@@ -92,3 +93,31 @@ lpt8
lpt9hs_err_pid*.log
hs_err_pid*.log
replay_pid*.log
# 屏蔽patch文件
*.patch
# 屏蔽wb产生的工作文件
.workbuddy/
# 屏蔽备份文件
backup/
# 屏蔽根目录下的所有json
/*.json
# 屏蔽垃圾文件
*.idx
*.pack
*.rar
test.*
/*.rev
# 屏蔽全部日志文件
*.log
# 屏蔽开发计划相关文件
*.xlsx
*.docs
*.doc
*.txt
# 屏蔽全部部署脚本(wb用)
*.py
# 屏蔽其它文件
*.md
# 过滤readme
!readme.md
-21
View File
@@ -1,21 +0,0 @@
## 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/ 目录
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+9
View File
@@ -1,5 +1,14 @@
# Sdf1系列登录插件使用说明
## 开源协议
本项目(Sdf1_login 插件,含 Java 端与 php 端)采用 **GNU Affero General Public License v3.0(AGPL-3.0)** 授权,协议全文见 [LICENSE](LICENSE)。
- 你可以自由使用、修改、分发本项目。
- **若你把本项目(或其修改版)部署为网络服务对外开放,必须向使用该服务的用户提供完整的对应源代码**(AGPL 第 13 条)。
- 衍生作品必须同样以 AGPL-3.0 授权分发。
- 本项目按「原样」提供,不附带任何担保。
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
## 1. 功能介绍
-5
View File
@@ -1,5 +0,0 @@
*.md text=auto encoding=UTF-8
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
-46
View File
@@ -1,46 +0,0 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
# 开发辅助文件(本地保留,不推送到仓库)
txt/
*.xlsx
*.patch
新建*.txt
群戏归类.txt
File diff suppressed because it is too large Load Diff
-569
View File
@@ -1,569 +0,0 @@
# Sdf1系列登录插件使用说明
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
## 1. 功能介绍
| 名字 | 描述/作用 | 施工状态 |
| :--- | :--- | :--- |
| 登录 | 用户登录/注册 | 已完成 |
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
| 工单系统 | 用户提交工单 | 已完成 |
| 签到系统 | 签到积分换奖励 | 已完成 |
| 新人礼包 | 新玩家7日礼包 | 已完成 |
| 挂机检测 | 挂机检测踢出 | 已完成 |
| 自定义菜单 | 可以自己diy指令 | 已完成 |
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
## 2.安装说明:
| 前置插件 | 选装类型 | 功能作用 |
| :--- | :--- | :--- |
| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
| EssentialsX | 可选 | 主流经济系统
| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统
### 2.1 下载
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
### 2.2. 配置文件
#### 2.2.1 消息.txt
此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)<br>
同时兼容`<b> </b>`标签,对段落加粗,`\n 和 <br>`进行段落换行
```txt
# ==========================================
# Sdf1_login 消息配置文件
# ==========================================
# 颜色代码使用 § 符号
# §0黑色 §1深蓝 §2深绿 §3深青
# §4深红 §5深紫 §6金色 §7灰色
# §8深灰 §9蓝色 §a绿色 §b浅蓝
# §c红色 §d粉色 §e黄色 §f白色
# §l粗体 §n下划线 §o斜体 §r重置
#
# 占位符用 {xxx} 表示,运行时自动替换
# 支持 <b>粗体</b> 标签
# 支持 \n 或 <br> 换行
# 修改后执行 /sdf1_login reload 重载
# ==========================================
# ---- 登录注册相关 ----
# login_timeout: 登录超时被踢出时的提示
login_timeout=§c登录超时,请重新登录
# not_registered: 未注册玩家收到的提示
not_registered=§e您尚未注册,请使用 /reg <密码>
# not_logged_in: 已注册但未登录的提示
not_logged_in=§e您尚未登录,请使用 /login <密码>
# already_registered: 已注册玩家再次注册时的提示
already_registered=§c您已注册过账号
# already_logged_in: 已登录玩家再次登录时的提示
already_logged_in=§c您已登录
# reg_success: 注册成功提示
reg_success=§a注册成功!欢迎来到服务器
# login_success: 登录成功提示
login_success=§a登录成功!
# login_failed: 密码错误提示
login_failed=§c密码错误,请重试
# ---- 密码相关 ----
# password_wrong: 旧密码验证失败
password_wrong=§c密码错误
# password_changed: 密码修改成功
password_changed=§a密码修改成功!
# password_format_error: 密码格式不符合要求
password_format_error=§c密码需6位以上,含大小写字母和数字
# password_same: 新密码与旧密码相同
password_same=§c新密码不能与旧密码相同
# reset_no_email: 找回密码时未绑定邮箱
reset_no_email=§c您未绑定邮箱,无法找回密码
# reset_sent: 临时密码已发送到邮箱
reset_sent=§a临时密码已发送到您的邮箱,请查收
# ---- 签到积分相关 ----
# checkin_already: 今日已签到
checkin_already=§c您今日已签到
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
# points_insufficient: 积分不足
points_insufficient=§c积分不足,当前: {points}
# points_purchase_success: 积分购买成功
points_purchase_success=§a购买成功!消耗 {count} 积分
# ---- 挂机检测相关 ----
# afk_set_enabled: 挂机检测开启
afk_set_enabled=§a挂机检测已开启
# afk_set_disabled: 挂机检测关闭
afk_set_disabled=§c挂机检测已关闭
# afk_kick: 因挂机被踢出
afk_kick=§c因挂机过久被踢出
# ---- 邮箱相关 ----
# email_set: 邮箱设置成功
email_set=§a邮箱设置成功:{email}
# email_format_error: 邮箱格式错误
email_format_error=§c邮箱格式不正确
# ---- 邀请相关 ----
# invite_code_generated: 邀请码生成
invite_code_generated=§a邀请码:{code}
# invite_used: 使用邀请码成功
invite_used=§a邀请成功,邀请人:{player}
# invite_self: 使用自己的邀请码
invite_self=§c不能使用自己的邀请码
# invite_not_found: 邀请码不存在
invite_not_found=§c邀请码不存在
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
# ---- 新人礼包相关 ----
# gift_not_ready: 礼包条件未满足
gift_not_ready=§c礼包条件未满足
# gift_claimed: 礼包领取成功
gift_claimed=§a礼包领取成功!
# ---- 审批与IP限制 ----
# need_approval: 注册需审批
need_approval=§e注册需审批 (工单#{id})
# ip_max_accounts: IP注册已达上限
ip_max_accounts=§cIP注册已达上限
# ---- 管理员操作 ----
# admin_delete_confirm: 管理员删除账号确认提示
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
# ---- 聊天过滤 ----
# chat_muted: 禁言玩家尝试聊天
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
# chat_url_blocked: 发送违规链接被拦截
# {url}=检测到的链接地址
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
# chat_url_violation: 当前违规次数
# {count}=当前违规次数
chat_url_violation=§7当前违规次数: §e{count}
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
# chat_url_broadcast: 全服通报,{player}=玩家名
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
# ---- 通用 ----
# 未知参数: 未知命令或权限不足时的提示
未知参数=§c未知参数或权限不足
# only_player: 仅玩家可用的命令
# only_player=§c此命令仅玩家可用
```
### 2.2.2 挂机白名单.txt
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
### 2.2.3 SMTP设置.txt
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
```txt
# ===== SMTP 邮件服务器配置 =====
smtp地址=smtp.example.com
smtp端口=465
smtp账号=mail@example.com
smtp密码=example.com
发件人名称=Sdf1_login
smtp加密=true
验证码接收邮箱=example@example.com
```
### 2.2.4 插件设置.txt
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
```txt
# ===== Sdf1_login 插件设置 =====
管理标签=admin
管理密码=qweasd
每IP最大账号数=3
审批模式=manual
自动审批延迟分钟=30
登录超时秒数=60
挂机踢出=false
挂机超时秒数=300
服务商积分倍率=2.0
基础经济奖励=100
经济奖励每评分=50.0
报单人积分奖励=10
# ---- 签到盲盒奖励 ----
签到固定金额=0
签到最小金额=10
签到最大金额=100
签到给积分=true
签到积分数量=10
```
### 2.2.5 chat.txt
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
可用的处罚规则:
- warn: 警告
- mute: 禁言
- kick: 踢出
- ban: 封禁
- banip:封禁IP
```txt
# Sdf1_chat 聊天过滤配置
通知管理员: true
全服通报: true
禁言时长: 300
白名单:
*.minecraft.net
baidu.com
github.com
处罚规则:
1:warn:0
2:warn:0
3:mute:300
5:mute:600
8:kick:0
10:ban:0
白名单玩家:
```
### 2.2.6 radio_config.txt
```txt
# ============ Sdf1 Radio Config ============
#
# resource-pack-url
# 填外部地址(HTTP或HTTPS均可)
# 例: https://example.com/radiopack.zip
# 留空 = 使用内置HTTP服务
#
# http-port
# 内置HTTP端口(仅resource-pack-url为空时生效)
#
# ============ Settings ===============
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
http-port=443
```
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
### 2.2.7 菜单.txt
- 自定义菜单配置文件
```json
# 服务器菜单
# 使用类json格式
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
{
标题:主城传送
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
权限类型:玩家
图标:COMPASS
}
{
标题:随机传送
指令:/rtp
权限类型:玩家
图标:ENDER_PEARL
}
{
标题:擂台比武
指令:/dom tp PVP
权限类型:玩家
图标:netherite_spear
}
{
标题:领地操作
指令:/dom
权限类型:玩家
图标:paper
}
{
标题:逛商店
指令:/dom tp shop
权限类型:玩家
图标:honeycomb
}
{
标题:传送开关
指令:/tptoggle
权限类型:玩家
图标:redstone_torch
}
{
标题:返回上一个位置
指令:/back
权限类型:玩家
图标:red_bed
}
{
标题:前往小黑塔
指令:/dom tp 小黑塔
权限类型:玩家
图标:dragon_head
}
{
标题:云背包
指令:/cy
权限类型:玩家
图标:potato
}
{
标题:自杀
指令:/suicide
权限类型:玩家
图标:wooden_axe
}
{
标题:游戏商人
指令:/shop
权限类型:玩家
图标:shield
格子:28
}
```
### 2.2.8 商品配置
- 配置此处的文件,负责展示游戏内所销售的商品。<br>
- 一个文件名,对应一个商品分类,使用md文件格式
```
# 建筑材料
| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 |
| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 |
| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 |
| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 |
| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 |
| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 |
| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 |
| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 |
```
## 2.3 商城部分介绍
1. 自助退款,买多买错,不用慌,一件退单很安心<br>
2. 购物车,买的多怕买错,先加购物车,后续统一结算<br>
3. 害怕订单找不到?一键查看历史订单<br>
![](img/shop4.png)
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
![](img/shop7.png)<br>
![](img/shop6.png)
5. 想查看玩家消费记录?小手一点,明明白白<br>
![](img/shop5.png)
## 2.4 打印流水部分介绍
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
## 3. 指令参数及介绍
- Sdf1_login 主指令<br>
- Sdf1_login reload 重载配置文件<br>
- reg 注册账号<br>
- login 登录账号<br>
- l 登录账号<br>
- sdf1_login del 删除玩家账号<br>
- sdf1_login radio 停止播放广播<br>
- sdf1_login radio reload 重载广播并重新打包资源包<br>
- menu 开/关 是否开启菜单<br>
- printer 打印流水数据,支持不带参数&带玩家名为参数<br>
- sdf1_login printer 作用同printer<br>
- sdf1_login shop 商城
- protect 区域防护
## 4. 自定义菜单物品
- 打开主界面,选择"我的"进入<br>
![](img/menu1.png)
- 将你的自定义物品与雪球交换<br>
![](img/menu2.png)
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
## 5. pvp战神榜
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
![](img/pvp1.png)
- 并且有着网游同款的击杀通报<br>
![](img/pvp3.png)
- 播报逻辑是:<br>
- 3杀<br>
- 5杀<br>
- 5杀以上每杀一次都是一条播报<br>
- 终结者通报,触发连杀的玩家被终结也会有全服通报
## 6. 区域防护
- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。<br>
- 还能够保护指定区域的玩家不受任何伤害。<br>
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
| :---: | :---: | :---: | :---: | :---: |
| protect | 无 | 无 | 无 | 帮助信息 |
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
示范:
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<br>
- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写
| 参数 | 效果 |
| :---: | :---: |
| 和平模式 | 所有敌对生物全部进虚空 |
| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 |
| 清除XX效果 | 立即清除玩家身上的效果
| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果
## 生物属性对照表
| 中文名称 | 类型 | 附加属性 |
| :---: | :----: | :----: |
| 溺尸 | 近战/远程 | 亡灵、水生 |
| 骷髅 | 远程 | 亡灵 |
| 流浪者 | 远程 | 亡灵、寒冷群系变种 |
| 沼骸 | 远程 | 亡灵、沼泽变种 |
| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 |
| 掠夺者 | 远程 | 灾厄村民、袭击成员 |
| 卫道士 | 近战 | 灾厄村民、袭击成员 |
| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 |
| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 |
| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 |
| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 |
| 守卫者 | 近战/激光远程 | 海底神殿、水下 |
配置文件解析:<br>
- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的<!--- --->注释`和`Java/* */块注释`
```txt
# 区域: 主城区
起点: world,-130,-713
终点: world,-427,-560
高度范围: 0-255
没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个)
// 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块
// 禁止破坏方块 # 同上
禁止PVP # 玩家之间不能PVP
禁止摔伤 # 摔落无法造成任何伤害
禁止饥饿 # 不会消耗饱食度
禁止一切伤害 # 任何怪物都无法造成伤害
# 禁止丢弃物品
# 禁止末影珍珠
# 禁止使用弓箭
# 禁止骑乘
// 和平模式 // 所有的敌对生物全部传送进虚空
禁止袭击
禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外
// 清除所有效果 # 清理所有负面效果
效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间
效果 生命提升 255 999999
效果 抗性提升 255 999999
效果 抗火 255 999999
# 禁止药水
禁止爆炸
禁止使用物品: TNT
# 效果: 夜视 1 999
进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示
离开提示: 已离开保护区
没收提示: 你的违禁品已被没收
通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域
# 惩罚命令: ban {player} 60s 违规
```
## 运行截图
主界面
![](img/login.png)
邀请数据
![](img/login2.png)
积分商店
![](img/login3.png)
任务中心
![](img/login4.png)
垃圾箱
![](img/login5.png)
管理员面板
![](img/login6.png)
管理面板-用户管理
![](img/login7.png)
工单中心
![](img/login8.png)
工单管理
![](img/login9.png)
我的工单
![](img/login10.png)
提交工单
![](img/login11.png)
服务商列表
![](img/login12.png)
服务商抢单大厅
![](img/login13.png)
服务商-接单界面
![](img/login14.png)
管理员-工单详情页面
![](img/login15.png)
商城主界面
![](img/shop1.png)
商城购物车
![](img/cart1.png)
商城退单
![](img/shop2.png)
## 4. 下载地址
- 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)
-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sdf1</groupId>
<artifactId>Sdf1_login</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.4-R0.1-SNAPSHOT</version>
<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>
<version>3.46.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>Sdf1_login</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,197 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
public class AFKManager {
// 玩家最后活动时间(空闲才开始计时)
private final Map<UUID, Long> lastAction =
new HashMap<>();
// 每个玩家当前的警告阶段
private final Map<UUID, Integer> lastStage =
new HashMap<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
new HashMap<>();
private final ConfigManager config;
private final JavaPlugin plugin;
public AFKManager(ConfigManager config,
JavaPlugin plugin) {
this.config = config;
this.plugin = plugin;
}
public void startCheck() {
new BukkitRunnable() {
public void run() {
tick();
}
}.runTaskTimer(plugin, 20L, 20L);
}
private void tick() {
if (!config.afkEnabled) return;
long now = System.currentTimeMillis();
for (Player p : Bukkit.getOnlinePlayers()) {
String name = p.getName();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
continue;
}
int stage = lastStage.getOrDefault(uuid, 0);
// 阶段3:最后10秒
if (remain <= 10) {
if (stage < 3) {
lastStage.put(uuid, 3);
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + remain + " 秒踢出",
0, 22, 3);
playSound(p);
continue;
}
// 阶段2:1分钟~10秒
if (remain <= 60) {
if (stage < 2) {
lastStage.put(uuid, 2);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + fmtSec(remain) + " 踢出",
5, 25, 5);
playSound(p);
continue;
}
// 阶段1:3分钟提醒
if (remain <= 180 && stage < 1) {
lastStage.put(uuid, 1);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§f还剩 " + fmtSec(remain) + " 踢出",
10, 60, 20);
for (int i = 0; i < 3; i++) {
final int delay = i * 10;
Bukkit.getScheduler().runTaskLater(
plugin, () -> {
if (p.isOnline()) playSound(p);
}, delay);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
private void playSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
private String fmtSec(long sec) {
if (sec < 60) return sec + "秒";
long m = sec / 60;
long s = sec % 60;
return m + "分" + (s > 0 ? s + "秒" : "");
}
}
@@ -1,146 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AccountRequestManager {
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
public static class Request {
public final int id;
public final String requester;
public final String target;
public final String ip;
public final long time;
public String status;
public String denyReason;
public Request(int id, String requester, String target, String ip) {
this.id = id;
this.requester = requester;
this.target = target;
this.ip = ip;
this.time = System.currentTimeMillis();
this.status = "PENDING";
this.denyReason = "";
}
}
private final Main plugin;
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
private int nextId = 1;
private ApprovalMode mode = ApprovalMode.MANUAL;
private int autoDelayMinutes = 60;
public AccountRequestManager(Main plugin) {
this.plugin = plugin;
}
public ApprovalMode getMode() { return mode; }
public void setMode(String m) {
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
}
public void setAutoDelayMinutes(int min) {
this.autoDelayMinutes = min;
}
public Request createRequest(String requester, String target, String ip) {
int id = nextId++;
Request req = new Request(id, requester, target, ip);
requests.put(id, req);
pendingByPlayer.put(requester, id);
if (mode == ApprovalMode.AUTO) {
scheduleAutoApprove(id);
} else if (mode == ApprovalMode.HYBRID) {
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
if (ip != null && ip.equals(targetIp)) {
scheduleAutoApprove(id);
}
}
return req;
}
private void scheduleAutoApprove(int id) {
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
Request req = requests.get(id);
if (req == null) return;
String requesterName = req.requester;
String targetName = req.target;
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Request r = requests.get(id);
if (r != null && "PENDING".equals(r.status)) {
approveRequest(id);
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
Player requester = Bukkit.getPlayer(requesterName);
if (requester != null && requester.isOnline()) {
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
}
}
}, delayTicks);
}
public boolean approveRequest(int id) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "APPROVED";
plugin.getDb().deleteUser(req.target);
pendingByPlayer.remove(req.requester);
// 清背包+传送出生点+踢出
Player target = Bukkit.getPlayer(req.target);
if (target != null && target.isOnline()) {
target.getInventory().clear();
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
target.teleport(target.getWorld().getSpawnLocation());
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
}
return true;
}
public boolean denyRequest(int id, String reason) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "DENIED";
req.denyReason = reason;
pendingByPlayer.remove(req.requester);
return true;
}
public List<Request> getPendingRequests() {
List<Request> list = new ArrayList<>();
for (Request r : requests.values()) {
if ("PENDING".equals(r.status)) list.add(r);
}
return list;
}
public Request getRequest(int id) {
return requests.get(id);
}
public boolean hasPending(String player) {
return pendingByPlayer.containsKey(player);
}
public void cancelPending(String player) {
Integer id = pendingByPlayer.remove(player);
if (id != null) {
Request r = requests.get(id);
if (r != null) r.status = "CANCELLED";
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,697 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.File;
import java.sql.*;
import java.util.*;
public class BondManager {
private final Main plugin;
private Connection db;
// ===== 交易类型常量 =====
public static final String TX_ADMIN_GIVE = "admin_give";
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
public static final String TX_TRANSFER_OUT = "transfer_out";
public static final String TX_TRANSFER_IN = "transfer_in";
public static final String TX_REDEEM = "redeem";
public static final String TX_SHOP_BUY = "shop_buy";
public static final String TX_DAILY_SIGN = "daily_sign";
public static final String TX_FREEZE = "freeze";
public static final String TX_UNFREEZE = "unfreeze";
public static final int MAX_WATER_DAYS = 14;
public static final int ADMIN_MAX_WATER_DAYS = 30;
public BondManager(Main plugin) {
this.plugin = plugin;
initDB();
}
// ======================== 数据库初始化 ========================
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
// 债券余额(含冻结状态)
st.execute("CREATE TABLE IF NOT EXISTS bonds ("
+ "player_name TEXT PRIMARY KEY,"
+ "amount INTEGER DEFAULT 0,"
+ "status TEXT DEFAULT 'normal')");
// 兼容旧表
try {
st.execute("ALTER TABLE bonds "
+ "ADD COLUMN status "
+ "TEXT DEFAULT 'normal'");
} catch (SQLException ignored) {}
// 旧流水(保留兼容)
st.execute("CREATE TABLE IF NOT EXISTS bond_log ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "reason TEXT DEFAULT '',"
+ "time INTEGER DEFAULT 0)");
// 新交易流水表
st.execute("CREATE TABLE IF NOT EXISTS "
+ "bond_transaction ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "type TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "target_player TEXT DEFAULT '',"
+ "operator TEXT DEFAULT '',"
+ "reason TEXT DEFAULT '',"
+ "balance_after INTEGER DEFAULT 0,"
+ "time INTEGER DEFAULT 0)");
try {
st.execute("CREATE INDEX "
+ "IF NOT EXISTS idx_tx_pt "
+ "ON bond_transaction("
+ "player_name, time)");
} catch (SQLException ignored) {}
try {
st.execute("ALTER TABLE bond_transaction "
+ "ADD COLUMN balance_before "
+ "INTEGER DEFAULT 0");
} catch (SQLException ignored) {}
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[Bond] DB init failed: "
+ e.getMessage(), e);
}
}
// ======================== 账户状态 ========================
public boolean isFrozen(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return "frozen".equals(s);
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return false;
}
public String getAccountStatus(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return s != null ? s : "normal";
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return "normal";
}
public void freezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='frozen' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
public void unfreezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='normal' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
private void ensureAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,0,'normal')");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
// ======================== 余额查询 ========================
public int getBonds(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT amount FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
int v = 0;
if (rs.next()) v = rs.getInt("amount");
rs.close(); ps.close();
return v;
} catch (SQLException e) { return 0; }
}
/** 该玩家是否在 bond 表中有真实记录 */
public boolean hasBondAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM bonds "
+ "WHERE player_name=? "
+ "AND amount > 0");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next();
rs.close(); ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
// ======================== 增加债券 ========================
public int addBonds(String player, int amount) {
return addBondsInternal(player, amount,
TX_ADMIN_GIVE, "", "",
"管理员发放");
}
public int addBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return -1;
return addBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
private int addBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
int after = before + amount;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status)"
+ " VALUES (?,?, 'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, after);
ps.setInt(3, after);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, amount);
return after;
}
// ======================== 扣除债券 ========================
private boolean deductBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
if (before < amount) return false;
int after = before - amount;
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET amount=? "
+ "WHERE player_name=?");
ps.setInt(1, after);
ps.setString(2, player);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, -amount);
return true;
}
// ======================== 流水写入 ========================
private void logTransaction(String player,
String type,
int amount,
String targetPlayer,
String operator,
String reason,
int balanceBefore,
int balanceAfter) {
try {
// 合并模式:shop_buy 30秒内同商品合并
if ("shop_buy".equals(type)
&& reason != null
&& !reason.isEmpty()) {
long mergeWindow =
System.currentTimeMillis() - 30000L;
PreparedStatement chk =
db.prepareStatement(
"SELECT id, amount "
+ "FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND type=? "
+ "AND reason=? "
+ "AND time > ? "
+ "ORDER BY time DESC "
+ "LIMIT 1");
chk.setString(1, player);
chk.setString(2, type);
chk.setString(3, reason);
chk.setLong(4, mergeWindow);
ResultSet rs = chk.executeQuery();
if (rs.next()) {
int existId = rs.getInt("id");
int existAmt = rs.getInt("amount");
rs.close();
chk.close();
PreparedStatement upd =
db.prepareStatement(
"UPDATE bond_transaction "
+ "SET amount=?, "
+ "balance_after=?, "
+ "time=? "
+ "WHERE id=?");
upd.setInt(1, existAmt + amount);
upd.setInt(2, balanceAfter);
upd.setLong(3,
System.currentTimeMillis());
upd.setInt(4, existId);
upd.executeUpdate();
upd.close();
return;
}
rs.close();
chk.close();
}
// 原始插入
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_transaction "
+ "(player_name, type, amount,"
+ " target_player, operator,"
+ " reason, balance_before,"
+ " balance_after, time)"
+ " VALUES (?,?,?,?,?,?,?,?,?)");
ps.setString(1, player);
ps.setString(2, type);
ps.setInt(3, amount);
ps.setString(4,
targetPlayer != null ? targetPlayer : "");
ps.setString(5,
operator != null ? operator : "");
ps.setString(6,
reason != null ? reason : "");
ps.setInt(7, balanceBefore);
ps.setInt(8, balanceAfter);
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ======================== 扣除债券 ========================
/** 完整版:含冻结检查 + 流水记录 */
public boolean deductBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return false;
return deductBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
public boolean checkoutCart(String player,
String[] itemNames,
int[] subtotals) {
int total = 0;
for (int s : subtotals) total += s;
if (isFrozen(player)) return false;
int before = getBonds(player);
if (before < total) return false;
int after = before - total;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,?,'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, after);
ps.setInt(3, after);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
int running = before;
for (int i = 0; i < itemNames.length; i++) {
int next = running - subtotals[i];
logTransaction(player, "shop_buy", subtotals[i],
"", "商店系统",
"购物车购买-" + itemNames[i],
running, next);
running = next;
}
logLegacy(player, -total);
return true;
}
public boolean setBonds(String player, int amount) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,?, 'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, amount);
ps.setInt(3, amount);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return true;
}
// ======================== 转账 ========================
public TransferResult transfer(String from,
String to,
int amount) {
if (from.equalsIgnoreCase(to))
return new TransferResult(false,
"不能给自己转账");
if (isFrozen(from))
return new TransferResult(false,
"你的账户已被冻结");
if (isFrozen(to))
return new TransferResult(false,
to + " 的账户已被冻结");
if (isSameIP(from, to))
return new TransferResult(false,
"不能与同IP玩家转账");
if (!deductBondsInternal(from, amount,
TX_TRANSFER_OUT, to, from,
"转账给" + to))
return new TransferResult(false, "债券不足");
addBondsInternal(to, amount,
TX_TRANSFER_IN, from, from,
"收到来自" + from + "的转账");
return new TransferResult(true, "转账成功");
}
// ======================== 同IP检测 ========================
public boolean isSameIP(String p1, String p2) {
String ip1 = getPlayerIPAny(p1);
String ip2 = getPlayerIPAny(p2);
if (ip1 == null || ip2 == null) return false;
return ip1.equals(ip2);
}
/** 获取同IP下的所有其他在线玩家 */
public List<String> getSameIPPlayers(String player) {
String ip = getPlayerIPAny(player);
if (ip == null) return Collections.emptyList();
List<String> list = new ArrayList<>();
for (Player p :
plugin.getServer().getOnlinePlayers()) {
String pIp = plugin.getPlayerIP(p);
if (pIp != null && pIp.equals(ip)
&& !p.getName()
.equalsIgnoreCase(player)) {
list.add(p.getName());
}
}
return list;
}
private String getPlayerIPAny(String player) {
// 在线:直接取
Player p = plugin.getServer()
.getPlayerExact(player);
if (p != null) {
String ip = plugin.getPlayerIP(p);
if (ip != null) return ip;
}
// 离线:查主库
try {
Object v = plugin.getDb()
.getField(player, "last_login_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
try {
Object v = plugin.getDb()
.getField(player, "register_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
return null;
}
// ======================== 流水查询 ========================
public List<Map<String, Object>> getTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
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_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
// 放在 getTransactions 方法后面:
/**
* 获取该玩家全部流水(不限时间,供打印使用)
*/
public List<Map<String, Object>>
getAllTransactions(String player) {
List<Map<String, Object>> list =
new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "ORDER BY time ASC");
ps.setString(1, player);
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_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
// 放在 getAllTransactions 方法后面:
/**
* 获取所有有流水记录的玩家名(去重)
*/
public List<String> getAllPlayerNames() {
List<String> list = new ArrayList<>();
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT DISTINCT player_name "
+ "FROM bond_transaction "
+ "ORDER BY player_name ASC");
while (rs.next()) {
list.add(rs.getString("player_name"));
}
rs.close(); st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public List<Map<String, Object>>
getTransferTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "AND (type=? OR type=?) "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ps.setString(3, TX_TRANSFER_OUT);
ps.setString(4, TX_TRANSFER_IN);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("type", rs.getString("type"));
row.put("amount",
rs.getInt("amount"));
row.put("target_player",
rs.getString("target_player"));
row.put("reason",
rs.getString("reason"));
row.put("balance_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
private void logLegacy(String player, int amount) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_log "
+ "(player_name,amount,time) "
+ "VALUES (?,?,?)");
ps.setString(1, player);
ps.setInt(2, amount);
ps.setLong(3,
System.currentTimeMillis());
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
public Map<String, Object> getLog(
String player, int limit) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("balance", getBonds(player));
return info;
}
// ======================== 转账结果 ========================
public static class TransferResult {
public final boolean success;
public final String message;
public TransferResult(boolean success,
String message) {
this.success = success;
this.message = message;
}
}
}
@@ -1,378 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
public class BondPrinter {
private final Main plugin;
private final File outputDir;
private static final SimpleDateFormat FILE_SDF =
new SimpleDateFormat(
"yyyy-MM-dd_HH-mm-ss");
private static final SimpleDateFormat CELL_SDF =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
private static final LinkedHashMap<String, String>
TYPE_NAMES = new LinkedHashMap<>();
static {
TYPE_NAMES.put("admin_give", "管理员发放");
TYPE_NAMES.put("admin_deduct", "管理员扣除");
TYPE_NAMES.put("transfer_out", "转出");
TYPE_NAMES.put("transfer_in", "转入");
TYPE_NAMES.put("redeem", "CDK兑换");
TYPE_NAMES.put("shop_buy", "商城购买");
TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上
TYPE_NAMES.put("daily_sign", "签到奖励");
TYPE_NAMES.put("freeze", "冻结");
TYPE_NAMES.put("unfreeze", "解冻");
}
public BondPrinter(Main plugin) {
this.plugin = plugin;
this.outputDir = new File(
plugin.getDataFolder(), "printer");
if (!this.outputDir.exists()) {
this.outputDir.mkdirs();
}
}
// ============ 打印单个玩家 ============
public void printPlayer(String player,
CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doPrintPlayer(
player, sender));
}
private void doPrintPlayer(String player,
CommandSender sender) {
try {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(player);
String fileName = player + "_"
+ FILE_SDF.format(new Date())
+ ".xls";
File file = new File(outputDir, fileName);
deleteOld(player);
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(file),
StandardCharsets.UTF_8));
try {
writeXmlHeader(pw,
escXml(player));
writeXmlRow(pw,
new String[]{
"时间", "类型",
"金额", "对象",
"操作者", "理由",
"操作前余额",
"操作后余额"});
for (Map<String, Object> tx : txs) {
writeXmlRow(pw, new String[]{
txTime(tx),
txTypeName(tx),
String.valueOf(
txInt(tx,
"amount")),
txStr(tx,
"target_player"),
txStr(tx, "operator"),
txStr(tx, "reason"),
String.valueOf(
txInt(tx,
"balance_before")),
String.valueOf(
txInt(tx,
"balance_after"))
});
}
writeXmlFooter(pw);
pw.flush();
} finally {
pw.close();
}
if (sender != null) {
sender.sendMessage("§a[打印] §f"
+ player + " §a流水已导出");
sender.sendMessage("§7文件: §f"
+ fileName);
sender.sendMessage("§7共 §f"
+ txs.size()
+ " §7条,§724小时后删除");
}
} catch (Exception e) {
if (sender != null) {
sender.sendMessage("§c导出失败: "
+ e.getMessage());
}
plugin.getLogger().severe(
"[Printer] " + e.getMessage());
}
}
// ============ 打印全服 ============
public void printAll(CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doPrintAll(sender));
}
private void doPrintAll(CommandSender sender) {
try {
List<String> players =
plugin.getBonds()
.getAllPlayerNames();
if (players.isEmpty()) {
if (sender != null) {
sender.sendMessage("§c没有流水数据");
}
return;
}
String fileName = "全服流水_"
+ FILE_SDF.format(new Date())
+ ".xls";
File file = new File(outputDir, fileName);
// ===== 收集所有流水 =====
// key = 时间戳+玩家+金额,用于去重
// value = 一行数据
List<String[]> allRows = new ArrayList<>();
Set<String> seenKeys = new HashSet<>();
for (String player : players) {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(player);
for (Map<String, Object> tx : txs) {
int txId = ((Number) tx.get("id"))
.intValue();
String dedupKey =
String.valueOf(txId);
if (!seenKeys.add(dedupKey)) {
continue;
}
String timeStr = txTime(tx);
long timeMs = 0;
Object t = tx.get("time");
if (t != null) {
timeMs =
((Number) t).longValue();
}
allRows.add(new String[]{
player,
timeStr,
txTypeName(tx),
String.valueOf(
txInt(tx, "amount")),
txStr(tx, "target_player"),
txStr(tx, "operator"),
txStr(tx, "reason"),
String.valueOf(
txInt(tx,
"balance_before")),
String.valueOf(
txInt(tx,
"balance_after")),
// 额外存时间戳用于排序
String.valueOf(timeMs)
});
}
}
// ===== 按时间戳排序 =====
allRows.sort((a, b) -> {
long ta = Long.parseLong(
a[a.length - 1]);
long tb = Long.parseLong(
b[b.length - 1]);
return Long.compare(ta, tb);
});
// ===== 写入文件 =====
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(file),
StandardCharsets.UTF_8));
try {
writeXmlHeader(pw, "全服流水");
writeXmlRow(pw, new String[]{
"玩家", "时间", "类型",
"金额", "对象", "操作者",
"理由", "操作前余额",
"操作后余额"
});
for (String[] row : allRows) {
// 去掉最后的时间戳列
String[] cells =
new String[row.length - 1];
System.arraycopy(row, 0,
cells, 0, cells.length);
writeXmlRow(pw, cells);
}
writeXmlFooter(pw);
pw.flush();
if (sender != null) {
sender.sendMessage(
"§a[打印] §f全服流水 §a已导出");
sender.sendMessage("§7文件: §f"
+ fileName);
sender.sendMessage("§7共 §f"
+ players.size()
+ " 名玩家,§f"
+ allRows.size() + " §7条");
sender.sendMessage(
"§7按时间从早到晚排序");
sender.sendMessage(
"§724小时后自动删除");
}
} finally {
pw.close();
}
} catch (Exception e) {
if (sender != null) {
sender.sendMessage("§c导出失败: "
+ e.getMessage());
}
plugin.getLogger().severe(
"[Printer] " + e.getMessage());
}
}
// ============ 清理 ============
public void cleanupOldFiles() {
long now = System.currentTimeMillis();
long maxAge = 24L * 60 * 60 * 1000;
File[] files = outputDir.listFiles(
(d, n) -> n.endsWith(".xls"));
if (files == null) {
return;
}
int count = 0;
for (File f : files) {
if (now - f.lastModified() > maxAge) {
if (f.delete()) {
count++;
}
}
}
if (count > 0) {
plugin.getLogger().info(
"[Printer] 清理 "
+ count + " 个过期文件");
}
}
private void deleteOld(String player) {
File[] old = outputDir.listFiles(
(d, n) -> n.startsWith(player + "_")
&& n.endsWith(".xls"));
if (old != null) {
for (File f : old) {
f.delete();
}
}
}
// ============ XML工具 ============
private void writeXmlHeader(PrintWriter pw,
String sheetName) {
pw.println("<?xml version=\"1.0\"?>");
pw.println("<?mso-application "
+ "progid=\"Excel.Sheet\"?>");
pw.println("<Workbook xmlns="
+ "\"urn:schemas-microsoft-com"
+ ":office:spreadsheet\"");
pw.println(" xmlns:ss="
+ "\"urn:schemas-microsoft-com"
+ ":office:spreadsheet\">");
pw.println(" <Worksheet ss:Name=\""
+ sheetName + "\">");
pw.println(" <Table>");
}
private void writeXmlRow(PrintWriter pw,
String[] cells) {
pw.println(" <Row>");
for (String cell : cells) {
pw.println(" <Cell>"
+ "<Data ss:Type=\"String\">"
+ escXml(cell != null ? cell : "")
+ "</Data></Cell>");
}
pw.println(" </Row>");
}
private void writeXmlFooter(PrintWriter pw) {
pw.println(" </Table>");
pw.println(" </Worksheet>");
pw.println("</Workbook>");
}
private String escXml(String val) {
if (val == null) {
return "";
}
return val.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
private String txTime(Map<String, Object> tx) {
Object t = tx.get("time");
if (t == null) return "";
long ms = ((Number) t).longValue();
return CELL_SDF.format(new Date(ms));
}
private String txTypeName(
Map<String, Object> tx) {
String type = (String) tx.get("type");
return TYPE_NAMES.getOrDefault(type, type);
}
private int txInt(Map<String, Object> tx,
String key) {
Object v = tx.get(key);
if (v == null) return 0;
return ((Number) v).intValue();
}
private String txStr(Map<String, Object> tx,
String key) {
Object v = tx.get(key);
return v != null ? v.toString() : "";
}
}
@@ -1,293 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.charset.StandardCharsets;
public class CDKManager {
private final Main plugin;
private Connection db;
private final Map<String, CDKInput> pendingInputs
= new ConcurrentHashMap<>();
public CDKManager(Main plugin) {
this.plugin = plugin;
initDB();
}
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
st.execute("CREATE TABLE IF NOT EXISTS cdk ("
+ "code TEXT PRIMARY KEY,"
+ "amount INTEGER DEFAULT 0,"
+ "type TEXT DEFAULT 'bond',"
+ "used INTEGER DEFAULT 0,"
+ "used_by TEXT DEFAULT '',"
+ "created_time INTEGER DEFAULT 0,"
+ "used_time INTEGER DEFAULT 0)");
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[CDK] DB init failed: "
+ e.getMessage(), e);
}
}
// ===== CDK兑换(供SDF1调用) =====
/**
* 兑换CDK,返回结果字符串
* @return "success:金额:余额前:余额后"
* 或 "fail:原因"
*/
public String redeem(String code, String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT amount,type,used FROM cdk "
+ "WHERE code=?");
ps.setString(1, code);
ResultSet rs = ps.executeQuery();
if (!rs.next()) {
rs.close(); ps.close();
return "fail:not_found";
}
int amount = rs.getInt("amount");
String type = rs.getString("type");
int used = rs.getInt("used");
rs.close(); ps.close();
if (used == 1)
return "fail:already_used";
if (plugin.getBonds()
.isFrozen(player))
return "fail:frozen";
PreparedStatement up = db.prepareStatement(
"UPDATE cdk SET used=1,"
+ "used_by=?,used_time=? "
+ "WHERE code=?");
up.setString(1, player);
up.setLong(2,
System.currentTimeMillis());
up.setString(3, code);
up.executeUpdate(); up.close();
if ("bond".equals(type)) {
int bef = plugin.getBonds()
.getBonds(player);
plugin.getBonds().addBonds(player,
amount,
BondManager.TX_REDEEM,
"", "CDK系统",
"CDK兑换: " + code);
int aft = plugin.getBonds()
.getBonds(player);
return "success:" + amount
+ ":" + bef + ":" + aft;
}
return "fail:unknown_type";
} catch (SQLException e) {
return "fail:" + e.getMessage();
}
}
// ===== 聊天输入系统 =====
public void requestInput(Player p,
String type,
String context) {
CDKInput input = new CDKInput();
input.type = type;
input.context = context;
input.time = System.currentTimeMillis();
pendingInputs.put(p.getName(), input);
}
public boolean isListening(String playerName) {
return pendingInputs
.containsKey(playerName);
}
public void onChat(Player p, String msg) {
CDKInput input = pendingInputs
.remove(p.getName());
if (input == null) return;
if (System.currentTimeMillis()
- input.time > 60000) {
p.sendMessage("§c输入超时");
return;
}
if ("cdk".equals(input.type)) {
String result = redeem(
msg.trim(), p.getName());
if (result != null
&& result.startsWith("success:")) {
String[] parts = result.split(":");
int amt = Integer.parseInt(parts[1]);
int bef = Integer.parseInt(parts[2]);
int aft = Integer.parseInt(parts[3]);
p.sendMessage("§e§l[CDK] §f兑换成功!"
+ "获得 §c" + amt + "§f债券");
p.sendMessage("§7余额: §e" + bef
+ " §7-> §a" + aft);
} else {
p.sendMessage("§c§l[CDK] §f"
+ (result != null
? result.replace("fail:", "")
: "无效的兑换码"));
}
return;
}
if ("econ".equals(input.type)) {
handleEconInput(p, msg.trim(),
input.context);
return;
}
if ("bond".equals(input.type)) {
handleBondInput(p, msg.trim(),
input.context);
}
}
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格式错误");
}
}
private void handleBondInput(Player admin,
String msg,
String target) {
if (!msg.startsWith("+")
&& !msg.startsWith("-")) {
admin.sendMessage("§c格式: §a+100"
+ " §7或 §c-50");
return;
}
try {
String sign = msg.substring(0, 1);
String rest =
msg.substring(1).trim();
String[] parts =
rest.split("\\s+", 2);
int amount = Integer.parseInt(parts[0]);
String reason = parts.length >= 2
? parts[1].trim()
: "管理员手动调整";
if (amount <= 0) {
admin.sendMessage("§c金额必须>0");
return;
}
String opName = admin.getName();
String txType = "-".equals(sign)
? BondManager.TX_ADMIN_DEDUCT
: BondManager.TX_ADMIN_GIVE;
if ("-".equals(sign)) {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c账户冻结");
return;
}
if (plugin.getBonds().deductBonds(
target, amount, txType,
"", opName, reason)) {
admin.sendMessage("§a扣除成功");
} else {
admin.sendMessage("§c债券不足");
}
} else {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c账户冻结");
return;
}
plugin.getBonds().addBonds(target,
amount, txType,
"", opName, reason);
admin.sendMessage("§a发放成功");
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误");
}
}
public void cancelInput(String player) {
pendingInputs.remove(player);
}
static class CDKInput {
String type;
String context;
long time;
}
}
@@ -1,612 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ChatFilterManager {
private final JavaPlugin plugin;
private final List<String> whitelistUrls =
new ArrayList<>();
private final Set<String> whitelistPlayers =
new TreeSet<>(
String.CASE_INSENSITIVE_ORDER);
private final Map<Integer, String>
punishmentRules =
new LinkedHashMap<>();
private final Map<Integer, Integer>
punishmentDurations =
new LinkedHashMap<>();
private final Map<String, Integer>
violationCount =
new ConcurrentHashMap<>();
private final Map<String, Long>
mutedPlayers =
new ConcurrentHashMap<>();
private final Map<String, String> messages =
new LinkedHashMap<>();
private boolean notifyAdmin = true;
private boolean notifyAll = false;
private int muteDuration = 300;
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean v) {
enabled = v;
}
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*)?");
private static final Pattern DOT_VARIANTS =
Pattern.compile(
"[\uff0e\u3002\u2025\u2026\u00b7]");
private static final Pattern COLOR_CODES =
Pattern.compile(
"\u00a7[0-9a-fk-orA-FK-OR]");
private static final Set<String> FILE_EXT;
static {
Set<String> s = new HashSet<>(
Arrays.asList(
"txt", "yml", "yaml",
"json", "xml", "java",
"class", "jar", "png",
"jpg", "jpeg", "gif",
"bmp", "svg", "ico",
"webp", "mp3", "mp4",
"avi", "mkv", "wav",
"flac", "ogg", "zip",
"rar", "7z", "tar",
"gz", "cfg", "conf",
"ini", "log", "db",
"sql", "sh", "bat",
"html", "htm", "css",
"js", "md", "pdf",
"dll", "so", "exe",
"bin", "c", "cpp",
"py", "rb", "go",
"rs", "kt", "lua",
"php", "asp", "jsp"));
FILE_EXT = Collections.unmodifiableSet(s);
}
public ChatFilterManager(JavaPlugin plugin) {
this.plugin = plugin;
}
// ========== 配置加载 ==========
public void loadConfig() {
whitelistUrls.clear();
whitelistPlayers.clear();
punishmentRules.clear();
punishmentDurations.clear();
messages.clear();
File f = new File(
plugin.getDataFolder(), "chat.txt");
if (!f.exists()) writeDefaultFile(f);
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8))) {
String section = "";
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("\uFEFF"))
line = line.substring(1);
String t = line.trim();
if (t.isEmpty()
|| t.startsWith("#"))
continue;
if (t.equals("白名单")
|| t.equals("白名单:")) {
section = "whitelist";
continue;
}
if (t.equals("处罚规则")
|| t.equals("处罚规则:")) {
section = "punishment";
continue;
}
if (t.equals("白名单玩家")
|| t.equals("白名单玩家:")) {
section = "players";
continue;
}
if (t.contains(":")) {
String[] kv = t.split(":", 2);
String k = kv[0].trim();
String v = kv[1].trim();
if (equals("启用过滤")
|| equals("enabled")) {
enabled = parseBool(v);
continue;
}
if (k.equals("通知管理员")) {
notifyAdmin = parseBool(v);
continue;
}
if (k.equals("全服通报")) {
notifyAll = parseBool(v);
continue;
}
if (k.equals("禁言时长")) {
try {
muteDuration =
Integer.parseInt(v);
} catch (Exception ignored) {
}
continue;
}
}
switch (section) {
case "whitelist":
whitelistUrls.add(
t.toLowerCase());
break;
case "punishment":
String[] parts = t.split(":");
if (parts.length >= 2) {
try {
int cnt =
Integer.parseInt(
parts[0]
.trim());
String type =
parts[1].trim()
.toLowerCase();
int dur =
parts.length >= 3
? Integer.parseInt(
parts[2]
.trim())
: muteDuration;
punishmentRules.put(
cnt, type);
punishmentDurations.put(
cnt, dur);
} catch (Exception ignored) {
}
}
break;
case "players":
whitelistPlayers.add(t);
break;
}
}
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_chat] 配置加载失败: "
+ e.getMessage());
}
// 从消息.txt读取chat_开头的消息
loadMessages();
}
private void loadMessages() {
File msgFile = new File(
plugin.getDataFolder(),
"消息.txt");
if (!msgFile.exists()) return;
try (BufferedReader mr =
new BufferedReader(
new InputStreamReader(
new FileInputStream(
msgFile),
StandardCharsets.UTF_8))) {
String mline;
while ((mline = mr.readLine()) != null) {
mline = mline.trim();
if (mline.isEmpty()
|| mline.startsWith("#"))
continue;
// 支持 = 和 : 两种分隔符
int eqIdx = mline.indexOf('=');
int coIdx = mline.indexOf(':');
int splitIdx = -1;
if (eqIdx >= 0 && coIdx >= 0) {
splitIdx = Math.min(eqIdx, coIdx);
} else if (eqIdx >= 0) {
splitIdx = eqIdx;
} else if (coIdx >= 0) {
splitIdx = coIdx;
}
if (splitIdx < 0) continue;
String k = mline.substring(0,
splitIdx).trim();
String v = mline.substring(
splitIdx + 1).trim();
if (k.startsWith("chat_")) {
messages.put(k, v);
}
}
} catch (Exception ignored) {
}
}
private void writeDefaultFile(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# Sdf1_chat 聊天过滤配置");
pw.println("启用过滤: " + enabled);
pw.println("通知管理员: true");
pw.println("全服通报: false");
pw.println("禁言时长: 300");
pw.println();
pw.println("白名单:");
pw.println("*.minecraft.net");
pw.println("baidu.com");
pw.println("github.com");
pw.println();
pw.println("处罚规则:");
pw.println("1:warn:0");
pw.println("2:warn:0");
pw.println("3:mute:300");
pw.println("5:mute:600");
pw.println("8:kick:0");
pw.println("10:ban:0");
pw.println();
pw.println("白名单玩家:");
} catch (IOException ignored) {
}
}
public void saveConfig() {
File f = new File(
plugin.getDataFolder(), "chat.txt");
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# Sdf1_chat 聊天过滤配置");
pw.println("通知管理员: " + notifyAdmin);
pw.println("全服通报: " + notifyAll);
pw.println("禁言时长: " + muteDuration);
pw.println();
pw.println("白名单:");
for (String u : whitelistUrls)
pw.println(u);
pw.println();
pw.println("处罚规则:");
for (Map.Entry<Integer, String> entry
: punishmentRules.entrySet()) {
int dur = punishmentDurations
.getOrDefault(entry.getKey(),
muteDuration);
pw.println(entry.getKey() + ":"
+ entry.getValue()
+ ":" + dur);
}
pw.println();
pw.println("白名单玩家:");
for (String p : whitelistPlayers)
pw.println(p);
} catch (IOException e) {
plugin.getLogger().severe(
"[Sdf1_chat] 保存失败: "
+ e.getMessage());
}
}
// ========== 消息 ==========
public String msg(String key,
String... args) {
String template = messages.getOrDefault(
key, key);
for (int i = 0; i < args.length - 1;
i += 2) {
template = template.replace(
"{" + args[i] + "}",
args[i + 1]);
}
return template;
}
// ========== URL检测 ==========
private String normalizeMessage(String msg) {
msg = COLOR_CODES.matcher(msg)
.replaceAll("");
msg = DOT_VARIANTS.matcher(msg)
.replaceAll(".");
return msg;
}
public List<String> extractUrls(
String message) {
String normalized =
normalizeMessage(message);
List<String> urls = new ArrayList<>();
Matcher m = URL_PATTERN
.matcher(normalized);
while (m.find()) {
String url = m.group().toLowerCase();
url = url.replaceAll(
"[\\s.,;!?,。;!?、)》」』】"
+ "\\]\\[\\((]+$",
"");
if (!url.isEmpty()) urls.add(url);
}
return urls;
}
public boolean isWhitelisted(String url) {
String clean = url
.replaceAll("^https?://", "")
.toLowerCase();
int colon = clean.indexOf(':');
int slash = clean.indexOf('/');
if (colon > 0 && (slash < 0
|| colon < slash)) {
clean = clean.substring(0, colon);
}
slash = clean.indexOf('/');
if (slash > 0) {
clean = clean.substring(0, slash);
}
for (String entry : whitelistUrls) {
String e = entry.toLowerCase().trim();
if (e.startsWith("*.")) {
String suffix = e.substring(1);
if (clean.endsWith(suffix)
|| clean.equals(
suffix.substring(1)))
return true;
} else {
if (clean.equals(e)
|| clean.endsWith(
"." + e))
return true;
}
}
return false;
}
public boolean isLikelyDomain(String url) {
int lastDot = url.lastIndexOf('.');
if (lastDot < 0
|| lastDot >= url.length() - 1)
return false;
String ext = url.substring(lastDot + 1)
.toLowerCase();
return !FILE_EXT.contains(ext);
}
// ========== 玩家操作 ==========
public boolean isMuted(String name) {
Long expiry = mutedPlayers.get(name);
if (expiry == null) return false;
if (System.currentTimeMillis()
>= expiry) {
mutedPlayers.remove(name);
return false;
}
return true;
}
public String fmtDuration(int seconds) {
if (seconds <= 0) return "0秒";
int day = seconds / 86400;
int hr = (seconds % 86400) / 3600;
int min = (seconds % 3600) / 60;
int sec = seconds % 60;
StringBuilder sb = new StringBuilder();
if (day > 0) sb.append(day).append("天");
if (hr > 0)
sb.append(hr).append("小时");
if (min > 0)
sb.append(min).append("分");
if (sec > 0 || sb.length() == 0)
sb.append(sec).append("秒");
return sb.toString();
}
public void applyPunishment(Player player,
int violation) {
String type = "warn";
int duration = muteDuration;
int maxRule = 0;
for (Map.Entry<Integer, String> entry
: punishmentRules.entrySet()) {
if (violation >= entry.getKey()
&& entry.getKey() > maxRule) {
maxRule = entry.getKey();
type = entry.getValue();
duration = punishmentDurations
.getOrDefault(
entry.getKey(),
muteDuration);
}
}
if (type.equals("mute")) {
if (isMuted(player.getName()))
return;
mutedPlayers.put(player.getName(),
System.currentTimeMillis()
+ (long) duration
* 1000L);
}
final String fType = type;
final int fDur = duration;
final int fV = violation;
final String fName = player.getName();
Bukkit.getScheduler().runTask(plugin,
() -> {
Player p = Bukkit.getPlayer(fName);
if (p == null) return;
switch (fType) {
case "warn":
// 警告不处罚,只提示
break;
case "mute":
p.sendMessage(
msg("chat_muted"));
break;
case "kick":
p.kickPlayer(
msg("chat_muted"));
break;
case "ban":
Bukkit.dispatchCommand(
Bukkit.getConsoleSender(),
"ban " + fName
+ " §c多次发送违规链接");
break;
case "banip":
InetSocketAddress addr =
p.getAddress();
if (addr != null) {
String ip = addr
.getAddress()
.getHostAddress();
Bukkit.dispatchCommand(
Bukkit.getConsoleSender(),
"ban-ip " + ip
+ " §c发送违规链接");
}
break;
}
});
}
public void unmutePlayer(String name) {
mutedPlayers.remove(name);
}
public void resetPlayer(String name) {
mutedPlayers.remove(name);
violationCount.remove(name);
}
public boolean isPlayerWhitelisted(
String name) {
return whitelistPlayers.contains(name);
}
public boolean isNotifyAdmin() {
return notifyAdmin;
}
public boolean isNotifyAll() {
return notifyAll;
}
public int getViolationCount(String name) {
return violationCount
.getOrDefault(name, 0);
}
public void incrementViolation(
String name) {
violationCount.put(name,
getViolationCount(name) + 1);
}
public List<String> getWhitelistUrls() {
return whitelistUrls;
}
public Set<String> getWhitelistPlayers() {
return whitelistPlayers;
}
public Map<Integer, String>
getPunishmentRules() {
return punishmentRules;
}
public Map<Integer, Integer>
getPunishmentDurations() {
return punishmentDurations;
}
public Map<String, Long>
getMutedPlayers() {
return mutedPlayers;
}
public void setNotifyAdmin(boolean v) {
notifyAdmin = v;
}
public void setNotifyAll(boolean v) {
notifyAll = v;
}
public void setMuteDuration(int v) {
muteDuration = v;
}
public void addUrl(String url) {
whitelistUrls.add(
url.toLowerCase().trim());
saveConfig();
}
public boolean removeUrl(String url) {
boolean r = whitelistUrls.remove(
url.toLowerCase().trim());
saveConfig();
return r;
}
public void addWhitelistPlayer(
String name) {
whitelistPlayers.add(name);
saveConfig();
}
public boolean removeWhitelistPlayer(
String name) {
boolean r =
whitelistPlayers.remove(name);
saveConfig();
return r;
}
public void cleanExpired() {
long now = System.currentTimeMillis();
mutedPlayers.entrySet().removeIf(
e -> e.getValue() <= now);
}
private boolean parseBool(String v) {
String s = v.toLowerCase().trim();
return s.equals("true")
|| s.equals("on")
|| s.equals("yes")
|| s.equals("1")
|| s.contains("开启")
|| s.contains("启用")
|| s.contains("是");
}
}
@@ -1,842 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class ChatInputManager {
public enum InputType {
NONE,
CHANGE_PWD_STEP1,
CHANGE_PWD_STEP2,
SET_EMAIL,
SET_EMAIL_VERIFY,
ADMIN_AUTH,
ADMIN_SET_POINTS,
ADMIN_SET_PWD,
ADMIN_DELETE_CONFIRM,
ADMIN_SET_AFK_TIME,
SMTP_CONFIG,
SMTP_VERIFY,
BACK_CHECKIN,
INVITE_INPUT_CODE,
TICKET_TITLE,
TICKET_DESC,
TICKET_SCORE,
TICKET_REJECT,
TICKET_ASSIGN,
TICKET_INVITE_PROVIDER,
TICKET_REMOVE_PROVIDER,
TICKET_COMPLETE,
TICKET_REPORT_ISSUE,
TICKET_REPLY,
TICKET_ADMIN_TYPE,
TICKET_ADMIN_TITLE,
TICKET_ADMIN_DESC,
TICKET_ADMIN_PROVIDER,
BACK_CHECK_ECONOMY_CONFIRM,
USER_MGMT_SEARCH,
}
public static class PlayerState {
public InputType type = InputType.NONE;
public String targetPlayer = "";
public String ticketType = "";
public String ticketTitle = "";
public String ticketDesc = "";
public int ticketId = 0;
public int smtpStep = 0;
public int scoreValue = 0;
}
private final Map<String, PlayerState> states =
new HashMap<>();
private Main plugin;
public PlayerState getState(Player p) {
return states.computeIfAbsent(
p.getName(),
k -> new PlayerState());
}
public void reset(Player p) {
states.remove(p.getName());
}
public boolean isInFlow(Player p) {
PlayerState state = states.get(p.getName());
return state != null
&& state.type != InputType.NONE;
}
public void handleInput(Player p, String msg,
Main mainPlugin) {
if (this.plugin == null)
this.plugin = mainPlugin;
PlayerState state = getState(p);
ConfigManager config =
mainPlugin.getConfig2();
DatabaseManager db = mainPlugin.getDb();
switch (state.type) {
case CHANGE_PWD_STEP1: {
String name = p.getName();
String salt = (String) db.getField(
name, "password_salt");
String hash =
PasswordUtils.hash(msg, salt);
boolean mainOk =
db.checkPassword(name, hash);
if (mainOk) {
state.type =
InputType.CHANGE_PWD_STEP2;
state.ticketTitle = "main";
p.sendMessage(
"§e请输入新密码:");
return;
}
boolean tempOk =
db.checkPasswordOrTemp(
name, hash);
if (tempOk) {
state.type =
InputType.CHANGE_PWD_STEP2;
state.ticketTitle = "temp";
p.sendMessage(
"§e请输入新密码:");
return;
}
p.sendMessage(config.msg(
"password_wrong"));
return;
}
case CHANGE_PWD_STEP2: {
String name = p.getName();
if (!PasswordUtils.validate(msg)) {
p.sendMessage(config.msg(
"password_format_error"));
reset(p);
return;
}
String oldHash =
(String) db.getField(name,
"password_hash");
String oldSalt =
(String) db.getField(name,
"password_salt");
String newSalt =
PasswordUtils.generateSalt();
String newHash =
PasswordUtils.hash(
msg, newSalt);
if (newHash.equals(oldHash)) {
p.sendMessage(config.msg(
"password_same"));
reset(p);
return;
}
mainPlugin.recordPasswordChange(
name, oldHash, oldSalt);
db.setField(name,
"password_hash", newHash);
db.setField(name,
"password_salt", newSalt);
mainPlugin
.getNeedsPasswordChange()
.remove(name);
p.sendMessage(config.msg(
"password_changed"));
reset(p);
return;
}
case SET_EMAIL: {
if (!msg.contains("@")
|| !msg.contains(".")) {
p.sendMessage(config.msg(
"email_format_error"));
reset(p);
return;
}
String oldEmail = (String)
db.getField(p.getName(),
"email");
String emailAddr = msg.trim();
// 已绑定邮箱:验证登录密码
if (oldEmail != null
&& !oldEmail.isEmpty()) {
state.targetPlayer = emailAddr;
state.type =
InputType.SET_EMAIL_VERIFY;
state.ticketTitle =
"need_pwd";
p.sendMessage(
"§e您已绑定邮箱: "
+ oldEmail);
p.sendMessage(
"§e请验证登录密码以确认修改:");
return;
}
// 未绑定邮箱:直接发验证码
String code = String.valueOf(
(int) (Math.random()
* 900000
+ 100000));
state.targetPlayer = emailAddr;
state.ticketTitle = code;
state.type =
InputType.SET_EMAIL_VERIFY;
final String to = emailAddr;
final String c = code;
final Player fp = p;
Bukkit.getScheduler()
.runTaskAsynchronously(
mainPlugin, () -> {
boolean sent = mainPlugin
.getEmail()
.sendVerifyCode(
to, c);
Bukkit.getScheduler()
.runTask(mainPlugin,
() -> {
if (sent) {
fp.sendMessage(
"§a验证码已发送到 "
+ to);
fp.sendMessage(
"§e输入6位验证码(0取消):");
} else {
fp.sendMessage(
"§c发送失败");
reset(fp);
}
});
});
return;
}
case SET_EMAIL_VERIFY: {
if ("0".equals(msg.trim())
|| "取消".equals(
msg.trim())) {
p.sendMessage(
"§c邮箱设置已取消");
reset(p);
return;
}
String expected =
state.ticketTitle;
if (msg.trim().equals(expected)) {
db.setField(p.getName(),
"email",
state.targetPlayer);
p.sendMessage(
"§a邮箱已设置: "
+ state.targetPlayer);
reset(p);
} else {
p.sendMessage(
"§c验证码错误(0取消):");
}
return;
}
case ADMIN_AUTH: {
if (msg.equals(
config.adminPassword)) {
Bukkit.getScheduler().runTask(
mainPlugin, () ->
mainPlugin
.getGui()
.openAdmin(p));
} else {
p.sendMessage(
"§c管理密码错误");
}
reset(p);
return;
}
case ADMIN_SET_POINTS: {
try {
int pts = Integer.parseInt(
msg.trim());
db.setField(
state.targetPlayer,
"points", pts);
p.sendMessage("§a已将 "
+ state.targetPlayer
+ " 积分设为 " + pts);
} catch (
NumberFormatException e) {
p.sendMessage(
"§c请输入有效数字");
}
String tgt =
state.targetPlayer;
Bukkit.getScheduler().runTask(
mainPlugin, () ->
mainPlugin
.getGui()
.openUserDetail(
p, tgt));
reset(p);
return;
}
case ADMIN_SET_PWD: {
if (!PasswordUtils.validate(msg)) {
p.sendMessage(config.msg(
"password_format_error"));
reset(p);
return;
}
String tgt = state.targetPlayer;
String salt =
PasswordUtils
.generateSalt();
String hash =
PasswordUtils.hash(
msg, salt);
db.setField(tgt,
"temp_password", hash);
db.setField(tgt, "temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
db.setField(tgt,
"temp_pw_used", 0);
p.sendMessage("§a已为 " + tgt
+ " 设置临时密码");
Player tp =
Bukkit.getPlayer(tgt);
if (tp != null && tp.isOnline())
tp.kickPlayer(
"§c管理员已设置临时密码,"
+ "请使用临时密码登录");
// 邮件通知
String emailAddr = (String)
db.getField(tgt, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String time =
new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date());
String body = "玩家 " + tgt
+ ":\n您的密码已于 "
+ time
+ " 被管理员重置为临时密码。\n"
+ "如果是本人操作请忽略。\n"
+ "如非本人操作请立即联系管理员。";
final String to = emailAddr;
final String fBody = body;
final String fName = tgt;
Bukkit.getScheduler()
.runTaskAsynchronously(
mainPlugin, () ->
mainPlugin.getEmail()
.sendTempPassword(
to,
fName,
fBody));
}
reset(p);
return;
}
case ADMIN_DELETE_CONFIRM: {
if (msg.equals(
state.targetPlayer)) {
db.deleteUser(
state.targetPlayer);
p.sendMessage("§a已删除 "
+ state.targetPlayer);
Player tgt =
Bukkit.getPlayer(
state.targetPlayer);
if (tgt != null
&& tgt.isOnline())
tgt.kickPlayer(
"§c账号已被删除");
} else {
p.sendMessage(
"§c输入不匹配,已取消");
}
reset(p);
return;
}
case ADMIN_SET_AFK_TIME: {
try {
int minutes =
Integer.parseInt(
msg.trim());
config.afkTimeout =
minutes * 60;
config.saveSettings();
p.sendMessage(
"§a挂机时长已设为 "
+ minutes
+ " 分钟");
} catch (
NumberFormatException e) {
p.sendMessage(
"§c请输入有效数字");
}
reset(p);
return;
}
case SMTP_CONFIG: {
String m = msg.trim();
if ("000".equals(m)
|| "取消取消取消"
.equals(m)) {
p.sendMessage(
"§cSMTP配置已取消");
reset(p);
return;
}
if ("取消".equals(m)) {
p.sendMessage(
"§cSMTP配置已取消");
reset(p);
return;
}
int step = state.smtpStep;
if (step <= 1) {
if (!"0".equals(m))
config.setSmtp(
"smtp地址", m);
state.smtpStep = 2;
p.sendMessage("§e当前端口: "
+ config.getSmtp(
"smtp端口"));
p.sendMessage(
"§e输入端口(0跳过):");
} else if (step == 2) {
if (!"0".equals(m))
config.setSmtp(
"smtp端口", m);
state.smtpStep = 3;
p.sendMessage(
"§e输入发件邮箱:");
} else if (step == 3) {
if (!"0".equals(m))
config.setSmtp(
"smtp账号", m);
state.smtpStep = 4;
p.sendMessage(
"§e输入SMTP密码(0跳过):");
} else if (step == 4) {
if (!"0".equals(m))
config.setSmtp(
"smtp密码", m);
state.smtpStep = 5;
p.sendMessage(
"§e输入接收邮箱(留空发到发件邮箱):");
} else if (step == 5) {
String recv = m;
if (recv.isEmpty()
|| "0".equals(recv))
recv = config.getSmtp(
"smtp账号");
config.setSmtp(
"验证码接收邮箱", recv);
if (!config
.canSendSmtpVerify()) {
p.sendMessage(
"§c发送间隔不足1分钟");
reset(p);
return;
}
String code = config
.generateSmtpVerifyCode();
String addr = config.getSmtp(
"smtp地址");
String acct = config.getSmtp(
"smtp账号");
if (acct.isEmpty()
|| addr.isEmpty()) {
p.sendMessage(
"§c地址或账号为空");
reset(p);
return;
}
config.recordSmtpSendTime();
final String fRecv = recv;
final String fCode = code;
final Player fp = p;
Bukkit.getScheduler()
.runTaskAsynchronously(
mainPlugin, () -> {
Bukkit.getScheduler()
.runTask(
mainPlugin,
() -> fp.sendMessage(
"§e正在发送验证码..."));
boolean sent =
mainPlugin.getEmail()
.sendVerifyCode(
fRecv,
fCode);
final boolean fSent = sent;
Bukkit.getScheduler()
.runTask(
mainPlugin,
() -> {
if (fSent) {
state.type =
InputType
.SMTP_VERIFY;
state.smtpStep = 0;
fp.sendMessage(
"§a验证码已发送到 "
+ fRecv);
fp.sendMessage(
"§e输入6位验证码(0取消):");
} else {
fp.sendMessage(
"§c发送失败,查看控制台");
reset(fp);
}
});
});
}
return;
}
case SMTP_VERIFY: {
if ("0".equals(msg.trim())
|| "取消".equals(
msg.trim())) {
p.sendMessage(
"§cSMTP配置已取消");
reset(p);
return;
}
if (config.checkSmtpVerifyCode(
msg.trim())) {
config.saveSmtp();
config.clearSmtpVerifyCode();
String addr = config.getSmtp(
"smtp地址");
String port = config.getSmtp(
"smtp端口");
String acct = config.getSmtp(
"smtp账号");
String recv = config.getSmtp(
"验证码接收邮箱");
boolean ssl = config.smtpSsl;
p.sendMessage("§a§l=== SMTP配置完成 ===");
p.sendMessage("§7地址: §f" + addr);
p.sendMessage("§7端口: §f" + port);
p.sendMessage("§7账号: §f" + acct);
p.sendMessage("§7接收: §f" + recv);
p.sendMessage("§7加密: §f" + ssl);
p.sendMessage("§a§l====================");
reset(p);
} else {
p.sendMessage(
"§c验证码错误(0取消):");
}
return;
}
case BACK_CHECKIN: {
String result =
mainPlugin.getCheckIn()
.backCheckIn(p, msg);
if (result == null) {
// 等待选择(NEED_CONFIRM)
return;
}
if (result.startsWith(
"NEED_CONFIRM:")) {
String[] parts =
result.split(":");
// 存储到Main的Map
mainPlugin.pendingBackCheck.put(
p.getName(),
parts[1] + ":" + parts[2]);
return;
}
if (result.startsWith("§c")) {
p.sendMessage(result);
} else {
p.sendMessage(result);
}
reset(p);
return;
}
case INVITE_INPUT_CODE: {
String code = msg.trim();
String myName = p.getName();
String myCode =
(String) db.getField(
myName,
"invite_code");
if (code.isEmpty()) {
p.sendMessage(
"§c邀请码不能为空");
reset(p);
return;
}
if (code.equals(myCode)) {
p.sendMessage(
"§c不能用自己邀请码");
reset(p);
return;
}
boolean found = false;
for (Map<String, Object> u :
db.getAllUsers()) {
String uc = (String)
u.getOrDefault(
"invite_code", "");
if (code.equals(uc)) {
String inv = (String)
u.get("player_name");
db.setField(myName,
"invited_by", inv);
db.addPoints(inv, 50);
p.sendMessage(
"§a邀请成功!邀请人: "
+ inv);
Player ip =
Bukkit.getPlayer(inv);
if (ip != null
&& ip.isOnline())
ip.sendMessage("§a"
+ myName
+ " 使用了您的邀请码");
found = true;
break;
}
}
if (!found)
p.sendMessage(
"§c邀请码不存在");
reset(p);
return;
}
case TICKET_TITLE: {
state.ticketTitle = msg;
state.type =
InputType.TICKET_DESC;
p.sendMessage(
"§e请输入工单详细描述:");
return;
}
case TICKET_DESC: {
String tt = state.ticketType;
String tl = state.ticketTitle;
reset(p);
mainPlugin.getTicket()
.submitTicket(
p, tt, tl, msg);
return;
}
case TICKET_SCORE: {
int tid = state.ticketId;
try {
int sc = Integer.parseInt(
msg.trim());
reset(p);
mainPlugin.getTicket()
.confirmAndScore(
p, tid, sc);
} catch (
NumberFormatException e) {
reset(p);
p.sendMessage(
"§c请输入数字1-5");
}
return;
}
case TICKET_REJECT: {
int tid = state.ticketId;
reset(p);
mainPlugin.getTicket()
.rejectTicket(
p, tid, msg);
return;
}
case TICKET_ASSIGN: {
int tid = state.ticketId;
String[] parts = msg.trim()
.split("\\s+");
String prov = parts[0];
int price = 0;
if (parts.length >= 2) {
try {
price = Integer.parseInt(
parts[1]);
} catch (
NumberFormatException e) {
p.sendMessage(
"§c价格格式错误");
}
}
reset(p);
mainPlugin.getTicket()
.assignToProvider(
p, tid, prov,
price);
final int fTid = tid;
Bukkit.getScheduler().runTask(
mainPlugin, () ->
mainPlugin
.getTicket()
.openDetail(
p, fTid));
return;
}
case TICKET_INVITE_PROVIDER: {
String tgt = msg.trim();
reset(p);
if (!db.userExists(tgt)) {
p.sendMessage(
"§c玩家不存在");
return;
}
db.addServiceProvider(
tgt, "waiter");
p.sendMessage("§a已邀请 " + tgt
+ " 成为服务商");
return;
}
case TICKET_REMOVE_PROVIDER: {
String tgt = msg.trim();
reset(p);
if (!db.isServiceProvider(tgt)) {
p.sendMessage("§c" + tgt
+ " 不是服务商");
return;
}
db.removeServiceProvider(tgt);
p.sendMessage("§a已移除 " + tgt);
return;
}
case TICKET_COMPLETE: {
try {
int tid = Integer.parseInt(
msg.trim());
mainPlugin.getTicket()
.completeAsProvider(
p, tid);
} catch (
NumberFormatException e) {
p.sendMessage(
"§c请输入有效工单号");
}
reset(p);
return;
}
case TICKET_REPORT_ISSUE: {
String report = msg.trim();
reset(p);
p.sendMessage("§e异常已上报");
String tag = config.adminTag;
for (Player op :
Bukkit.getOnlinePlayers()) {
if (op.getScoreboardTags()
.contains(tag)) {
op.sendMessage(
"§c§l[工单异常] §f"
+ p.getName()
+ ": " + report);
}
}
return;
}
case TICKET_REPLY: {
int replyTid = state.ticketId;
reset(p);
mainPlugin.getTicket()
.replyTicket(
p, replyTid, msg);
return;
}
case TICKET_ADMIN_TYPE: {
state.ticketType = msg.trim();
state.type =
InputType
.TICKET_ADMIN_TITLE;
p.sendMessage(
"§e请输入工单标题:");
return;
}
case TICKET_ADMIN_TITLE: {
state.ticketTitle = msg;
state.type =
InputType
.TICKET_ADMIN_DESC;
p.sendMessage(
"§e请输入工单描述:");
return;
}
case TICKET_ADMIN_DESC: {
state.targetPlayer = msg;
state.type =
InputType
.TICKET_ADMIN_PROVIDER;
p.sendMessage(
"§e输入服务商名和价格:");
p.sendMessage(
"§7例: playerA 20");
return;
}
case TICKET_ADMIN_PROVIDER: {
String[] parts2 = msg.trim()
.split("\\s+");
String prov2 = parts2[0];
int price2 = 0;
if (parts2.length >= 2) {
try {
price2 = Integer.parseInt(
parts2[1]);
} catch (
NumberFormatException e) {
}
}
String t2 = state.ticketType;
String ti2 = state.ticketTitle;
String d2 = state.targetPlayer;
reset(p);
mainPlugin.getTicket()
.adminCreateAndAssign(
p, t2, ti2,
d2, prov2, price2);
return;
}
default:
reset(p);
return;
}
}
}
@@ -1,302 +0,0 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class CheckInManager {
private final Main plugin;
public CheckInManager(Main plugin) {
this.plugin = plugin;
}
// ===== 今日是否已签到 =====
public boolean isCheckedInToday(
String name) {
String last = (String) plugin.getDb()
.getField(name,
"last_checkin_date");
String today = new SimpleDateFormat(
"yyyy-MM-dd")
.format(new Date());
return today.equals(last);
}
// ===== 签到 =====
// ===== 签到 =====
public String checkIn(Player p) {
String name = p.getName();
if (isCheckedInToday(name)) {
return plugin.getConfig2()
.msg("checkin_already");
}
Object streakObj = plugin.getDb()
.getField(name, "checkin_streak");
int streak = streakObj != null
? ((Number) streakObj).intValue()
: 0;
String lastDate = (String) plugin.getDb()
.getField(name, "last_checkin_date");
String today = new SimpleDateFormat("yyyy-MM-dd")
.format(new Date());
if (lastDate != null && !lastDate.isEmpty()) {
try {
java.util.Date lastD =
new SimpleDateFormat("yyyy-MM-dd")
.parse(lastDate);
java.util.Date todayD =
new SimpleDateFormat("yyyy-MM-dd")
.parse(today);
long diff = todayD.getTime()
- lastD.getTime();
if (diff > 86400000L * 2) {
streak = 0;
} else if (diff > 86400000L) {
streak++;
}
} catch (Exception ignored) {
}
}
if (streak == 0) streak = 1;
else streak++;
double multi = 1.0;
if (streak >= 21) multi = 1.5;
else if (streak >= 14) multi = 1.3;
else if (streak >= 7) multi = 1.1;
int basePoints =
plugin.getConfig2().checkinPoints;
int points = (int) (basePoints * multi);
plugin.getDb().addPoints(name, points);
plugin.getDb().setField(name,
"checkin_streak", streak);
plugin.getDb().setField(name,
"last_checkin_date", today);
Object totalObj = plugin.getDb()
.getField(name, "total_checkin_days");
int total = totalObj != null
? ((Number) totalObj).intValue()
: 0;
plugin.getDb().setField(name,
"total_checkin_days", total + 1);
// ★ 债券盲盒奖励(6参数版,写入流水)★
int bondMin =
plugin.getConfig2().checkinBondMin;
int bondMax =
plugin.getConfig2().checkinBondMax;
if (bondMin <= 0) bondMin = 1;
if (bondMax < bondMin) bondMax = bondMin;
int bondReward = bondMin
+ new java.util.Random()
.nextInt(bondMax - bondMin + 1);
plugin.getBonds().addBonds(name, bondReward,
"daily_sign", "", "签到系统",
"每日签到连续" + streak + "天");
return plugin.getConfig2()
.msg("checkin_success")
.replace("{points}",
String.valueOf(points))
.replace("{streak}",
String.valueOf(streak))
.replace("{multi}",
String.valueOf(multi))
+ " §e+" + bondReward + "债券";
}
// ===== 获取连续天数 =====
public int getStreak(String name) {
Object val = plugin.getDb()
.getField(name,
"checkin_streak");
if (val instanceof Number) {
return ((Number) val)
.intValue();
}
return 0;
}
// 标准化日期
private String normalizeDate(String input) {
String s = input.trim().replace("/", "-");
if (s.matches("\\d{8}")) {
return s.substring(0, 4) + "-"
+ s.substring(4, 6) + "-"
+ s.substring(6, 8);
}
return s;
}
// ===== 补签入口 =====
public String backCheckIn(
Player p, String dateStr) {
String name = p.getName();
String normalized =
normalizeDate(dateStr);
// 严格日期验证
java.text.SimpleDateFormat sdf =
new java.text.SimpleDateFormat(
"yyyy-MM-dd");
sdf.setLenient(false);
java.util.Date target;
try {
target = sdf.parse(normalized);
} catch (Exception ex) {
return "§c无效日期: " + dateStr;
}
String reformat = sdf.format(target);
if (!reformat.equals(normalized)) {
return "§c无效日期: " + dateStr;
}
String today = sdf.format(new Date());
if (normalized.compareTo(today) >= 0) {
return "§c今天不需要补签";
}
String threeDaysAgo = getPastDate(3);
if (normalized.compareTo(
threeDaysAgo) < 0) {
return "§c只能补签3天内"
+ "(最多到 " + threeDaysAgo + ")";
}
Object val = plugin.getDb()
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
boolean alreadyBacked = false;
if (!existing.isEmpty()) {
for (String d : existing.split(",")) {
if (normalizeDate(d.trim())
.equals(normalized)) {
alreadyBacked = true;
break;
}
}
}
if (alreadyBacked) {
return "§c该日期已补签: " + normalized;
}
Object normalCheck =
plugin.getDb().getField(
name, "last_checkin_date");
String lastCheckin = normalCheck != null
? normalCheck.toString() : "";
if (lastCheckin.equals(normalized)) {
return "§c该日期已签到: " + normalized;
}
// ★ 补签消耗:3债券 + 10积分
// 积分不够时:用 9债券(3倍)代替 ★
int bondCost = 3;
int pointCost = 10;
int bondAltCost = 9; // 3倍债券替代
int myBonds = plugin.getBonds()
.getBonds(name);
Object ptsObj = plugin.getDb()
.getField(name, "points");
int myPoints = ptsObj != null
? ((Number) ptsObj).intValue()
: 0;
// 方案A:3债券 + 10积分
boolean canA = myBonds >= bondCost
&& myPoints >= pointCost;
// 方案B:9债券(积分不够时)
boolean canB = myBonds >= bondAltCost
&& myPoints < pointCost;
if (!canA && !canB) {
if (myPoints < pointCost
&& myBonds < bondAltCost) {
return "§c补签费用不足"
+ "(需3债券+10积分,"
+ "或9债券代替)"
+ " 当前: "
+ myBonds + "债券 "
+ myPoints + "积分";
}
if (myBonds < bondCost) {
return "§c债券不足(需"
+ bondCost + "枚)";
}
return "§c积分不足(需"
+ pointCost + "分)";
}
// 执行扣款
if (canA) {
// 方案A:3债券 + 10积分
plugin.getBonds().deductBonds(
name, bondCost,
"admin_deduct", "",
"补签系统", "补签扣款");
plugin.getDb().deductPoints(
name, pointCost);
} else {
// 方案B:9债券代替
plugin.getBonds().deductBonds(
name, bondAltCost,
"admin_deduct", "",
"补签系统", "补签扣款(债券替代)");
}
doBackCheckIn(p, normalized);
String costMsg = canA
? bondCost + "债券+" + pointCost + "积分"
: bondAltCost + "债券(替代)";
return "§a补签成功!日期: "
+ normalized
+ " §7消耗: " + costMsg;
}
// ===== 执行补签 =====
// ===== 执行补签 =====
public void doBackCheckIn(
Player p, String dateStr) {
String name = p.getName();
Object val = plugin.getDb()
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
String updated = existing.isEmpty()
? dateStr
: existing + "," + dateStr;
plugin.getDb().setField(name,
"backcheck_dates", updated);
int streak = getStreak(name);
plugin.getDb().setField(name,
"checkin_streak", streak + 1);
// ★ 补签不发放奖励,只记录
}
// ===== 获取N天前日期 =====
private String getPastDate(
int daysAgo) {
Calendar cal =
Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH,
-daysAgo);
return new SimpleDateFormat(
"yyyy-MM-dd")
.format(cal.getTime());
}
}
@@ -1,227 +0,0 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Logger;
public class CommissionManager {
private final Main main;
private final Logger logger;
private final List<double[]> rules =
new ArrayList<>();
public CommissionManager(Main main) {
this.main = main;
this.logger = main.getLogger();
loadRules();
}
public void loadRules() {
rules.clear();
File f = new File(
main.getDataFolder(),
"提成规则.txt");
if (!f.exists()) {
writeDefault(f);
}
try (BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
// 格式: 邀请人数:提成百分比
// 如 "5:15" = 邀请5人提成15%
String[] parts =
line.split(":");
if (parts.length >= 2) {
try {
int count = Integer.parseInt(
parts[0].trim());
double rate = Double
.parseDouble(
parts[1]
.trim()) / 100.0;
rules.add(new double[]{
count, rate});
} catch (Exception ignored) {
}
}
}
// 按人数排序(从大到小)
rules.sort((a, b) ->
Double.compare(b[0], a[0]));
logger.info(
"[Commission] 加载 "
+ rules.size() + " 条提成规则");
} catch (Exception e) {
logger.warning(
"[Commission] 加载失败: "
+ e.getMessage());
}
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# 邀请提成规则");
pw.println("# 格式: 邀请人数:提成百分比");
pw.println("# 邀请10人提成20%");
pw.println("10:20");
pw.println("# 邀请5人提成15%");
pw.println("5:15");
pw.println("# 邀请1人提成10%");
pw.println("1:10");
} catch (IOException ignored) {
}
}
/**
* 获取邀请人的提成比例
* 根据已邀请人数匹配最高规则
*/
public double getCommissionRate(
String inviter) {
int inviteCount = getInviteCount(
inviter);
for (double[] rule : rules) {
if (inviteCount >= (int) rule[0]) {
return rule[1];
}
}
return 0;
}
/**
* 获取邀请人数
*/
public int getInviteCount(
String inviter) {
try {
Object val =
main.getDb().getField(
inviter,
"monthly_invite_count");
if (val instanceof Number) {
return ((Number) val).intValue();
}
Object total =
main.getDb().getField(
inviter,
"total_invite_count");
if (total instanceof Number) {
return ((Number) total).intValue();
}
} catch (Exception ignored) {
}
return 0;
}
/**
* 发放提成
* 被邀请人获得奖励时,邀请人获得提成
*/
public void payCommission(
String invitee,
double originalAmount) {
// 查找邀请人
String inviter = getInviter(invitee);
if (inviter == null
|| inviter.isEmpty()) return;
double rate = getCommissionRate(
inviter);
if (rate <= 0) return;
double commission =
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);
// 通知邀请人
Player inviterP =
main.getServer()
.getPlayer(inviter);
if (inviterP != null
&& inviterP.isOnline()) {
inviterP.sendMessage(
"§a§l[邀请提成] §f"
+ invitee + " 完成任务,"
+ "你获得 §e"
+ String.format("%.2f",
commission)
+ " §f游戏币提成 ("
+ (int)(rate * 100) + "%)");
}
logger.info(
"[Commission] 提成: "
+ inviter + " ← "
+ invitee + " | "
+ String.format("%.2f",
originalAmount)
+ " × "
+ (int)(rate * 100) + "% = "
+ String.format("%.2f",
commission));
}
/**
* 获取邀请人
*/
private String getInviter(String player) {
try {
Object val =
main.getDb().getField(
player,
"invited_by");
if (val != null
&& !val.toString()
.isEmpty()) {
return val.toString();
}
} catch (Exception ignored) {
}
return null;
}
/**
* 判断金额是否包含经济内容
*/
public boolean hasEconomyContent(
String text) {
String t = text.toLowerCase();
return t.contains("游戏币")
|| t.contains("金币")
|| t.contains("积分")
|| t.contains("块钱")
|| t.contains("元")
|| t.contains("$")
|| t.contains("经济")
|| t.contains("余额");
}
}
@@ -1,372 +0,0 @@
package Sdf1_login;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class ConfigManager {
private final File dataFolder;
private final Map<String, String> messages
= new LinkedHashMap<>();
private final Map<String, String> smtpSettings
= new LinkedHashMap<>();
// ===== 公共字段 =====
public String adminTag = "admin";
public String adminPassword = "qweasd";
public int maxAccountsPerIP = 3;
public String approvalMode = "manual";
public int autoApproveDelayMinutes = 30;
public int loginTimeout = 60;
public boolean afkEnabled = false;
public int afkTimeout = 300;
public boolean smtpSsl = true;
public String smtpVerifyCode = "";
public long smtpVerifyExpire = 0;
public long smtpLastSendTime = 0;
public String smtpConfiguringPlayer = "";
public String broadcastServerUrl = "";
public int httpPort = 8080;
public String garbageTag = "admin";
public double checkinRewardFixed = 0;
public double checkinRewardMin = 0;
public double checkinRewardMax = 0;
public boolean checkinGivePoints = true;
public int checkinPoints = 10;
public String checkinRewardType = "none";
public int backCheckPointMultiplier = 5;
public int checkinBondMin = 1;
public int checkinBondMax = 3;
public double providerPointsPerScore = 2.0;
public int baseEconomyReward = 100;
public double providerEconomyPerScore = 50.0;
public int requesterPointsReward = 10;
public boolean garbageEnabled = true;
public int garbageInterval = 300;
public int garbageMaxRounds = 1;
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
public ConfigManager(File dataFolder) {
this.dataFolder = dataFolder;
}
public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济
// ==================== 消息 ====================
public void loadMessages() {
messages.clear();
File file = new File(dataFolder, "消息.txt");
if (!file.exists()) createDefaultMessages(file);
loadMap(file, messages);
}
public String msg(String key) {
String val = messages.getOrDefault(key, key);
val = val.replace("<b>", "&l");
val = val.replace("</b>", "&r");
val = val.replace("<br>", "\n");
val = val.replace("\\n", "\n");
val = val.replaceAll(
"&([0-9a-fk-orA-FK-OR])", "§$1");
return val;
}
private void createDefaultMessages(File f) {
List<String> L = new ArrayList<>();
L.add("# ===== Sdf1_login 消息配置 =====");
L.add("login_timeout=§c登录超时");
L.add("not_registered=§e您尚未注册");
L.add("not_logged_in=§e您尚未登录");
L.add("already_registered=§c您已注册过");
L.add("already_logged_in=§c您已登录");
L.add("reg_success=§a注册成功!");
L.add("login_success=§a登录成功!");
L.add("login_failed=§c密码错误");
L.add("password_wrong=§c密码错误");
L.add("password_changed=§a密码修改成功!");
L.add("password_format_error=§c密码需6位以上含大小写和数字");
L.add("password_same=§c新密码不能与旧密码相同");
L.add("checkin_already=§c您今日已签到");
L.add("checkin_success=§a签到成功!获得 {points} 积分");
L.add("chat_muted=§c你已被禁言");
L.add("未知参数=§c未知参数或权限不足");
writeLines(f, L);
}
// ==================== SMTP ====================
public void loadSmtp() {
smtpSettings.clear();
File file = new File(dataFolder, "SMTP设置.txt");
if (!file.exists()) createDefaultSmtp(file);
loadMap(file, smtpSettings);
}
public String getSmtp(String key) {
return smtpSettings.getOrDefault(key, "");
}
public void setSmtp(String key, String value) {
smtpSettings.put(key, value);
}
public void saveSmtp() {
File file = new File(dataFolder, "SMTP设置.txt");
List<String> L = new ArrayList<>();
L.add("# ===== SMTP 配置 =====");
for (Map.Entry<String, String> e
: smtpSettings.entrySet()) {
L.add(e.getKey() + "=" + e.getValue());
}
writeLines(file, L);
}
private void createDefaultSmtp(File f) {
List<String> L = new ArrayList<>();
L.add("# ===== SMTP 配置 =====");
L.add("smtp地址=smtp.example.com");
L.add("smtp端口=465");
L.add("smtp账号=");
L.add("smtp密码=");
L.add("发件人名称=Sdf1_login");
L.add("smtp加密=true");
writeLines(f, L);
}
// ==================== 插件设置 ====================
public void loadSettings() {
File file = new File(dataFolder, "插件设置.txt");
if (!file.exists()) createDefaultSettings(file);
Map<String, String> m = new LinkedHashMap<>();
loadMap(file, m);
adminTag = m.getOrDefault("管理标签", adminTag);
adminPassword = m.getOrDefault("管理密码", adminPassword);
maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3);
approvalMode = m.getOrDefault("审批模式", approvalMode);
autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30);
loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60);
afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false"));
afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300);
smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true"));
httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080);
broadcastServerUrl = m.getOrDefault("resource-pack-url", "");
providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0);
baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100);
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0);
checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0);
checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0);
checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true"));
checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10);
backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5);
checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1);
checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3);
if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin;
if (checkinRewardFixed > 0) checkinRewardType = "fixed";
else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range";
else checkinRewardType = "none";
rewardChannel = m.getOrDefault("奖励发放方式", "债券");
rewardChannel = normalizeRewardChannel(rewardChannel);
// 验证合法值
if (!"economy".equalsIgnoreCase(rewardChannel)
&& !"bonds".equalsIgnoreCase(rewardChannel)) {
rewardChannel = "bonds";
}
// ★ 自动补充新配置项 ★
boolean needAppend = false;
List<String> newKeys = new ArrayList<>();
if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); }
if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); }
if (!m.containsKey("奖励发放方式")) {
needAppend = true;
newKeys.add("奖励发放方式=债券");
}
if (needAppend) {
try {
FileWriter fw = new FileWriter(file, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.newLine();
for (String key : newKeys) { bw.write(key); bw.newLine(); }
bw.flush(); bw.close();
System.out.println("[Config] 已补充签到债券配置");
} catch (IOException ignored) {}
if (!m.containsKey("奖励发放方式")) {
try {
FileWriter fw = new FileWriter(file, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.newLine();
bw.write("奖励发放方式=bonds");
bw.newLine();
bw.flush(); bw.close();
} catch (IOException ignored) {}
}
}
}
/**
* 奖励方式标准化:中英文全兼容
* "债券" "bonds" "Bond" "BONDS" → "bonds"
* "经济" "economy" "Economy" "ECONOMY" → "economy"
* 其他任何值 → "bonds"(默认债券)
*/
private String normalizeRewardChannel(String raw) {
if (raw == null) return "bonds";
String s = raw.trim().toLowerCase();
// 中文
if (s.contains("债券")) return "bonds";
if (s.contains("经济")) return "economy";
// 英文
if (s.contains("bond")) return "bonds";
if (s.contains("econ")) return "economy";
if (s.contains("Vault")) return "economy";
// 兜底
return "bonds";
}
public void saveSettings() {
File file = new File(dataFolder, "插件设置.txt");
List<String> L = new ArrayList<>();
L.add("# ===== Sdf1_login 插件设置 =====");
L.add("管理标签=" + adminTag);
L.add("管理密码=" + adminPassword);
L.add("每IP最大账号数=" + maxAccountsPerIP);
L.add("审批模式=" + approvalMode);
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
L.add("登录超时秒数=" + loginTimeout);
L.add("挂机踢出=" + afkEnabled);
L.add("挂机超时秒数=" + afkTimeout);
L.add("resource-pack-url=" + broadcastServerUrl);
L.add("http-port=" + httpPort);
L.add("垃圾站_启用=" + garbageEnabled);
L.add("垃圾站_清理间隔秒=" + garbageInterval);
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
L.add("签到固定金额=" + checkinRewardFixed);
L.add("签到最小金额=" + checkinRewardMin);
L.add("签到最大金额=" + checkinRewardMax);
L.add("签到给积分=" + checkinGivePoints);
L.add("签到积分数量=" + checkinPoints);
L.add("签到债券最小=" + checkinBondMin);
L.add("签到债券最大=" + checkinBondMax);
L.add("补签积分倍率=" + backCheckPointMultiplier);
L.add("奖励发放方式=债券");
writeLines(file, L);
}
private void createDefaultSettings(File f) {
List<String> L = new ArrayList<>();
L.add("# ===== Sdf1_login 插件设置 =====");
L.add("管理标签=admin");
L.add("管理密码=qweasd");
L.add("每IP最大账号数=3");
L.add("审批模式=manual");
L.add("自动审批延迟分钟=30");
L.add("登录超时秒数=60");
L.add("挂机踢出=false");
L.add("挂机超时秒数=300");
L.add("resource-pack-url=");
L.add("http-port=8080");
L.add("垃圾站_启用=true");
L.add("垃圾站_清理间隔秒=300");
L.add("垃圾站_保留轮数=1");
L.add("签到固定金额=0");
L.add("签到最小金额=50");
L.add("签到最大金额=500");
L.add("签到给积分=true");
L.add("签到积分数量=10");
L.add("签到债券最小=1");
L.add("签到债券最大=3");
L.add("补签积分倍率=5");
L.add("奖励发放方式=债券");
writeLines(f, L);
}
public List<String> getAfkWhitelist() {
List<String> list = new ArrayList<>();
File file = new File(dataFolder, "挂机白名单.txt");
if (!file.exists()) {
try {
file.createNewFile();
FileWriter fw = new FileWriter(file);
fw.write("# 每行一个玩家名\n");
fw.flush();
fw.close();
} catch (IOException ignored) {}
return list;
}
try (BufferedReader r = new BufferedReader(
new InputStreamReader(
new FileInputStream(file),
StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.startsWith("#"))
list.add(line);
}
} catch (IOException ignored) {}
return list;
}
// ==================== 其他 ====================
public String generateSmtpVerifyCode() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder();
java.util.Random r = new java.util.Random();
for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length())));
smtpVerifyCode = sb.toString();
smtpVerifyExpire = System.currentTimeMillis() + 300000L;
return smtpVerifyCode;
}
public boolean checkSmtpVerifyCode(String code) {
if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; }
return smtpVerifyCode.equalsIgnoreCase(code);
}
public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; }
public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; }
public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); }
// ==================== 文件工具 ====================
private void loadMap(File file, Map<String, String> map) {
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
int eq = line.indexOf('=');
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
}
} catch (IOException ignored) {}
}
private void writeLines(File file, List<String> lines) {
try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
for (String l : lines) { w.write(l); w.newLine(); }
} catch (IOException ignored) {}
}
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
}
@@ -1,577 +0,0 @@
package Sdf1_login;
import org.bukkit.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 java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class CypayCommand
implements CommandExecutor, TabCompleter {
private final Main plugin;
private static final SimpleDateFormat SDF =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
public CypayCommand(Main plugin) {
this.plugin = plugin;
}
private boolean hasAdminPerm(CommandSender s) {
if (s.isOp()) return true;
if (!(s instanceof Player)) return true;
Player p = (Player) s;
String tag =
plugin.getConfig2().adminTag;
return !tag.isEmpty()
&& p.getScoreboardTags().contains(tag);
}
private int parseAmount(String s) {
return Integer.parseInt(
s.trim().replaceAll("[+]", ""));
}
private List<String> filter(
List<String> list, String prefix) {
String low = prefix.toLowerCase();
return list.stream()
.filter(x -> x.toLowerCase()
.startsWith(low))
.collect(Collectors.toList());
}
@Override
public boolean onCommand(CommandSender sender,
Command cmd,
String label,
String[] args) {
// ===== /cypay — 查自己 =====
if (args.length == 0) {
String name = sender instanceof Player
? sender.getName() : "Console";
int bonds =
plugin.getBonds().getBonds(name);
String status = plugin.getBonds()
.getAccountStatus(name);
sender.sendMessage(
"§e§l[债券] §f" + name
+ " 余额: §a" + bonds);
sender.sendMessage(
"§e§l[债券] §f状态: "
+ ("frozen".equals(status)
? "§c冻结中"
: "§a正常"));
return true;
}
// ===== /cypay import =====
/* if (args.length == 2
&& "import".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String file = args[1];
if (!file.endsWith(".txt"))
file += ".txt";
int result = plugin.getCDK()
.importFromFile(file);
if (result == -1)
sender.sendMessage("§c文件不存在: "
+ file);
else
sender.sendMessage("§a导入完成: "
+ result + " 条");
return true;
}*/
// ===== /cypay freeze =====
if (args.length == 2
&& "freeze".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
if (plugin.getBonds().isFrozen(target)) {
sender.sendMessage("§c" + target
+ " 已处于冻结状态");
return true;
}
plugin.getBonds().freezeAccount(target);
sender.sendMessage("§a已冻结 " + target
+ " 的债券账户");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§c§l[系统] 你的债券账户"
+ "已被管理员冻结");
return true;
}
// ===== /cypay unfreeze =====
if (args.length == 2
&& "unfreeze".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
if (!plugin.getBonds()
.isFrozen(target)) {
sender.sendMessage("§c" + target
+ " 未被冻结");
return true;
}
plugin.getBonds().unfreezeAccount(target);
sender.sendMessage("§a已解冻 " + target
+ " 的债券账户");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§a§l[系统] 你的债券账户"
+ "已被解冻");
return true;
}
// ===== /cypay water [days] =====
if (args.length >= 2
&& "water".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
int days = 7;
if (args.length >= 3) {
try {
days = Integer.parseInt(args[2]);
} catch (NumberFormatException ignored) {
}
}
days = Math.min(days,
BondManager.ADMIN_MAX_WATER_DAYS);
if (days <= 0) days = 1;
List<Map<String, Object>> txs =
plugin.getBonds()
.getTransactions(
target, days);
sender.sendMessage(
"§e§l===== " + target
+ " 流水记录 (近"
+ days + "天) =====");
sender.sendMessage("§f当前余额: §a"
+ plugin.getBonds()
.getBonds(target)
+ " §f状态: "
+ (plugin.getBonds()
.isFrozen(target)
? "§c冻结" : "§a正常"));
if (txs.isEmpty()) {
sender.sendMessage("§7 (暂无记录)");
} else {
for (Map<String, Object> tx : txs) {
sender.sendMessage(
formatTxLine(tx));
}
}
sender.sendMessage(
"§e§l=========================="
+ "==========");
return true;
}
// ===== /cypay printer [player] =====
if ("printer".equalsIgnoreCase(args[0])) {
if (!hasAdminPerm(sender)
&& !(sender instanceof Player)
== false) {
// 控制台也可以用
if (!(sender instanceof Player))
; // 放行
else if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
}
if (args.length == 1) {
// 打印全服
sender.sendMessage(
"§e正在导出全服流水...");
plugin.getBonds(); // 确保实例就绪
if (plugin.getBondPrinter() != null) {
plugin.getBondPrinter()
.printAll(sender);
} else {
sender.sendMessage("§cPrinter未初始化");
}
return true;
}
if (args.length == 2) {
// 打印指定玩家
String target = args[1];
sender.sendMessage("§e正在导出 "
+ target + " 的流水...");
if (plugin.getBondPrinter() != null) {
plugin.getBondPrinter()
.printPlayer(target, sender);
} else {
sender.sendMessage("§cPrinter未初始化");
}
return true;
}
}
// ===== /cypay give/remove =====
if (args.length == 3) {
String action = args[0].toLowerCase();
if ("give".equals(action)
|| "remove".equals(action)) {
if (!hasAdminPerm(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
String target = args[1];
try {
int amount =
parseAmount(args[2]);
if (amount <= 0) {
sender.sendMessage(
"§c金额必须大于0");
return true;
}
String txType =
"give".equals(action)
? BondManager
.TX_ADMIN_GIVE
: BondManager
.TX_ADMIN_DEDUCT;
String opName =
sender instanceof Player
? sender.getName()
: "Console";
if ("give".equals(action)) {
plugin.getBonds().addBonds(
target, amount,
txType, "",
opName, "管理员发放");
sender.sendMessage(
"§a已给 " + target
+ " " + amount
+ " 债券");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage(
"§a管理员给了你 "
+ amount
+ " 债券");
} else {
if (plugin.getBonds()
.deductBonds(target,
amount,
txType, "",
opName,
"管理员扣除")) {
sender.sendMessage(
"§a已扣除 " + target
+ " " + amount
+ " 债券");
Player tp = plugin
.getServer()
.getPlayerExact(
target);
if (tp != null)
tp.sendMessage(
"§c管理员扣除了"
+ "你的 "
+ amount
+ " 债券");
} else {
sender.sendMessage(
"§c" + target
+ " 债券不足"
+ "或账户冻结");
}
}
} catch (NumberFormatException e) {
sender.sendMessage(
"§c金额格式错误");
}
return true;
}
}
// ===== /cypay <player> <amount> — 仅玩家转账 =====
if (args.length == 2) {
try {
int amount = parseAmount(args[1]);
if (amount <= 0) {
sender.sendMessage(
"§c金额必须大于0");
return true;
}
// ★ 控制台禁止转账
if (!(sender instanceof Player)) {
sender.sendMessage(
"§c控制台不能转账,请使用 "
+ "/cypay give <玩家> <金额>");
return true;
}
Player p = (Player) sender;
String target = args[0];
BondManager.TransferResult r =
plugin.getBonds().transfer(
p.getName(),
target, amount);
if (r.success) {
p.sendMessage("§e§l[债券转账] §f向 §a"
+ target + " §f转出 §c"
+ amount + " §f枚债券");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null)
tp.sendMessage("§e§l[债券转账] §f收到 §a"
+ p.getName()
+ " §f转入 §a"
+ amount + " §f枚债券");
} else {
p.sendMessage("§c" + r.message
+ ",当前: "
+ plugin.getBonds()
.getBonds(p.getName()));
}
return true;
} catch (NumberFormatException ignored) {
}
}
// ===== /cypay <player> — 查指定玩家 =====
if (args.length == 1) {
String target = args[0];
// ★ 验证玩家在 login.db 中存在 ★
if (!plugin.getDb().userExists(target)) {
sender.sendMessage("§c玩家 " + target
+ " 不存在于服务器");
return true;
}
// String target = args[0];
if (hasAdminPerm(sender)) {
int bonds = plugin.getBonds()
.getBonds(target);
String status = plugin.getBonds()
.getAccountStatus(target);
sender.sendMessage(
"§e§l[债券] §f" + target
+ " 余额: §a"
+ bonds);
sender.sendMessage(
"§e§l[债券] §f状态: "
+ ("frozen".equals(
status)
? "§c冻结中"
: "§a正常"));
} else {
int bonds = plugin.getBonds()
.getBonds(
sender.getName());
sender.sendMessage(
"§e§l[债券] §f你的余额: §a"
+ bonds);
}
return true;
}
showHelp(sender);
return true;
}
// ===== 流水行格式化 =====
private String formatTxLine(
Map<String, Object> tx) {
String type = (String) tx.get("type");
int amount = ((Number) tx.get("amount"))
.intValue();
String tgt = (String) tx.get("target_player");
String reason = (String) tx.get("reason");
int bal = ((Number) tx.get("balance_after"))
.intValue();
long time = ((Number) tx.get("time"))
.longValue();
boolean pos = amount > 0;
String sign = pos ? "+" : "";
String color = pos ? "§a" : "§c";
StringBuilder sb = new StringBuilder();
sb.append("§7[")
.append(SDF.format(new Date(time)))
.append("] ")
.append(formatTxType(type))
.append(" ")
.append(color).append(sign)
.append(amount).append("§7");
if (tgt != null && !tgt.isEmpty())
sb.append(" §e↔ ").append(tgt);
if (reason != null && !reason.isEmpty())
sb.append(" §7(").append(reason)
.append(")");
sb.append(" §7余额:§f").append(bal);
return sb.toString();
}
private String formatTxType(String type) {
if (type == null) return "§7未知";
switch (type) {
case BondManager.TX_ADMIN_GIVE:
return "§a管理员发放";
case BondManager.TX_ADMIN_DEDUCT:
return "§c管理员扣除";
case BondManager.TX_TRANSFER_OUT:
return "§c转出";
case BondManager.TX_TRANSFER_IN:
return "§a转入";
case BondManager.TX_REDEEM:
return "§aCDK兑换";
case BondManager.TX_SHOP_BUY:
return "§e商城购买";
case BondManager.TX_DAILY_SIGN:
return "§a签到奖励";
case BondManager.TX_FREEZE:
return "§c冻结";
case BondManager.TX_UNFREEZE:
return "§a解冻";
default:
return "§7" + type;
}
}
private void showHelp(CommandSender sender) {
sender.sendMessage("§e§l债券命令帮助:");
sender.sendMessage(
"§e/cypay §7- 查询自己余额");
sender.sendMessage(
"§e/cypay <玩家> §7- 查余额(管理员)");
sender.sendMessage(
"§e/cypay <玩家> <金额> §7- 转账");
if (hasAdminPerm(sender)) {
sender.sendMessage(
"§e/cypay give <玩家> <金额> §7- 给债券");
sender.sendMessage(
"§e/cypay remove <玩家> <金额> §7- 扣债券");
sender.sendMessage(
"§e/cypay import <文件> §7- 导入CDK");
sender.sendMessage(
"§e/cypay freeze <玩家> §7- 冻结");
sender.sendMessage(
"§e/cypay unfreeze <玩家> §7- 解冻");
sender.sendMessage(
"§e/cypay water <玩家> [天数] §7- 流水");
sender.sendMessage(
"§e/cypay printer §7- 导出全服流水xlsx");
sender.sendMessage(
"§e/cypay printer <玩家> §7- 导出指定玩家xlsx");
}
}
@Override
public List<String> onTabComplete(
CommandSender sender, Command command,
String label, String[] args) {
List<String> list = new ArrayList<>();
if (args.length == 1) {
String last = args[0].toLowerCase();
if (hasAdminPerm(sender)) {
list.add("give");
list.add("remove");
list.add("import");
list.add("freeze");
list.add("unfreeze");
list.add("water");
list.add("printer");
File cdkDir = new File(
plugin.getDataFolder(),
"cdk");
if (cdkDir.exists()) {
File[] files = cdkDir.listFiles(
(d, n) -> n
.endsWith(".txt"));
if (files != null)
for (File f : files)
list.add(f.getName());
}
}
for (Player o :
Bukkit.getOnlinePlayers())
list.add(o.getName());
return filter(list, last);
}
if (args.length == 2) {
String first = args[0].toLowerCase();
if ("give".equals(first)
|| "remove".equals(first)
|| "freeze".equals(first)
|| "unfreeze".equals(first)
|| "water".equals(first)) {
for (Player o :
Bukkit.getOnlinePlayers())
list.add(o.getName());
return filter(list,
args[1].toLowerCase());
}
list.add("10");
list.add("50");
list.add("100");
return filter(list,
args[1].toLowerCase());
}
if (args.length == 3) {
String first = args[0].toLowerCase();
if ("give".equals(first)
|| "remove".equals(first)) {
list.add("10");
list.add("50");
list.add("100");
list.add("999");
return filter(list,
args[2].toLowerCase());
}
if ("water".equals(first)) {
list.add("1");
list.add("3");
list.add("7");
list.add("14");
return filter(list,
args[2].toLowerCase());
}
}
return list;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,284 +0,0 @@
package Sdf1_login;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Base64;
import java.util.logging.Logger;
public class EmailManager {
private final ConfigManager config;
private final Logger logger;
public EmailManager(ConfigManager config) {
this.config = config;
this.logger = Logger.getLogger("Sdf1_login");
}
public boolean isConfigured() {
String host = config.getSmtp("smtp地址");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
return host != null && !host.isEmpty()
&& user != null && !user.isEmpty()
&& pass != null && !pass.isEmpty();
}
public boolean sendVerifyCode(String to,
String code) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置,"
+ "无法发送验证码");
return false;
}
logger.info("[Sdf1_login] 发送验证码到 " + to);
return sendEmailSync(to,
"[Sdf1_login] 验证码",
"您的验证码: " + code
+ "\n5分钟内有效。\n"
+ "如非本人操作请忽略。");
}
public boolean sendTempPassword(String to,
String player, String tempPwd) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置");
return false;
}
return sendEmailSync(to,
"[Sdf1_login] 临时密码",
"玩家 " + player
+ " 的临时密码: " + tempPwd
+ "\n请尽快登录并修改密码!");
}
public boolean sendBody(String to, String subject, String body) {
if (!isConfigured()) {
logger.warning("[Sdf1_login] SMTP未配置");
return false;
}
return sendEmailSync(to, subject, body);
}
private boolean sendEmailSync(String to,
String subject, String body) {
final boolean[] result = {false};
Thread t = new Thread(() -> {
try {
result[0] = sendEmail(
to, subject, body);
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件线程异常: "
+ e.getMessage());
}
}, "Sdf1_login-SMTP");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
logger.severe(
"[Sdf1_login] SMTP超时(15秒)");
return false;
}
return result[0];
}
private boolean sendEmail(String to,
String subject, String body) {
String host = config.getSmtp("smtp地址");
String portStr = config.getSmtp("smtp端口");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
String fromName = config.getSmtp(
"发件人名称");
int port;
try {
port = Integer.parseInt(portStr);
} catch (Exception e) {
port = 465;
}
try {
String encSubject = "=?UTF-8?B?"
+ Base64.getEncoder()
.encodeToString(
subject.getBytes("UTF-8"))
+ "?=";
String encBody = Base64.getEncoder()
.encodeToString(
body.getBytes("UTF-8"));
if (port == 465) {
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket socket = (SSLSocket)
factory.createSocket(
host, port);
socket.setSoTimeout(10000);
doSmtp(socket, host, user, pass,
to, encSubject, encBody,
fromName);
socket.close();
} else {
Socket plainSocket =
new Socket(host, port);
plainSocket.setSoTimeout(10000);
BufferedReader preReader =
new BufferedReader(
new InputStreamReader(
plainSocket
.getInputStream(),
"UTF-8"));
PrintWriter preWriter =
new PrintWriter(
plainSocket
.getOutputStream(),
true);
checkResponse(preReader, "220");
sendLine(preWriter,
"EHLO " + host);
checkResponse(preReader, "250");
sendLine(preWriter, "STARTTLS");
checkResponse(preReader, "220");
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket sslSocket = (SSLSocket)
factory.createSocket(
plainSocket, host,
port, true);
sslSocket.startHandshake();
doSmtp(sslSocket, host, user, pass,
to, encSubject, encBody,
fromName);
sslSocket.close();
}
logger.info(
"[Sdf1_login] 邮件发送成功 -> "
+ to);
return true;
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件异常: "
+ e.getClass()
.getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
logger.severe(
"[Sdf1_login] 根因: "
+ e.getCause()
.getMessage());
}
return false;
}
}
private void doSmtp(SSLSocket socket,
String host, String user, String pass,
String to, String subject, String body,
String fromName) throws Exception {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
"UTF-8"));
OutputStream rawOut =
socket.getOutputStream();
PrintWriter writer =
new PrintWriter(
new java.io.BufferedOutputStream(
rawOut), true);
checkResponse(reader, "220");
sendLine(writer, "EHLO " + host);
checkResponse(reader, "250");
sendLine(writer, "AUTH LOGIN");
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
user.getBytes("UTF-8")));
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
pass.getBytes("UTF-8")));
checkResponse(reader, "235");
sendLine(writer,
"MAIL FROM:<" + user + ">");
checkResponse(reader, "250");
sendLine(writer,
"RCPT TO:<" + to + ">");
checkResponse(reader, "250");
sendLine(writer, "DATA");
checkResponse(reader, "354");
sendLine(writer,
"From: " + fromName
+ " <" + user + ">");
sendLine(writer,
"To: <" + to + ">");
sendLine(writer,
"Reply-To: <" + user + ">");
sendLine(writer,
"Subject: " + subject);
sendLine(writer, "MIME-Version: 1.0");
sendLine(writer,
"Content-Type: text/plain; "
+ "charset=UTF-8");
sendLine(writer,
"Content-Transfer-Encoding: "
+ "base64");
sendLine(writer, "");
sendLine(writer, body);
sendLine(writer, ".");
checkResponse(reader, "250");
sendLine(writer, "QUIT");
}
private void sendLine(PrintWriter writer,
String line) {
writer.print(line + "\r\n");
writer.flush();
}
private void checkResponse(
BufferedReader reader,
String expected) throws Exception {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
if (line.length() >= 4
&& Character.isDigit(
line.charAt(0))
&& line.charAt(3) == ' ')
break;
}
String resp = sb.toString().trim();
if (!resp.startsWith(expected)) {
if (resp.startsWith("535"))
logger.severe(
"[Sdf1_login] SMTP 535 "
+ "认证失败");
throw new Exception(
"SMTP响应异常: 期望 "
+ expected
+ ",实际: " + resp);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,573 +0,0 @@
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;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
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;
import java.text.SimpleDateFormat;
import java.util.*;
public class GarbageManager {
private final Main plugin;
private Connection db;
private int cleanupInterval = 1800;
private int maxRounds = 1;
private boolean enabled = true;
private int currentRound = 0;
private final Map<String, Integer>
recyclePages = new HashMap<>();
public GarbageManager(Main plugin) {
this.plugin = plugin;
}
public void init() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(),
"garbage.db");
db = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
st.execute("CREATE TABLE IF NOT EXISTS "
+ "items ("
+ "id INTEGER PRIMARY KEY "
+ "AUTOINCREMENT,"
+ "item_data TEXT NOT NULL,"
+ "amount INTEGER DEFAULT 1,"
+ "round INTEGER DEFAULT 0,"
+ "create_time INTEGER NOT NULL"
+ ")");
st.execute("CREATE TABLE IF NOT EXISTS "
+ "config ("
+ "key TEXT PRIMARY KEY,"
+ "value TEXT NOT NULL"
+ ")");
st.close();
} catch (Exception e) {
plugin.getLogger().severe(
"[Garbage] DB失败: "
+ e.getMessage());
}
}
public void loadConfig() {
// 从 ConfigManager 读取
cleanupInterval =
plugin.getConfig2()
.garbageInterval;
enabled =
plugin.getConfig2()
.garbageEnabled;
maxRounds =
plugin.getConfig2()
.garbageMaxRounds;
// 从数据库读取当前轮次
try {
String val = getConfigValue("round");
if (val != null)
currentRound =
Integer.parseInt(val);
} catch (Exception ignored) {
}
plugin.getLogger().info(
"[Garbage] 间隔: "
+ cleanupInterval + "秒, "
+ "轮次保留: "
+ maxRounds + "轮, "
+ "启用: " + enabled);
}
private String getConfigValue(String key) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT value FROM config "
+ "WHERE key=?");
ps.setString(1, key);
ResultSet rs = ps.executeQuery();
String val = rs.next()
? rs.getString("value") : null;
rs.close();
ps.close();
return val;
} catch (Exception e) {
return null;
}
}
private void setConfigValue(String key,
String value) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO config "
+ "(key, value) "
+ "VALUES (?, ?)");
ps.setString(1, key);
ps.setString(2, value);
ps.executeUpdate();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void startAutoCleanup() {
if (!enabled) return;
new BukkitRunnable() {
int countdown = 0;
@Override
public void run() {
// 在线人数≤0时暂停
if (Bukkit.getOnlinePlayers()
.isEmpty()) {
countdown = 0;
return;
}
// 重新读取配置(支持热更)
cleanupInterval =
plugin.getConfig2()
.garbageInterval;
int remain =
cleanupInterval - countdown;
if (remain == 300)
broadcast("§e§l[垃圾站] §f距离清理还有 §e5分钟");
else if (remain == 180)
broadcast("§e§l[垃圾站] §f距离清理还有 §e3分钟");
else if (remain == 60)
broadcast("§e§l[垃圾站] §f距离清理还有 §e1分钟");
else if (remain == 30)
broadcast("§e§l[垃圾站] §f距离清理还有 §e30秒");
else if (remain == 10)
broadcast("§e§l[垃圾站] §f距离清理还有 §e10秒");
else if (remain <= 5 && remain > 0)
broadcast("§e§l[垃圾站] §c" + remain + "§f...");
else if (remain == 0) {
doCleanup();
countdown = 0;
return;
}
countdown++;
}
}.runTaskTimer(plugin, 20L, 20L);
}
private void doCleanup() {
// 删除过期轮次的物品
int deleteRound =
currentRound - maxRounds;
if (deleteRound >= 0) {
int deleted = deleteOldItems(
deleteRound);
if (deleted > 0) {
broadcast("§7[垃圾站] §f永久清理了 §e"
+ deleted + " §f个过期物品");
}
}
// 递增轮次
currentRound++;
setConfigValue("round",
String.valueOf(currentRound));
// 收集当前掉落物
int count = collectItems();
broadcast("§a§l[垃圾站] §f清理完成,"
+ "共清理 §e" + count
+ " §f个掉落物,存入回收站");
// 打印下次清理时间
long nextMs = System.currentTimeMillis()
+ (long) cleanupInterval * 1000L;
String nextTime =
new SimpleDateFormat("HH:mm:ss")
.format(new Date(nextMs));
broadcast("§e§l[垃圾站] §f下次清理时间: §a"
+ nextTime);
}
public int collectItems() {
int count = 0;
for (org.bukkit.World w :
Bukkit.getWorlds()) {
Collection<Item> items =
w.getEntitiesByClass(Item.class);
for (Item item : items) {
ItemStack stack =
item.getItemStack();
if (stack == null
|| stack.getType()
== Material.AIR)
continue;
saveItem(stack);
item.remove();
count++;
}
}
return count;
}
public void saveItem(ItemStack stack) {
try {
String data = stackToBase64(stack);
if (data.isEmpty()) return;
PreparedStatement ps = db.prepareStatement(
"INSERT INTO items "
+ "(item_data, amount, "
+ "round, create_time) "
+ "VALUES (?,?,?,?)");
ps.setString(1, data);
ps.setInt(2, stack.getAmount());
ps.setInt(3, currentRound);
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private int deleteOldItems(int beforeRound) {
try {
PreparedStatement ps = db.prepareStatement(
"DELETE FROM items "
+ "WHERE round <= ?");
ps.setInt(1, beforeRound);
int rows = ps.executeUpdate();
ps.close();
return rows;
} catch (Exception e) {
return 0;
}
}
// ========== 序列化 ==========
private String stackToBase64(ItemStack stack) {
try {
Map<String, Object> map =
stack.serialize();
String json = new Gson().toJson(map);
return Base64.getEncoder()
.encodeToString(
json.getBytes(
StandardCharsets.UTF_8));
} catch (Exception e) {
return "";
}
}
private ItemStack base64ToStack(String base64) {
try {
byte[] bytes = Base64.getDecoder()
.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);
map = fixNumberTypes(map);
return ItemStack.deserialize(map);
} catch (Exception e) {
return null;
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> fixNumberTypes(
Map<String, Object> map) {
Map<String, Object> result =
new LinkedHashMap<>();
for (Map.Entry<String, Object> e
: map.entrySet()) {
Object v = e.getValue();
if (v instanceof Double) {
double d = (Double) v;
if (d == (long) d) {
result.put(e.getKey(), (long) d);
} else {
result.put(e.getKey(), d);
}
} else if (v instanceof Map) {
result.put(e.getKey(),
fixNumberTypes(
(Map<String, Object>) v));
} else if (v instanceof List) {
List<Object> list =
new ArrayList<>();
for (Object item : (List<?>) v) {
if (item instanceof Map) {
list.add(fixNumberTypes(
(Map<String, Object>) item));
} else {
list.add(item);
}
}
result.put(e.getKey(), list);
} else {
result.put(e.getKey(), v);
}
}
return result;
}
// ========== 回收站GUI ==========
public void openRecycle(Player p) {
openRecyclePage(p, 0);
}
public void openRecyclePage(Player p, int page) {
Inventory g = Bukkit.createInventory(
null, 54, "§6§l垃圾回收站");
List<Map<String, Object>> allItems =
getStoredItems(999);
int total = allItems.size();
int pageSize = 40;
int totalPages = Math.max(1,
(int) Math.ceil(
(double) total / pageSize));
// 修正页码
if (page < 0) page = 0;
if (page >= totalPages)
page = totalPages - 1;
recyclePages.put(p.getName(), page);
// 填充物品(0-39 格 = 第1页,40-79 = 第2页...)
int start = page * pageSize;
int end = Math.min(start + pageSize,
total);
int slot = 0;
for (int i = start; i < end; i++) {
if (slot >= 40) break;
Map<String, Object> item =
allItems.get(i);
int id = ((Number) item.get("id"))
.intValue();
String data = (String) item
.get("item_data");
int amount = ((Number) item
.get("amount")).intValue();
ItemStack stack = base64ToStack(data);
if (stack != null) {
stack.setAmount(amount);
ItemMeta im =
stack.getItemMeta();
if (im != null) {
List<String> lore =
new ArrayList<>();
lore.add("§7ID: #" + id);
lore.add("§a点击取出");
im.setLore(lore);
stack.setItemMeta(im);
}
g.setItem(slot, stack);
}
slot++;
}
// 49 格:上一页
if (page > 0) {
g.setItem(49, mk(Material.ARROW,
"§e上一页 §7("
+ page + "/"
+ totalPages + ")"));
} else {
g.setItem(49, mk(Material.ARROW,
"§e第一页 §7("
+ "1/" + totalPages + ")"));
}
// 50 格:统计信息
g.setItem(50, mk(Material.CHEST,
"§e回收站统计",
"§7总存量: §f" + total + " 个",
"§7当前页: §f" + (page + 1)
+ "/" + totalPages));
// 51 格:下一页
boolean canNext = page < totalPages - 1;
if (canNext) {
g.setItem(51, mk(Material.ARROW,
"§e下一页 §7("
+ (page + 2) + "/"
+ totalPages + ")"));
} else {
g.setItem(51, mk(Material.ARROW,
"§7已是最后一页"));
}
// 52 格:返回
g.setItem(52, mk(Material.ARROW,
"§7返回",
"§7返回主菜单"));
p.openInventory(g);
}
public int getRecyclePage(String name) {
return recyclePages
.getOrDefault(name, 0);
}
public boolean handleClick(Player p,
String title, int slot) {
if (!title.equals("§6§l垃圾回收站"))
return false;
// 49:上一页
if (slot == 49) {
int page = recyclePages
.getOrDefault(p.getName(), 0);
if (page > 0)
openRecyclePage(p, page - 1);
return true;
}
// 51:下一页
if (slot == 51) {
int page = recyclePages
.getOrDefault(p.getName(), 0);
openRecyclePage(p, page + 1);
return true;
}
// 52:返回
if (slot == 52) {
plugin.getGui().openMain(p);
return true;
}
// 50/52/53:不处理
if (slot >= 49 && slot <= 53)
return true;
return true;
}
private List<Map<String, Object>>
getStoredItems(int limit) {
List<Map<String, Object>> list =
new ArrayList<>();
try {
PreparedStatement ps =
db.prepareStatement(
"SELECT id, item_data, "
+ "amount "
+ "FROM items "
+ "ORDER BY "
+ "create_time "
+ "DESC LIMIT ?");
ps.setInt(1, limit);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id",
rs.getInt("id"));
row.put("item_data",
rs.getString("item_data"));
row.put("amount",
rs.getInt("amount"));
list.add(row);
}
rs.close();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
public boolean removeItem(int id) {
try {
PreparedStatement ps =
db.prepareStatement(
"DELETE FROM items "
+ "WHERE id=?");
ps.setInt(1, id);
int rows = ps.executeUpdate();
ps.close();
return rows > 0;
} catch (Exception e) {
return false;
}
}
public int getItemCount() {
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT COUNT(*) FROM items");
int count = 0;
if (rs.next())
count = rs.getInt(1);
rs.close();
st.close();
return count;
} catch (Exception e) {
return 0;
}
}
private void broadcast(String msg) {
// 同步打印到控制台
plugin.getLogger().info(
msg.replaceAll("§[0-9a-fk-orA-FK-OR]", ""));
// 同步打印到聊天栏
Bukkit.broadcastMessage(msg);
}
private ItemStack mk(Material mat,
String name, String... lore) {
ItemStack it = new ItemStack(mat);
ItemMeta im = it.getItemMeta();
if (im != null) {
im.setDisplayName(name);
if (lore.length > 0) {
List<String> list =
new ArrayList<>();
for (String l : lore)
if (!l.isEmpty()) list.add(l);
if (!list.isEmpty())
im.setLore(list);
}
it.setItemMeta(im);
}
return it;
}
public void close() {
try {
if (db != null && !db.isClosed())
db.close();
} catch (Exception ignored) {
}
}
}
@@ -1,259 +0,0 @@
package Sdf1_login;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
public class GiftManager {
private final File giftFolder;
private final Main plugin;
private final List<Map<String, Object>> stages =
new ArrayList<>();
public GiftManager(Main plugin) {
this.plugin = plugin;
this.giftFolder = new File(
plugin.getDataFolder(), "任务面板");
}
public void loadStages() {
stages.clear();
if (!giftFolder.exists()) {
giftFolder.mkdirs();
writeDefaultStages();
}
File[] files = giftFolder.listFiles(
(dir, name) -> name.endsWith(".md"));
if (files == null) return;
Arrays.sort(files);
for (File f : files) {
stages.add(parseMarkdown(f));
}
}
private Map<String, Object> parseMarkdown(File f) {
Map<String, Object> stage = new LinkedHashMap<>();
stage.put("stage", stages.size() + 1);
stage.put("name",
f.getName().replace(".md", ""));
stage.put("rewards", new ArrayList<String>());
try {
BufferedReader r = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8));
String line;
List<String> rewards = new ArrayList<>();
while ((line = r.readLine()) != null) {
if (line.contains("礼包内容")
|| line.contains("奖励")
|| line.contains("任务内容")) {
int colon = line.indexOf(':');
if (colon < 0)
colon = line.indexOf('\uff1a');
if (colon >= 0) {
rewards.add(
line.substring(colon + 1)
.trim());
}
}
}
stage.put("rewards", rewards);
r.close();
} catch (Exception e) {
e.printStackTrace();
}
return stage;
}
private void writeStage(String name, String content) {
File f = new File(giftFolder, name);
if (f.exists()) return;
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.print(content);
} catch (IOException ignored) {}
}
private void writeDefaultStages() {
writeStage("第一阶段.md",
"# 第一阶段\n\n## 领取条件\n\n- 无前级条件\n\n## 礼包内容\n\n- 皮革套装 × 1\n- 木制武器工具套装 × 1\n");
writeStage("第二阶段.md",
"# 第二阶段\n\n## 领取条件\n\n- 在线时长>1小时\n\n## 礼包内容\n\n- 500 游戏币\n");
writeStage("第三阶段.md",
"# 第三阶段\n\n## 领取条件\n\n- 破坏100个方块\n- 在线>1天\n\n## 礼包内容\n\n- 1500 游戏币\n- 铁质工具套装 × 1\n- 铁质防具 × 1\n");
writeStage("第四阶段.md",
"# 第四阶段\n\n## 领取条件\n\n- 破坏/放置300方块\n- 在线≥7天\n\n## 礼包内容\n\n- 钻石套装 × 1\n- 2000 游戏币\n");
}
/** 从任意文件夹加载任务列表 */
public List<Map<String, Object>> loadTasksFromFolder(
File folder) {
List<Map<String, Object>> tasks =
new ArrayList<>();
if (!folder.exists() || !folder.isDirectory())
return tasks;
File[] files = folder.listFiles(
(dir, name) -> name.endsWith(".md"));
if (files == null) return tasks;
Arrays.sort(files);
for (File f : files) {
Map<String, Object> task =
new LinkedHashMap<>();
task.put("name",
f.getName().replace(".md", ""));
task.put("conditions",
new ArrayList<String>());
task.put("rewards",
new ArrayList<String>());
try {
BufferedReader r = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8));
String line;
List<String> current = null;
while ((line = r.readLine()) != null) {
if (line.startsWith("# ")) {
task.put("name",
line.substring(2)
.trim());
} else if (line.contains("条件")
|| line.contains("领取条件")
|| line.contains("完成条件")) {
current = (List<String>) task
.get("conditions");
} else if (line.contains("内容")
|| line.contains("奖励")
|| line.contains("任务内容")
|| line.contains("礼包内容")) {
current = (List<String>) task
.get("rewards");
} else if (line.startsWith("- ")
&& current != null) {
current.add(line.substring(2)
.trim());
}
}
r.close();
} catch (Exception e) {
e.printStackTrace();
}
tasks.add(task);
}
return tasks;
}
public boolean canClaim(Player p, int stage) {
String name = p.getName();
int curStage = ((Number) plugin.getDb()
.getField(name, "gift_stage"))
.intValue();
String claimed = (String) plugin.getDb()
.getField(name, "gift_claimed");
if (claimed == null) claimed = "";
return curStage >= stage
&& !claimed.contains(
String.valueOf(stage));
}
public void claimReward(Player p, int stage) {
if (!canClaim(p, stage)) return;
Map<String, Object> user = plugin.getDb()
.getUser(p.getName());
String claimed = (String) user.getOrDefault(
"gift_claimed", "");
claimed += "[" + stage + "]";
plugin.getDb().setField(
p.getName(), "gift_claimed", claimed);
plugin.getDb().setField(
p.getName(), "gift_stage",
Math.max(stage,
((Number) user.getOrDefault(
"gift_stage", 0))
.intValue()));
if (stage == 1) {
giveItem(p, Material.LEATHER_HELMET);
giveItem(p, Material.LEATHER_CHESTPLATE);
giveItem(p, Material.LEATHER_LEGGINGS);
giveItem(p, Material.LEATHER_BOOTS);
giveItem(p, Material.WOODEN_SWORD);
giveItem(p, Material.WOODEN_PICKAXE);
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);
}
} 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);
}
giveItem(p, Material.IRON_PICKAXE);
giveItem(p, Material.IRON_AXE);
giveItem(p, Material.IRON_SWORD);
giveItem(p, Material.IRON_HELMET);
giveItem(p, Material.IRON_CHESTPLATE);
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);
}
giveItem(p, Material.DIAMOND_HELMET);
giveItem(p, Material.DIAMOND_CHESTPLATE);
giveItem(p, Material.DIAMOND_LEGGINGS);
giveItem(p, Material.DIAMOND_BOOTS);
}
p.sendMessage(plugin.getConfig2().msg(
"gift_claimed"));
}
private void giveItem(Player p, Material mat) {
p.getInventory().addItem(new ItemStack(mat));
}
public List<Map<String, Object>> getStages() {
return stages;
}
public int getMaxStage() {
return stages.size();
}
}
@@ -1,110 +0,0 @@
package Sdf1_login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class IPGroupManager {
private final Main plugin;
private final int maxAccounts;
public IPGroupManager(Main plugin, int maxAccounts) {
this.plugin = plugin;
this.maxAccounts = maxAccounts;
}
public boolean canRegister(String ip) {
if (maxAccounts <= 0) return true;
if (ip == null || ip.isEmpty()) return true;
int count = getAccountCount(ip);
boolean can = count < maxAccounts;
plugin.getLogger().info(
"[IP限制] IP=" + ip
+ " 已注册=" + count
+ " 上限=" + maxAccounts
+ " 允许=" + can);
return can;
}
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
}
public List<String> getAccounts(String ip) {
return queryAccountsByIP(ip);
}
private List<String> queryAccountsByIP(String ip) {
List<String> accounts = new ArrayList<>();
Connection conn = plugin.getDb().getDb();
if (conn == null) return accounts;
try {
// 主查询:ip_address 列
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
accounts.add(
rs.getString("player_name"));
}
rs.close();
ps.close();
// 兜底:如果主列没数据,查 last_login_ip
if (accounts.isEmpty()) {
PreparedStatement ps2 = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE last_login_ip = ? "
+ "AND (ip_address IS NULL "
+ "OR ip_address = '' "
+ "OR ip_address != ?)");
ps2.setString(1, ip);
ps2.setString(2, ip);
ResultSet rs2 = ps2.executeQuery();
while (rs2.next()) {
String name = rs2.getString("player_name");
if (!accounts.contains(name))
accounts.add(name);
}
rs2.close();
ps2.close();
}
// 兜底2:查 register_ip
if (accounts.isEmpty()) {
PreparedStatement ps3 = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE register_ip = ? "
+ "AND (ip_address IS NULL "
+ "OR ip_address = '') "
+ "AND (last_login_ip IS NULL "
+ "OR last_login_ip = '' "
+ "OR last_login_ip != ?)");
ps3.setString(1, ip);
ps3.setString(2, ip);
ResultSet rs3 = ps3.executeQuery();
while (rs3.next()) {
String name = rs3.getString("player_name");
if (!accounts.contains(name))
accounts.add(name);
}
rs3.close();
ps3.close();
}
plugin.getLogger().info(
"[IP限制] 查询IP=" + ip
+ " 结果数=" + accounts.size()
+ " 账号=" + accounts);
} catch (Exception e) {
e.printStackTrace();
}
return accounts;
}
}
@@ -1,46 +0,0 @@
package Sdf1_login;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
public class InventorySerializer {
public static String toBase64(ItemStack[] items) throws IOException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(items.length);
for (ItemStack item : items) {
dataOutput.writeObject(item);
}
dataOutput.close();
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception e) {
throw new IOException("Unable to serialize item stacks.", e);
}
}
public static ItemStack[] fromBase64(String data) throws IOException {
if (data == null || data.isEmpty()) {
return new ItemStack[0];
}
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int i = 0; i < items.length; i++) {
items[i] = (ItemStack) dataInput.readObject();
}
dataInput.close();
return items;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to deserialize item stacks.", e);
}
}
}
@@ -1,96 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.text.SimpleDateFormat;
import java.util.*;
public class InviteManager {
private final Main plugin;
public InviteManager(Main plugin) {
this.plugin = plugin;
}
public String generateCode(Player p) {
String code = PasswordUtils.generateInviteCode();
plugin.getDb().setField(
p.getName(), "invite_code", code);
return code;
}
public void onInviteeCheckIn(String inviteeName) {
String invitedBy = (String) plugin.getDb()
.getField(inviteeName, "invited_by");
if (invitedBy == null || invitedBy.isEmpty())
return;
String month = new SimpleDateFormat("yyyy-MM")
.format(new Date());
String lastMonth = (String) plugin.getDb()
.getField(invitedBy, "invite_month");
if (!month.equals(lastMonth)) {
plugin.getDb().setField(
invitedBy, "invite_month", month);
plugin.getDb().setField(
invitedBy, "monthly_invite_count", 0);
}
int count = ((Number) plugin.getDb()
.getField(invitedBy,
"monthly_invite_count"))
.intValue();
plugin.getDb().setField(
invitedBy, "monthly_invite_count",
count + 1);
double rate = 0.10;
if (count + 1 > 15) {
rate = 0.60;
} else if (count + 1 > 10) {
rate = 0.35;
} else if (count + 1 > 5) {
rate = 0.15;
}
int bonus = (int) (1 * rate);
if (bonus > 0) {
plugin.getDb().addPoints(invitedBy, bonus);
Player inviterPlayer = Bukkit
.getPlayerExact(invitedBy);
if (inviterPlayer != null
&& inviterPlayer.isOnline()) {
inviterPlayer.sendMessage(
plugin.getConfig2()
.msg("invite_referral_bonus")
.replace("{points}",
String.valueOf(bonus)));
}
}
}
public List<Map<String, Object>> getAllInviteData() {
List<Map<String, Object>> result =
new ArrayList<>();
for (Map<String, Object> user
: plugin.getDb().getAllUsers()) {
String code = (String) user.getOrDefault(
"invite_code", "");
if (!code.isEmpty()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("player",
user.get("player_name"));
row.put("code", code);
row.put("monthly",
user.getOrDefault(
"monthly_invite_count", 0));
result.add(row);
}
}
return result;
}
}
@@ -1,95 +0,0 @@
package Sdf1_login;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class IronBlockElevator implements Listener {
private static final int MAX_RANGE = 64;
private static final int MIN_RANGE = 5; // 屏蔽±5格以内的铁块
private final Set<UUID> cooldown = new HashSet<>();
/**
* 检查指定位置脚下是否是铁块
*/
private boolean isIronBlockBelow(Location loc) {
Block block = loc.getWorld().getBlockAt(
loc.getBlockX(), loc.getBlockY() - 1,
loc.getBlockZ());
return block.getType() == Material.IRON_BLOCK;
}
/**
* 从起始位置向上或向下搜索铁块并传送
* @param dir 1=向上 -1=向下
*/
private boolean tryTeleport(Player p, int dir) {
Location loc = p.getLocation();
int baseY = loc.getBlockY();
for (int i = MIN_RANGE; i < MAX_RANGE; i++) {
int y = baseY + i * dir;
Block block = loc.getWorld().getBlockAt(
loc.getBlockX(), y, loc.getBlockZ());
if (block.getType() == Material.IRON_BLOCK) {
cooldown.add(p.getUniqueId());
Location dest = new Location(
loc.getWorld(),
loc.getX(),
block.getY() + 1,
loc.getZ());
p.teleport(dest);
org.bukkit.Bukkit.getScheduler()
.runTaskLater(
org.bukkit.Bukkit.getPluginManager()
.getPlugin("Sdf1_login"),
() -> cooldown.remove(
p.getUniqueId()),
15L);
return true;
}
}
return false;
}
// ===== 下蹲 → 向下传送 =====
@EventHandler
public void onSneak(PlayerToggleSneakEvent e) {
if (!e.isSneaking()) return;
Player p = e.getPlayer();
if (cooldown.contains(p.getUniqueId())) return;
// 必须站在铁块上
if (!isIronBlockBelow(p.getLocation())) return;
tryTeleport(p, -1);
}
// ===== 跳跃 → 向上传送 =====
@EventHandler
public void onMove(PlayerMoveEvent e) {
Player p = e.getPlayer();
if (cooldown.contains(p.getUniqueId())) return;
if (p.isSneaking()) return;
Location from = e.getFrom();
Location to = e.getTo();
if (to == null) return;
if (to.getY() <= from.getY()) return;
if (p.getVelocity().getY() <= 0) return;
// 关键:检查跳跃前的位置(from)脚下是否是铁块
// 跳跃后玩家已离开铁块上方,检查 to 会失败
if (!isIronBlockBelow(from)) return;
tryTeleport(p, 1);
}
}
@@ -1,311 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
public class LoginManager {
private final Main plugin;
public LoginManager(Main plugin) {
this.plugin = plugin;
}
public boolean handleRegister(Player p,
String[] args) {
String name = p.getName();
// 已注册玩家不能再注册
if (plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_registered"));
return true;
}
// IP注册限制检查
String ip = plugin.getPlayerIP(p);
if (ip != null && plugin.getConfig2()
.maxAccountsPerIP > 0) {
if (!plugin.getIPGroup()
.canRegister(ip)) {
int count = plugin.getIPGroup()
.getAccountCount(ip);
int max = plugin.getConfig2()
.maxAccountsPerIP;
p.sendMessage("§c该IP注册已达上限("
+ count + "/" + max + ")");
return true;
}
}
if (args.length < 1) {
p.sendMessage("§c用法: /reg <密码>");
p.sendMessage("§7密码要求: 6位以上,"
+ "包含大小写字母和数字");
return true;
}
String pwd = args[0];
if (pwd.length() < 6) {
p.sendMessage("§c密码长度不足6位");
return true;
}
if (!pwd.matches(".*[a-z].*")
|| !pwd.matches(".*[A-Z].*")
|| !pwd.matches(".*[0-9].*")) {
p.sendMessage("§c密码必须包含大写字母、"
+ "小写字母和数字");
return true;
}
String salt =
PasswordUtils.generateSalt();
String hash =
PasswordUtils.hash(pwd, salt);
plugin.getDb().createUser(
name, hash, salt);
plugin.getLoggedIn().add(name);
p.setAllowFlight(false);
p.setFlying(false);
plugin.getDb().setLoggedIn(name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.getDb().recordIP(name, ip);
plugin.getDb().setField(name,
"register_time",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("reg_success"));
playRegisterSound(p);
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 9, 0);
}
} catch (Exception ignored) {
}
return true;
}
private void playRegisterSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
public boolean handleLogin(Player p,
String[] args) {
String name = p.getName();
// ★ 在 handleLogin 方法开头
if (plugin.getNeedsPasswordChange().contains(name)
&& plugin.getLoggedIn().contains(name)) {
// 已登录但还没改密码,只允许执行 /sdf1_login pw
p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>");
return true;
}
try {
if (plugin.getLoggedIn().contains(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_logged_in"));
return true;
}
if (!plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("not_registered"));
return true;
}
if (args.length < 1) {
p.sendMessage("§c用法: /login <密码>");
return true;
}
String pwd = args[0];
String salt = (String) plugin.getDb()
.getField(name, "password_salt");
if (salt == null) {
p.sendMessage(
"§c数据异常,请联系管理员");
plugin.getLogger().severe(
"[Sdf1_login] 玩家 " + name
+ " 盐值为null");
return true;
}
String hash =
PasswordUtils.hash(pwd, salt);
boolean matchMain =
plugin.getDb().checkPassword(
name, hash);
if (matchMain) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
// 只在有有效备份时恢复
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
activateCY(p, name);
return true;
}
boolean matchTemp =
plugin.getDb()
.checkPasswordOrTemp(
name, hash);
if (matchTemp) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(name, true);
p.setAllowFlight(false);
p.setFlying(false);
plugin.getDb().setField(name, "last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name, "last_online_check",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
// ★ 确保这行存在
plugin.getNeedsPasswordChange().add(name);
p.sendMessage(plugin.getConfig2().msg("login_success"));
p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage("§7用法: /sdf1_login pw");
activateCY(p, name);
return true;
}
p.sendMessage(plugin.getConfig2()
.msg("login_failed"));
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_login] handleLogin异常: "
+ e.getMessage());
e.printStackTrace();
p.sendMessage(
"§c登录过程出错,请联系管理员");
}
return true;
}
public void handleReset(Player p) {
String name = p.getName();
String emailAddr = (String)
plugin.getDb().getField(
name, "email");
if (emailAddr == null
|| emailAddr.isEmpty()) {
p.sendMessage(
"§c未设置邮箱,无法重置密码");
return;
}
String tempPwd = PasswordUtils
.generateTempPassword();
String salt = (String)
plugin.getDb().getField(
name, "password_salt");
String hash = PasswordUtils.hash(
tempPwd, salt);
plugin.getDb().setField(name,
"temp_password", hash);
plugin.getDb().setField(name,
"temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
plugin.getDb().setField(name,
"temp_pw_used", 0);
p.sendMessage("§e正在发送临时密码...");
final String to = emailAddr;
final String fName = name;
final String pwd = tempPwd;
final Main self = plugin;
Bukkit.getScheduler()
.runTaskAsynchronously(self,
() -> {
final boolean[] sent = {false};
Thread t = new Thread(() -> {
sent[0] = self.getEmail()
.sendTempPassword(
to, fName, pwd);
}, "Sdf1_login-reset");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (
InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
Bukkit.getScheduler().runTask(
self, () ->
p.sendMessage(
"§c邮件发送超时"));
return;
}
Bukkit.getScheduler().runTask(
self, () -> {
if (sent[0]) {
p.sendMessage(
"§a临时密码已发送到 "
+ to);
} else {
p.sendMessage(
"§c邮件发送失败");
}
});
});
}
private void activateCY(Player p,
String name) {
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 0, 0);
}
} catch (Exception ignored) {
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,152 +0,0 @@
package Sdf1_login;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
public class MenuIconManager {
private final Main plugin;
private final NamespacedKey iconKey;
public MenuIconManager(Main plugin) {
this.plugin = plugin;
this.iconKey = new NamespacedKey(
plugin, "custom_menu_icon");
}
// ========== GUIManager 调用的方法 ==========
/**
* Base64 → ItemStack
* 对应 GUIManager 中的
* plugin.getMenuIconMgr().deserializeItem(...)
*/
public ItemStack deserializeItem(String b64) {
if (b64 == null || b64.isEmpty())
return null;
try {
byte[] data =
Base64.getDecoder().decode(b64);
ByteArrayInputStream bais =
new ByteArrayInputStream(data);
BukkitObjectInputStream is =
new BukkitObjectInputStream(bais);
Object obj = is.readObject();
is.close();
if (obj instanceof ItemStack)
return (ItemStack) obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* ItemStack → Base64
* 对应 GUIManager 中的
* plugin.getMenuIconMgr().serializeItem(...)
*/
public String serializeItem(ItemStack item) {
if (item == null) return null;
try {
ByteArrayOutputStream baos =
new ByteArrayOutputStream();
BukkitObjectOutputStream os =
new BukkitObjectOutputStream(baos);
os.writeObject(item);
os.close();
return Base64.getEncoder()
.encodeToString(
baos.toByteArray());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 给物品打上菜单图标标记
* 对应 GUIManager 中的
* plugin.getMenuIconMgr().ensureMenuIconTag(...)
*/
public void ensureMenuIconTag(ItemStack item) {
if (item == null) return;
ItemMeta meta = item.getItemMeta();
if (meta == null) return;
meta.getPersistentDataContainer().set(
iconKey,
PersistentDataType.STRING,
"menu");
item.setItemMeta(meta);
}
/**
* 获取图标Key
* 对应 GUIManager 中的
* plugin.getMenuIconMgr().getIconKey()
*/
public NamespacedKey getIconKey() {
return iconKey;
}
// ========== LoginManager / Main 调用的方法 ==========
/**
* 获取玩家自定义图标物品
* 有返回物品,无返回null
*/
public ItemStack getIcon(Player player) {
String b64 = plugin.getDb()
.getMenuIcon(player.getName());
if (b64 == null || b64.isEmpty())
return null;
ItemStack item = deserializeItem(b64);
if (item == null) return null;
ensureMenuIconTag(item);
return item;
}
/**
* 保存图标到DB
*/
public void saveIcon(Player player,
ItemStack item) {
String b64 = serializeItem(item);
if (b64 != null) {
plugin.getDb().saveMenuIcon(
player.getName(), b64,
item.getType().name());
}
}
/**
* 删除图标
*/
public void removeIcon(Player player) {
plugin.getDb().deleteMenuIcon(
player.getName());
}
/**
* 检查物品是否是自定义菜单图标
*/
public boolean isCustomMenuIcon(
ItemStack item) {
if (item == null) return false;
ItemMeta meta = item.getItemMeta();
if (meta == null) return false;
return meta.getPersistentDataContainer()
.has(iconKey,
PersistentDataType.STRING);
}
}
@@ -1,452 +0,0 @@
package Sdf1_login;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.*;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.file.Files;
public class MenuManager implements Listener {
private final Main plugin;
private final List<MenuItem> items =
new ArrayList<>();
private final Set<String> chatWait =
ConcurrentHashMap.newKeySet();
private final Map<String, String> chatField =
new ConcurrentHashMap<>();
private final Map<String, Integer> chatIdx =
new ConcurrentHashMap<>();
// ===== 菜单项 =====
public static class MenuItem {
public String title = "新菜单项";
public String command = "";
public String permType = "玩家";
public Integer slot = null;
public ItemStack icon;
public MenuItem() {
icon = randomIcon();
}
public MenuItem(String t,
String c, String p) {
title = t;
command = c;
permType = p;
icon = randomIcon();
}
static ItemStack randomIcon() {
Material[] pool = {
Material.COMPASS,
Material.ENDER_PEARL,
Material.DIAMOND,
Material.GOLD_INGOT,
Material.EMERALD,
Material.NETHER_STAR,
Material.BEACON,
Material.ENCHANTING_TABLE,
Material.CHEST,
Material.FIREWORK_ROCKET,
Material.BOOK,
Material.ELYTRA,
Material.TOTEM_OF_UNDYING
};
Material m = pool[
(int) (Math.random()
* pool.length)];
ItemStack it =
new ItemStack(m);
ItemMeta im = it.getItemMeta();
im.setDisplayName("§e默认图标");
it.setItemMeta(im);
return it;
}
}
public MenuManager(Main plugin) {
this.plugin = plugin;
}
public List<MenuItem> getItems() {
return items;
}
// ===== 读取菜单.txt =====
public void loadMenu() {
items.clear();
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
if (!f.exists()) createDefault(f);
parseFile(f);
plugin.getLogger().info(
"[Menu] 加载 "
+ items.size()
+ " 个菜单项");
}
private void parseFile(File f) {
plugin.getLogger().info("[Menu] ===== 扫描 =====");
plugin.getLogger().info("[Menu] 路径: "
+ f.getAbsolutePath());
plugin.getLogger().info("[Menu] 大小: "
+ f.length());
try {
List<String> lines = Files.readAllLines(
f.toPath(), StandardCharsets.UTF_8);
plugin.getLogger().info("[Menu] 总行数: "
+ lines.size());
boolean inBlock = false;
boolean inHtml = false;
StringBuilder buf = new StringBuilder();
boolean inItem = false;
int parsed = 0;
for (int lineNum = 1;
lineNum <= lines.size();
lineNum++) {
String raw = lines.get(lineNum - 1);
String l = raw.trim();
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 原文[" + l.length()
+ "字符] " + l);
// ===== 第1优先:跨行块注释 =====
if (inBlock) {
if (l.contains("*/")) {
inBlock = false;
l = l.substring(
l.indexOf("*/") + 2)
.trim();
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 块注释结束");
} else {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [块注释中]");
continue;
}
}
// ===== 第2优先:跨行HTML注释 =====
if (inHtml) {
if (l.contains("-->")) {
inHtml = false;
l = l.substring(
l.indexOf("-->") + 3)
.trim();
} else {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [HTML注释中]");
continue;
}
}
// ===== 第3优先:整行注释(最高裁决) =====
// 以 # 或 // 开头的行,直接跳过
// 不管后面包含什么 /* <!-- 等
if (l.startsWith("#")
|| l.startsWith("//")) {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [整行注释,跳过]");
continue;
}
// ===== 第4优先:块注释开始 =====
if (l.contains("/*")) {
l = l.substring(0, l.indexOf("/*"))
.trim();
inBlock = true;
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 进入/*块注释"
+ " 剩余[" + l + "]");
if (l.isEmpty()) {
continue;
}
}
// ===== 第5优先:HTML注释开始 =====
if (l.contains("<!--")) {
l = l.substring(0,
l.indexOf("<!--")).trim();
inHtml = true;
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 进入<!--注释");
if (l.isEmpty()) {
continue;
}
}
// ===== 第6优先:行内注释 =====
int hashPos = l.indexOf("#");
int slashPos = l.indexOf("//");
if (hashPos >= 0) {
l = l.substring(0, hashPos).trim();
}
if (slashPos >= 0) {
if (hashPos < 0
|| slashPos < hashPos) {
l = l.substring(0, slashPos)
.trim();
}
}
if (l.isEmpty()) {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 空");
continue;
}
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [保留] " + l);
if (l.equals("{")) {
inItem = true;
buf.setLength(0);
plugin.getLogger().info(
"[Menu] === 块开始 ===");
continue;
}
if (l.equals("}")) {
if (inItem) {
plugin.getLogger().info(
"[Menu] === 块结束 ===");
parseBlock(buf.toString());
parsed++;
plugin.getLogger().info(
"[Menu] 当前菜单项: "
+ items.size());
}
inItem = false;
continue;
}
if (inItem) {
buf.append(l).append("\n");
}
}
plugin.getLogger().info(
"[Menu] ===== 完毕 =====");
plugin.getLogger().info(
"[Menu] 块数: " + parsed);
plugin.getLogger().info(
"[Menu] 菜单项: "
+ items.size());
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 异常: "
+ e.getMessage());
}
}
private void parseBlock(String block) {
String title = "";
String cmd = "";
String perm = "玩家";
String iconMat = "";
int slotNum = -1;
for (String line : block.split("\n")) {
line = line.trim();
if (line.isEmpty()) continue;
String v = "";
int idx = line.indexOf(":");
if (idx < 0) idx = line.indexOf(":");
if (idx >= 0) {
v = line.substring(idx + 1).trim();
if (v.startsWith("\"")
&& v.endsWith("\"")
&& v.length() >= 2) {
v = v.substring(1, v.length() - 1);
}
}
if (line.startsWith("标题")) {
title = v;
} else if (line.startsWith("指令")) {
cmd = v;
if (!cmd.isEmpty()
&& !cmd.startsWith("/")) {
cmd = "/" + cmd;
}
} else if (line.startsWith("权限")) {
perm = v;
} else if (line.startsWith("图标")) {
iconMat = v;
} else if (line.startsWith("格子")) {
try {
int s = Integer.parseInt(v);
if (s >= 1 && s <= 54) {
slotNum = s;
}
} catch (NumberFormatException ignored) {
}
}
}
if (title.isEmpty()) return;
MenuItem mi = new MenuItem(title, cmd, perm);
if (slotNum >= 1 && slotNum <= 54) {
mi.slot = slotNum;
}
if (!iconMat.isEmpty()) {
try {
String matName = iconMat
.replace("minecraft:", "")
.replace("bukkit:", "")
.trim().toUpperCase();
Material m =
Material.valueOf(matName);
mi.icon = new ItemStack(m);
ItemMeta im =
mi.icon.getItemMeta();
im.setDisplayName(
"§e" + title);
mi.icon.setItemMeta(im);
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 无法解析: "
+ iconMat);
}
}
items.add(mi);
}
private void createDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets
.UTF_8))) {
pw.println("# 服务器菜单");
pw.println("# {}定义菜单项");
pw.println("# 支持"
+ " # // /* */ <!-- -->");
pw.println();
pw.println("{");
pw.println(" 标题:主城传送");
pw.println(" 指令:/spawn");
pw.println(" 权限类型:玩家");
pw.println(" 图标:COMPASS");
pw.println("}");
pw.println();
pw.println("{");
pw.println(" 标题:随机传送");
pw.println(" 指令:/rtp");
pw.println(" 权限类型:玩家");
pw.println(" 图标:ENDER_PEARL");
pw.println("}");
} catch (Exception ignored) {
}
}
// ===== 保存 =====
public void saveMenu() {
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# 服务器菜单");
pw.println();
for (MenuItem mi : items) {
pw.println("{");
pw.println(" 标题:"
+ mi.title);
pw.println(" 指令:"
+ mi.command);
pw.println(" 权限类型:"
+ mi.permType);
// ★ 保存实际材质名 ★
pw.println(" 图标:"
+ mi.icon.getType().name());
pw.println("}");
pw.println();
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 保存失败: "
+ e.getMessage());
}
}
// ===== 聊天编辑 =====
public boolean isEditing(String name) {
return chatWait.contains(name);
}
public void editTitle(
Player p, int idx) {
chatIdx.put(p.getName(), idx);
chatWait.add(p.getName());
chatField.put(p.getName(), "title");
p.closeInventory();
p.sendMessage("§e请输入菜单标题:");
}
public void editCommand(
Player p, int idx) {
chatIdx.put(p.getName(), idx);
chatWait.add(p.getName());
chatField.put(p.getName(), "command");
p.closeInventory();
p.sendMessage("§e请输入指令"
+ "(自动补全/):");
}
public void onChat(Player p, String input) {
String name = p.getName();
if (!chatWait.remove(name)) return;
String field = chatField.remove(name);
Integer idx = chatIdx.get(name);
if (idx == null || idx >= items.size()) return;
MenuItem mi = items.get(idx);
if ("title".equals(field)) {
mi.title = input;
p.sendMessage("§a标题: " + input);
} else if ("command".equals(field)) {
if (!input.startsWith("/"))
input = "/" + input;
mi.command = input;
p.sendMessage("§a指令: " + input);
}
Bukkit.getScheduler().runTask(plugin, () ->
plugin.gui.openEditor(p, idx));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
package Sdf1_login;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class PasswordUtils {
private static final SecureRandom RANDOM =
new SecureRandom();
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public static String hash(String password, String salt) {
try {
MessageDigest md =
MessageDigest.getInstance("SHA-256");
md.update(Base64.getDecoder().decode(salt));
byte[] hash =
md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean validate(String password) {
if (password == null) return false;
if (password.length() < 6 || password.length() > 20)
return false;
boolean hasUpper = false;
boolean hasLower = false;
boolean hasDigit = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
if (Character.isLowerCase(c)) hasLower = true;
if (Character.isDigit(c)) hasDigit = true;
}
return hasUpper && hasLower && hasDigit;
}
public static String generateTempPassword() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateInviteCode() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < 6; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateVerifyCode() {
int code = 100000 + RANDOM.nextInt(900000);
return String.valueOf(code);
}
}
@@ -1,130 +0,0 @@
package Sdf1_login;
import java.io.File;
import java.sql.*;
import java.util.logging.Logger;
public class PlayerStorage {
private Connection conn;
private final Logger logger;
public PlayerStorage(File dataFolder,
Logger logger) {
this.logger = logger;
File dbFile = new File(
dataFolder,
"quest_data.db");
try {
Class.forName(
"org.sqlite.JDBC");
conn = DriverManager
.getConnection(
"jdbc:sqlite:"
+ dbFile
.getAbsolutePath());
conn.setAutoCommit(true);
Statement s =
conn.createStatement();
s.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_kv ("
+ "player TEXT NOT NULL,"
+ "key TEXT NOT NULL,"
+ "value TEXT,"
+ "PRIMARY KEY"
+ "(player, key))");
s.close();
logger.info(
"[QuestStorage] 初始化: "
+ dbFile.getAbsolutePath());
} catch (Exception e) {
logger.severe(
"[QuestStorage] 失败: "
+ e.getMessage());
}
}
public Object getCondition(
String player, String key) {
if (conn == null) return null;
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value "
+ "FROM quest_kv "
+ "WHERE player=? "
+ "AND key=?");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ResultSet rs =
ps.executeQuery();
String val = rs.next()
? rs.getString("value")
: null;
rs.close();
ps.close();
return parseValue(val);
} catch (SQLException e) {
return null;
}
}
public void setCondition(
String player, String key) {
setConditionRaw(
player, key, "true");
}
public void debugPlayer(String player) {
}
public void setConditionRaw(
String player, String key,
String value) {
if (conn == null) return;
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO quest_kv"
+ "(player,key,value)"
+ " VALUES(?,?,?)");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ps.setString(3, value);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
logger.warning(
"[QuestStorage] 写入失败: "
+ key + "=" + value
+ " | " + e.getMessage());
}
}
public void close() {
try {
if (conn != null
&& !conn.isClosed())
conn.close();
} catch (Exception e) {}
}
private Object parseValue(String v) {
if (v == null) return null;
try {
return Long.parseLong(v);
} catch (Exception e1) {
try {
return Double.parseDouble(v);
} catch (Exception e2) {
if ("true".equalsIgnoreCase(v))
return true;
if ("false".equalsIgnoreCase(v))
return false;
return v;
}
}
}
}
@@ -1,374 +0,0 @@
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 java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Method;
import java.util.*;
public class PointsManager {
private final Main plugin;
private Object cyInstance = null;
private Method cyActivationMethod = null;
private Method cyGetShopItemsMethod = null;
private final List<Map<String, Object>> cyItems =
new ArrayList<>();
public PointsManager(Main plugin) {
this.plugin = plugin;
discoverCY();
}
private void discoverCY() {
try {
org.bukkit.plugin.Plugin p = Bukkit
.getPluginManager()
.getPlugin("CY_beibao");
if (p != null && p.isEnabled()) {
cyInstance = p;
cyGetShopItemsMethod = p.getClass()
.getMethod("onSdf1GetShopItems");
cyActivationMethod = p.getClass()
.getMethod("onSdf1Activation",
String.class,
int.class, int.class);
plugin.getLogger().info(
"[Sdf1_login] CY_beibao 联控已连接");
}
} catch (Exception ignored) {
}
}
private void refreshCYItems() {
cyItems.clear();
if (cyInstance == null
|| cyGetShopItemsMethod == null) return;
try {
Object result =
cyGetShopItemsMethod.invoke(cyInstance);
if (result == null) return;
String data = result.toString();
if (data.isEmpty()) return;
String[] items = data.split(";");
for (String itemStr : items) {
String[] p = itemStr.split("\\|");
if (p.length < 8) continue;
Map<String, Object> item =
new LinkedHashMap<>();
item.put("id", p[0]);
item.put("name", p[1]);
item.put("days",
Integer.parseInt(p[2]));
item.put("slots",
Integer.parseInt(p[3]));
item.put("price",
Integer.parseInt(p[4]));
item.put("stock",
Integer.parseInt(p[5]));
item.put("icon", p[6]);
item.put("lifetime",
"1".equals(p[7]));
cyItems.add(item);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 拉取CY商品失败: "
+ e.getMessage());
}
}
private void activateCY(String name,
int slots, int days) {
try {
if (cyInstance == null
|| cyActivationMethod == null) return;
cyActivationMethod.invoke(
cyInstance, name, slots, days);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] CY激活失败: "
+ e.getMessage());
}
}
public int getPoints(String name) {
return ((Number) plugin.getDb()
.getField(name, "points")).intValue();
}
public Inventory createShopGUI(Player p) {
int points = getPoints(p.getName());
Inventory inv = Bukkit.createInventory(
null, 54,
"§d§l积分商城 | 当前积分: " + points);
fillBg(inv);
inv.setItem(10, mkItem(Material.EMERALD,
"§a游戏币充值",
"§7消耗积分兑换游戏币",
"§7100积分 = $1000",
"",
"§e点击兑换"));
inv.setItem(12, mkItem(Material.ENDER_CHEST,
"§b背包空间",
"§7消耗积分兑换背包格子",
"§750积分 = 9格永久空间",
"",
"§e点击兑换"));
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
"§e经验瓶",
"§7消耗积分兑换经验",
"§720积分 = 10级经验",
"",
"§e点击兑换"));
if (cyInstance != null) {
inv.setItem(31, mkItem(Material.CHEST,
"§6背包商城",
"§7来自CY_beibao的商品",
"",
"§e点击查看"));
}
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
return inv;
}
public Inventory createCYShopGUI(Player p) {
refreshCYItems();
int points = getPoints(p.getName());
int size = Math.max(54,
((cyItems.size() / 9) + 1) * 9);
if (size > 54) size = 54;
Inventory inv = Bukkit.createInventory(
null, size,
"§d§lCY背包商城 | 当前积分: " + points);
fillBg(inv);
for (int i = 0; i < cyItems.size()
&& i < 45; i++) {
Map<String, Object> item = cyItems.get(i);
String name = (String) item.get("name");
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
List<String> lore = new ArrayList<>();
lore.add("§7价格: §e" + price + " 积分");
if (lifetime) {
lore.add("§7格子: §e终身 +" + slots);
} else {
lore.add("§7格子: §e+" + slots);
lore.add("§7时间: §e" + days + "天");
}
if (stock == 0) {
lore.add("§7库存: §a无限");
} else if (stock == -1) {
lore.add("§7库存: §c已下架");
} else {
lore.add("§7库存: §e" + stock);
}
lore.add("");
if (points >= price && stock != -1) {
lore.add("§e点击购买");
} else if (stock == -1) {
lore.add("§c已下架");
} else {
lore.add("§c积分不足");
}
inv.setItem(i, mkItem(Material.CHEST,
"§a" + name,
lore.toArray(new String[0])));
}
inv.setItem(size - 1,
mkItem(Material.ARROW, "§7返回"));
return inv;
}
public void addItem(String name,
int price, String cmd2) {
// 追加到商品配置文件
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.io.FileWriter fw =
new java.io.FileWriter(file, true);
fw.write(name + ":" + price
+ ":" + cmd2 + "\n");
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void removeItem(int index) {
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.util.List<String> lines =
new java.util.ArrayList<>();
java.io.BufferedReader br =
new java.io.BufferedReader(
new java.io.FileReader(
file));
String line;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()
|| line.startsWith("#")) {
lines.add(line);
continue;
}
if (i != index) {
lines.add(line);
}
i++;
}
br.close();
java.io.PrintWriter pw =
new java.io.PrintWriter(file);
for (String l : lines) pw.println(l);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void handleClick(Player p, int slot) {
int points = getPoints(p.getName());
switch (slot) {
case 10:
if (points >= 100) {
plugin.getDb().deductPoints(
p.getName(), 100);
if (plugin.getEconomy() != null) {
plugin.getEconomy()
.depositPlayer(p, 1000);
}
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 12:
if (points >= 50) {
plugin.getDb().deductPoints(
p.getName(), 50);
activateCY(p.getName(), 9, 0);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "50"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 14:
if (points >= 20) {
plugin.getDb().deductPoints(
p.getName(), 20);
p.setLevel(p.getLevel() + 10);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "20"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 31:
if (cyInstance != null) {
p.openInventory(
createCYShopGUI(p));
}
break;
}
}
public void handleCYClick(Player p, int slot) {
if (slot < 0 || slot >= cyItems.size()
|| slot >= 45) return;
Map<String, Object> item = cyItems.get(slot);
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
if (stock == -1) {
p.sendMessage("§c该商品已下架");
return;
}
int points = getPoints(p.getName());
if (points < price) {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
return;
}
plugin.getDb().deductPoints(p.getName(), price);
int actDays = lifetime ? 0 : days;
activateCY(p.getName(), slots, actDays);
String tl = lifetime
? "终身" : (days + "天");
p.sendMessage("§a购买成功!获得 "
+ tl + " +" + slots + "格 背包空间");
}
private void fillBg(Inventory inv) {
ItemStack gl = mkItem(
Material.GRAY_STAINED_GLASS_PANE, " ");
for (int i = 0; i < inv.getSize(); i++)
inv.setItem(i, gl);
}
private ItemStack mkItem(Material mat, String name,
String... lore) {
ItemStack it = new ItemStack(mat);
ItemMeta im = it.getItemMeta();
if (im != null) {
im.setDisplayName(name);
if (lore.length > 0)
im.setLore(Arrays.asList(lore));
it.setItemMeta(im);
}
return it;
}
}
@@ -1,138 +0,0 @@
package Sdf1_login;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class QuestStorage {
private Connection conn;
public QuestStorage(File dataFolder) {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
dataFolder,
"quest_data.db");
conn = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getPath());
initTable();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initTable()
throws SQLException {
Statement stmt =
conn.createStatement();
stmt.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_conditions ("
+ "player TEXT NOT NULL, "
+ "condition_key TEXT NOT NULL, "
+ "value TEXT DEFAULT '1', "
+ "timestamp INTEGER, "
+ "PRIMARY KEY "
+ "(player, condition_key))");
stmt.close();
}
/**
* 标记条件已完成(value = "1")
*/
public void setCondition(
String player,
String condition) {
setConditionRaw(
player, condition, "1");
}
/**
* 写入任意值(计数器等)
*/
public void setConditionRaw(
String player,
String condition,
String value) {
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO "
+ "quest_conditions "
+ "(player, "
+ "condition_key, "
+ "value, timestamp) "
+ "VALUES "
+ "(?, ?, ?, ?)");
ps.setString(1, player);
ps.setString(2, condition);
ps.setString(3, value);
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 查询条件值
*/
public Object getCondition(
String player,
String condition) {
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value FROM "
+ "quest_conditions "
+ "WHERE player = ? "
+ "AND "
+ "condition_key = ?");
ps.setString(1, player);
ps.setString(2, condition);
ResultSet rs =
ps.executeQuery();
if (rs.next()) {
String val =
rs.getString("value");
rs.close();
ps.close();
// 尝试解析为数字
try {
if (val.contains(".")) {
return Double.parseDouble(
val);
}
return Long.parseLong(val);
} catch (NumberFormatException e) {
return val;
}
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public void close() {
try {
if (conn != null
&& !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,144 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player
.PlayerResourcePackStatusEvent;
import java.util.UUID;
import java.util.concurrent
.ConcurrentHashMap;
public class RadioDownloadListener
implements Listener {
private final Main plugin;
private final java.util.logging.Logger log;
private final ConcurrentHashMap<UUID, Integer>
failCount = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, Boolean>
blocked = new ConcurrentHashMap<>();
private static final int MAX_FAILED = 3;
public RadioDownloadListener(Main plugin) {
this.plugin = plugin;
this.log = plugin.getLogger();
}
public void onPlayerQuit(UUID uuid) {
failCount.remove(uuid);
blocked.remove(uuid);
}
@EventHandler
public void onStatus(
PlayerResourcePackStatusEvent e) {
Player p = e.getPlayer();
UUID uuid = p.getUniqueId();
String status =
e.getStatus().name();
log.info("[Radio] === 资源包状态 ===");
log.info("[Radio] 玩家: "
+ p.getName());
log.info("[Radio] UUID: " + uuid);
log.info("[Radio] 状态: "
+ status);
log.info("[Radio] 原始状态枚举: "
+ e.getStatus().getClass()
.getSimpleName()
+ "."
+ e.getStatus().name());
if (blocked.containsKey(uuid)) {
log.info("[Radio] 已熔断,跳过: "
+ p.getName());
return;
}
switch (status) {
case "SUCCESS":
log.info("[Radio] "
+ p.getName()
+ " ✓ 资源包加载成功");
failCount.remove(uuid);
blocked.remove(uuid);
break;
case "ACCEPTED":
log.info("[Radio] "
+ p.getName()
+ " 客户端已接受,"
+ "等待下载...");
break;
case "DECLINED":
log.warning("[Radio] "
+ p.getName()
+ " ✗ 拒绝了资源包");
log.warning("[Radio] "
+ "可能原因: "
+ "1.客户端设置禁用资源包"
+ " 2.已缓存拒绝记录"
+ " 3.SSL证书不匹配");
handleFailure(p, uuid);
break;
case "FAILED_DOWNLOAD":
log.warning("[Radio] "
+ p.getName()
+ " ✗ 下载失败");
log.warning("[Radio] "
+ "可能原因: "
+ "1.URL无法访问"
+ " 2.SSL证书错误"
+ " 3.网络超时"
+ " 4.文件不存在(404)");
handleFailure(p, uuid);
break;
default:
log.info("[Radio] "
+ p.getName()
+ " 未知状态: "
+ status);
break;
}
}
private void handleFailure(Player p, UUID uuid) {
int failNum = failCount.getOrDefault(uuid, 0) + 1;
failCount.put(uuid, failNum);
log.warning("[Radio] " + p.getName()
+ " 失败次数: " + failNum + "/" + MAX_FAILED);
if (failNum >= MAX_FAILED) {
blocked.put(uuid, true);
log.severe("[Radio] " + p.getName()
+ " 已达最大失败次数,停止重试,"
+ "玩家继续留在服务器");
// 不踢出,只记录熔断,玩家正常游玩
p.sendMessage("§e§l[Radio] 资源包加载失败"
+ ",已取消下载资源包,部分画面音频可能出现问题"
+ "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员");
return;
}
// 打了包没测试
p.sendMessage("§e§l[Radio] 下载失败,"
+ "重试... (" + failNum + "/" + MAX_FAILED + ")");
// 5秒后重试
Bukkit.getScheduler()
.runTaskLater(plugin, () -> {
if (p.isOnline() && plugin.radio != null) {
plugin.radio.sendResourcePack(p);
}
}, 100L);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,62 +0,0 @@
package Sdf1_login;
import org.bukkit.Material;
import java.util.UUID;
public class RefundRecord {
public final UUID playerUUID;
public final String playerName;
public final String itemId;
public final String itemName;
public final Material material;
public final Material shulkerMaterial;
public int amount;
public int totalPaid;
public final long timestamp;
// 普通购买
public RefundRecord(UUID uuid, String name,
String itemId, String itemName,
Material material, int amount,
int totalPaid, long timestamp) {
this.playerUUID = uuid;
this.playerName = name;
this.itemId = itemId;
this.itemName = itemName;
this.material = material;
this.shulkerMaterial = null;
this.amount = amount;
this.totalPaid = totalPaid;
this.timestamp = timestamp;
}
// 盒装购买
public RefundRecord(UUID uuid, String name,
String itemId, String itemName,
Material material,
Material shulkerMat,
int amount, int totalPaid,
long timestamp) {
this.playerUUID = uuid;
this.playerName = name;
this.itemId = itemId;
this.itemName = itemName;
this.material = material;
this.shulkerMaterial = shulkerMat;
this.amount = amount;
this.totalPaid = totalPaid;
this.timestamp = timestamp;
}
public boolean isExpired() {
return System.currentTimeMillis()
- timestamp > 300000L;
}
public int getSecondsLeft() {
long left = 300000L
- (System.currentTimeMillis()
- timestamp);
return (int) Math.max(0, left / 1000);
}
}
@@ -1,248 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
*/
public class RiskControlManager {
private final Main plugin;
// 每个玩家名 -> 连续失败次数
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
*/
public void resetFailAttempts(String playerName) {
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
}
/**
* 清理过期的冷却记录(可由定时任务调用)
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
}
/**
* 获取所有玩家的失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
}
}
@@ -1,170 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class SalesStatsManager {
private final Main plugin;
private final Map<String, Integer> sessionSales =
new ConcurrentHashMap<>();
private int sessionRevenue = 0;
private int sessionOrders = 0;
private int sessionItems = 0;
private int sessionRefunds = 0;
private int sessionRefundAmount = 0;
private long sessionStart = System.currentTimeMillis();
// 状态
private boolean waitingForEmpty = false;
private boolean hasReported = false;
private BukkitRunnable waitTask = null;
public SalesStatsManager(Main plugin) {
this.plugin = plugin;
startMonitor();
}
// ★ 每10秒检测一次
private void startMonitor() {
new BukkitRunnable() {
@Override
public void run() {
int online = Bukkit.getOnlinePlayers().size();
if (online > 0) {
// 有玩家在线 → 重置等待状态
if (waitingForEmpty) {
// 刚有人进来了,清除已汇报标记
// 下次没人时会重新汇报
}
waitingForEmpty = false;
hasReported = false;
cancelWaitTask();
} else if (!waitingForEmpty && !hasReported) {
// 没人了,且还没汇报过 → 开始1分钟倒计时
waitingForEmpty = true;
startWaitCountdown();
}
}
}.runTaskTimer(plugin, 200L, 200L);
}
// ★ 1分钟等待
private void startWaitCountdown() {
cancelWaitTask();
waitTask = new BukkitRunnable() {
@Override
public void run() {
int online = Bukkit.getOnlinePlayers().size();
if (online > 0) {
// 等待期间有人进来 → 取消,等下次没人
waitingForEmpty = false;
hasReported = false;
waitTask = null;
cancel();
return;
}
// 1分钟没人 → 汇报一次
if (!hasReported) {
hasReported = true;
report();
}
waitingForEmpty = false;
waitTask = null;
cancel();
}
};
waitTask.runTaskLater(plugin, 1200L); // 60秒
}
private void cancelWaitTask() {
if (waitTask != null) {
waitTask.cancel();
waitTask = null;
}
}
// ===== 记录 =====
public void recordSale(String itemName, int price) {
sessionSales.merge(itemName, 1, Integer::sum);
sessionRevenue += price;
sessionOrders++;
sessionItems++;
}
public void recordRefund(int amount) {
sessionRefunds++;
sessionRefundAmount += amount;
}
// ===== 汇报 =====
public void report() {
if (sessionItems == 0 && sessionRefunds == 0) {
// plugin.getLogger().info("[Sales] 无数据,跳过");
return;
}
long minutes =
(System.currentTimeMillis() - sessionStart)
/ 60000;
if (minutes < 1) minutes = 1;
StringBuilder sb = new StringBuilder();
sb.append("§6§l========== 销量汇总 ==========");
sb.append("\n§7时长: §f").append(minutes).append("分钟");
sb.append("\n§7订单: §f").append(sessionOrders).append("笔");
sb.append("\n§7商品: §f").append(sessionItems).append("件");
sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚");
sb.append("\n§7退款: §c").append(sessionRefunds)
.append("次(§c").append(sessionRefundAmount)
.append("枚§7)");
sb.append("\n§6§l------------------------------");
if (!sessionSales.isEmpty()) {
sb.append("\n§7销量排行:");
List<Map.Entry<String, Integer>> sorted =
new ArrayList<>(sessionSales.entrySet());
sorted.sort((a, b) ->
b.getValue() - a.getValue());
int rank = 1;
for (Map.Entry<String, Integer> e : sorted) {
if (rank > 10) break;
sb.append("\n§e").append(rank).append(". ")
.append(e.getKey())
.append(" §7x").append(e.getValue());
rank++;
}
}
sb.append("\n§6§l==============================");
String msg = sb.toString();
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.isOp()) {
for (String line : msg.split("\n"))
p.sendMessage(line);
}
}
plugin.getLogger().info("[Sales] 汇报完成");
resetSession();
}
public void resetSession() {
sessionSales.clear();
sessionRevenue = 0;
sessionOrders = 0;
sessionItems = 0;
sessionRefunds = 0;
sessionRefundAmount = 0;
sessionStart = System.currentTimeMillis();
}
}
@@ -1,32 +0,0 @@
package Sdf1_login;
import java.util.ArrayList;
import java.util.List;
public class ShopCategory {
private final String name;
private final String fileName;
private final List<ShopItem> items;
public ShopCategory(String name, String fileName) {
this.name = name;
this.fileName = fileName;
this.items = new ArrayList<>();
}
public String getName() { return name; }
public String getFileName() { return fileName; }
public List<ShopItem> getItems() { return items; }
public ShopItem getItem(String id) {
for (ShopItem item : items) {
if (item.getId().equalsIgnoreCase(id))
return item;
}
return null;
}
public void addItem(ShopItem item) {
items.add(item);
}
}
@@ -1,49 +0,0 @@
package Sdf1_login;
import org.bukkit.Material;
public class ShopItem {
private final String id;
private final String displayName;
private final Material material;
private final int buyPrice;
private final int sellPrice;
private int stock;
private int hourlySales;
private int totalSales;
public ShopItem(String id, String displayName,
Material material, int buyPrice,
int sellPrice, int stock,
int hourlySales, int totalSales) {
this.id = id;
this.displayName = displayName;
this.material = material;
this.buyPrice = buyPrice;
this.sellPrice = sellPrice;
this.stock = stock;
this.hourlySales = hourlySales;
this.totalSales = totalSales;
}
public int getEffectiveSellPrice() {
if (sellPrice <= 0) return 0;
return sellPrice;
}
public String getId() { return id; }
public String getDisplayName() { return displayName; }
public Material getMaterial() { return material; }
public int getBuyPrice() { return buyPrice; }
public int getSellPrice() { return sellPrice; }
public int getStock() { return stock; }
public int getHourlySales() { return hourlySales; }
public int getTotalSales() { return totalSales; }
public void setStock(int s) { this.stock = s; }
public void setHourlySales(int s) { this.hourlySales = s; }
public void setTotalSales(int s) { this.totalSales = s; }
public void addSales(int n) {
this.hourlySales += n;
this.totalSales += n;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,523 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private final Main plugin;
private final String currentVersion;
// ===== 仓库信息 =====
private static final String GITHUB_OWNER =
"ypsdf1";
private static final String GITHUB_REPO =
"sdf1_login";
private static final String GITEE_OWNER =
"nihaoshidifu";
private static final String GITEE_REPO =
"sdf1_login";
// API 地址
private static final String GITHUB_API =
"https://api.github.com/repos/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO
+ "/tags";
private static final String GITEE_API =
"https://gitee.com/api/v5/repos/"
+ GITEE_OWNER + "/"
+ GITEE_REPO
+ "/tags";
// 仓库页面链接(给用户看的)
private static final String GITHUB_LINK =
"https://github.com/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO + "/releases";
private static final String GITEE_LINK =
"https://gitee.com/"
+ GITEE_OWNER + "/"
+ GITEE_REPO + "/releases";
// 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 3000;
// 熔断阈值
private static final int TRIP_THRESHOLD = 5;
// ===== 熔断状态(内存态,重启自动清除) =====
private volatile boolean circuitTripped =
false;
private volatile int failCount = 0;
// ===== 自动检测禁用(持久化到文件) =====
private volatile boolean autoCheckDisabled =
false;
private final File disabledFile;
public UpdateChecker(Main plugin) {
this.plugin = plugin;
this.currentVersion =
plugin.getDescription()
.getVersion();
this.disabledFile = new File(
plugin.getDataFolder(),
"update_disabled.dat");
loadDisabledState();
}
// ========== 持久化禁用状态 ==========
private void loadDisabledState() {
if (disabledFile.exists()) {
try (BufferedReader br =
new BufferedReader(
new FileReader(
disabledFile))) {
String line = br.readLine();
if (line != null) {
autoCheckDisabled =
"true".equals(
line.trim());
}
} catch (Exception ignored) {
}
}
}
private void saveDisabledState() {
try (PrintWriter pw =
new PrintWriter(
new FileWriter(
disabledFile))) {
pw.println(autoCheckDisabled
? "true" : "false");
} catch (Exception ignored) {
}
}
// ========== 公开方法 ==========
/**
* onEnable 调用(异步静默)
* 熔断或禁用则直接跳过
*/
public void checkOnEnable() {
if (autoCheckDisabled) return;
if (circuitTripped) return;
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doCheck(null, true));
}
/**
* /sdf1_login update 调用(异步)
* 忽略熔断和禁用状态,强制检测
* 只有拿到版号才解除熔断
*/
public void checkUpdate(
CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() ->
doCheck(sender,
false));
}
// ========== 核心检测逻辑 ==========
private void doCheck(CommandSender sender,
boolean isAuto) {
if (isAuto) {
if (autoCheckDisabled
|| circuitTripped) {
return;
}
}
// 通道1:GitHub
CheckResult gh =
tryChannel(GITHUB_API,
"GitHub", GITHUB_LINK);
// 通道2:Gitee
CheckResult ge =
tryChannel(GITEE_API,
"Gitee", GITEE_LINK);
// 选择成功的结果
CheckResult chosen = null;
String failMsg = null;
if (gh.isSuccess()) {
chosen = gh;
} else if (ge.isSuccess()) {
chosen = ge;
} else {
// 双通道均失败
failCount++;
// 拼接两个通道的失败原因
StringBuilder sb =
new StringBuilder();
if (gh.error != null) {
sb.append("§7GitHub: §c")
.append(gh.error);
}
if (ge.error != null) {
if (sb.length() > 0)
sb.append("\n");
sb.append("§7Gitee: §c")
.append(ge.error);
}
if (failCount >= TRIP_THRESHOLD) {
circuitTripped = true;
plugin.getLogger().warning(
"[Sdf1_login] 更新检测"
+ "已熔断,连续失败"
+ failCount + "次");
}
if (sender != null) {
final String msg =
sb.toString();
final boolean tripped =
circuitTripped;
final int fc = failCount;
Bukkit.getScheduler()
.runTask(plugin, () -> {
sender.sendMessage(
"§c§l[更新] "
+ "检测失败");
sender.sendMessage(msg);
sender.sendMessage(
"§e失败次数: "
+ fc + "/"
+ TRIP_THRESHOLD);
if (tripped) {
sender.sendMessage(
"§c已熔断,"
+ "请重启服务器"
+ "重置");
}
});
}
return;
}
// 至少一个通道成功
failCount = 0;
// 检查禁用信号
if (isDisableSignal(chosen.version)) {
autoCheckDisabled = true;
saveDisabledState();
plugin.getLogger().info(
"[Sdf1_login] 远程版本信号"
+ " \"" + chosen.version
+ "\",自动更新检测已禁用");
if (sender != null) {
Bukkit.getScheduler()
.runTask(plugin, () ->
sender.sendMessage(
"§e[更新] 远程已"
+ "禁用自动"
+ "更新检测"));
}
return;
}
// 手动调用:成功才解除熔断和禁用
if (!isAuto) {
if (circuitTripped) {
circuitTripped = false;
}
if (autoCheckDisabled) {
autoCheckDisabled = false;
saveDisabledState();
}
}
final String src = chosen.source;
final String lnk = chosen.link;
final String rv = chosen.version;
Bukkit.getScheduler()
.runTask(plugin, () -> {
if (sender == null) return;
sender.sendMessage(
"§e[更新] 检测成功"
+ " via: §a" + src);
sender.sendMessage(
"§e[更新] 本地版本:"
+ " §f"
+ currentVersion);
sender.sendMessage(
"§e[更新] 远程版本:"
+ " §f" + rv);
if (isNewer(rv,
currentVersion)) {
sender.sendMessage(
"§c[更新] 发现新"
+ "版本: " + rv);
sender.sendMessage(
"§a请前往仓库下载"
+ "更新:");
sender.sendMessage(
"§a" + lnk);
} else {
sender.sendMessage(
"§a[更新] 已是"
+ "最新版本");
}
// 显示另一通道的失败原因
// (仅信息,不影响结果)
CheckResult other =
(src.equals("GitHub"))
? ge : gh;
if (!other.isSuccess()
&& other.error != null) {
sender.sendMessage(
"§7[更新] "
+ other.source
+ "不可用: "
+ other.error);
}
});
}
// ========== 禁用信号判断 ==========
private boolean isDisableSignal(
String version) {
if (version == null) return false;
String v = version.trim().toLowerCase();
return "999".equals(v)
|| "关闭".equals(v)
|| "false".equals(v)
|| "禁用".equals(v);
}
// ========== HTTP 请求(带超时) ==========
// ========== 检测结果 ==========
private static class CheckResult {
final String version;
final String source;
final String link;
final String error;
CheckResult(String version,
String source,
String link,
String error) {
this.version = version;
this.source = source;
this.link = link;
this.error = error;
}
boolean isSuccess() {
return version != null;
}
}
// ========== HTTP 请求 ==========
private CheckResult tryChannel(
String apiUrl,
String sourceName,
String link) {
HttpURLConnection conn = null;
try {
URL url = new URL(apiUrl);
conn = (HttpURLConnection)
url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; "
+ "Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) "
+ "Chrome/120.0.0.0 Safari/537.36");
conn.setRequestProperty(
"Accept",
"*/*");
conn.setRequestProperty(
"Accept-Language",
"zh-CN,zh;q=0.9");
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
conn.setInstanceFollowRedirects(true);
int code = conn.getResponseCode();
if (code == 404) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 未找到 (HTTP 404)");
}
if (code == 403) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 被限流 (HTTP 403)");
}
if (code != 200) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": HTTP " + code);
}
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn
.getInputStream()));
StringBuilder sb =
new StringBuilder();
String line;
while ((line = reader.readLine())
!= null) {
sb.append(line);
}
reader.close();
String tag =
parseTagName(sb.toString());
if (tag == null) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 无法解析版本号");
}
return new CheckResult(tag,
sourceName, link, null);
} catch (java.net
.SocketTimeoutException e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": 连接超时");
} catch (java.net.ConnectException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 连接被拒绝");
} catch (java.net
.UnknownHostException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": DNS解析失败");
} catch (Exception e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": "
+ e.getMessage());
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
/**
* 从 JSON 中提取 tag_name
*/
private String parseTagName(String json) {
// tags API 返回数组: [{"name":"v1.1",...}]
// releases API 返回对象: {"tag_name":"v1.1"}
// 尝试 tag_name(releases格式)
int idx =
json.indexOf("\"tag_name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
// 尝试 "name"(tags格式)
idx = json.indexOf("\"name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
return null;
}
private String cleanTag(String tag) {
if (tag.startsWith("v")
|| tag.startsWith("V")) {
tag = tag.substring(1);
}
return tag.isEmpty() ? null : tag;
}
// ========== 版本比较 ==========
private boolean isNewer(String remote,
String local) {
String[] r = remote.split("\\.");
String[] l = local.split("\\.");
int len = Math.max(r.length, l.length);
for (int i = 0; i < len; i++) {
int ri = i < r.length
? parseNum(r[i]) : 0;
int li = i < l.length
? parseNum(l[i]) : 0;
if (ri > li) return true;
if (ri < li) return false;
}
return false;
}
private int parseNum(String s) {
try {
return Integer.parseInt(
s.replaceAll("[^0-9]", ""));
} catch (Exception e) {
return 0;
}
}
}
@@ -1,370 +0,0 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.*;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
public class UserGroupManager {
private final Main plugin;
private Connection db;
private final Map<String, UserGroupConfig> groupConfigs = new ConcurrentHashMap<>();
public UserGroupManager(Main plugin) {
this.plugin = plugin;
initDB();
loadGroupConfigs();
startExpiryChecker();
}
// ===== DB =====
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(plugin.getDataFolder(), "usergroup.db");
db = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
st.execute("CREATE TABLE IF NOT EXISTS user_group_config ("
+ "group_name TEXT PRIMARY KEY,"
+ "display_name TEXT DEFAULT '',"
+ "display_color TEXT DEFAULT '§f',"
+ "display_emoji TEXT DEFAULT '',"
+ "priority INTEGER DEFAULT 0,"
+ "is_permanent INTEGER DEFAULT 1,"
+ "duration_minutes INTEGER DEFAULT 0,"
+ "pack_url TEXT DEFAULT '',"
+ "pack_hash TEXT DEFAULT '',"
+ "pack_prompt TEXT DEFAULT 'Custom Resource Pack',"
+ "pack_size INTEGER DEFAULT 0)");
st.execute("CREATE TABLE IF NOT EXISTS user_group_member ("
+ "player_name TEXT NOT NULL,"
+ "group_name TEXT NOT NULL,"
+ "added_by TEXT DEFAULT 'system',"
+ "added_time INTEGER DEFAULT 0,"
+ "expiry_time INTEGER DEFAULT 0,"
+ "PRIMARY KEY(player_name, group_name))");
st.close();
} catch (Exception e) {
throw new RuntimeException("[UserGroup] DB init failed: " + e.getMessage(), e);
}
}
// ===== 配置加载 =====
public void loadGroupConfigs() {
groupConfigs.clear();
File dir = new File(plugin.getDataFolder(), "用户组");
if (!dir.exists()) dir.mkdirs();
File[] files = dir.listFiles((d, n) -> n.endsWith(".txt"));
if (files == null) return;
for (File f : files) {
try {
List<String> lines = java.nio.file.Files.readAllLines(
f.toPath(), StandardCharsets.UTF_8);
UserGroupConfig cfg = parseGroupConfig(lines);
if (cfg != null) {
groupConfigs.put(cfg.name, cfg);
saveGroupConfigToDB(cfg);
}
} catch (Exception e) {
plugin.getLogger().warning("[UserGroup] Failed to load: " + f.getName());
}
}
}
private UserGroupConfig parseGroupConfig(List<String> lines) {
UserGroupConfig cfg = new UserGroupConfig();
boolean inBlock = false;
for (String raw : lines) {
String l = raw;
// 块注释
if (l.contains("<!--")) { inBlock = true; continue; }
if (l.contains("-->")) { inBlock = false; continue; }
if (l.contains("/*")) { inBlock = true; continue; }
if (l.contains("*/")) { inBlock = false; continue; }
if (inBlock) continue;
// 行注释
l = l.trim();
if (l.isEmpty()) continue;
if (l.startsWith("#") || l.startsWith("//")) continue;
// 行内注释
int h = l.indexOf('#');
int s = l.indexOf("//");
if (h >= 0) l = l.substring(0, h).trim();
if (s >= 0 && (h < 0 || s < h)) l = l.substring(0, s).trim();
if (l.isEmpty()) continue;
// 解析键值对
String key = l;
String val = "";
int idx = l.indexOf('\uff1a');
if (idx < 0) idx = l.indexOf(':');
if (idx >= 0) {
key = l.substring(0, idx).trim();
val = l.substring(idx + 1).trim();
if (val.startsWith("\"") && val.endsWith("\""))
val = val.substring(1, val.length() - 1);
}
switch (key) {
case "组名":
case "name": cfg.name = val; break;
case "显示名":
case "display": cfg.displayName = val; break;
case "颜色":
case "color": cfg.displayColor = val; break;
case "符号":
case "emoji": cfg.displayEmoji = val; break;
case "优先级":
case "priority":
try { cfg.priority = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "类型":
case "type":
cfg.permanent = val.equalsIgnoreCase("永久") || val.equalsIgnoreCase("permanent");
break;
case "时长":
case "duration":
try { cfg.durationMinutes = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "资源包URL":
case "pack_url": cfg.packUrl = val; break;
case "资源包Hash":
case "pack_hash": cfg.packHash = val; break;
case "资源包提示":
case "pack_prompt": cfg.packPrompt = val; break;
case "资源包大小":
case "pack_size":
try { cfg.packSize = Integer.parseInt(val); } catch (Exception ignored) {}
break;
case "本地文件":
case "local_file": cfg.localFile = val; break;
}
}
if (cfg.name == null || cfg.name.isEmpty()) return null;
return cfg;
}
private void saveGroupConfigToDB(UserGroupConfig cfg) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO user_group_config "
+ "(group_name, display_name, display_color, display_emoji,"
+ " priority, is_permanent, duration_minutes,"
+ " pack_url, pack_hash, pack_prompt, pack_size)"
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)");
ps.setString(1, cfg.name);
ps.setString(2, cfg.displayName);
ps.setString(3, cfg.displayColor);
ps.setString(4, cfg.displayEmoji);
ps.setInt(5, cfg.priority);
ps.setInt(6, cfg.permanent ? 1 : 0);
ps.setInt(7, cfg.durationMinutes);
ps.setString(8, cfg.packUrl);
ps.setString(9, cfg.packHash);
ps.setString(10, cfg.packPrompt);
ps.setInt(11, cfg.packSize);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ===== 玩家操作 =====
public boolean addPlayer(String player, String groupName, String addedBy, long durationMinutes) {
UserGroupConfig cfg = groupConfigs.get(groupName);
if (cfg == null) return false;
long now = System.currentTimeMillis();
long expiry = cfg.permanent ? 0 : now + durationMinutes * 60 * 1000;
if (!cfg.permanent && durationMinutes > 0) expiry = now + durationMinutes * 60 * 1000;
else if (!cfg.permanent) expiry = now + cfg.durationMinutes * 60 * 1000;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR REPLACE INTO user_group_member "
+ "(player_name, group_name, added_by, added_time, expiry_time)"
+ " VALUES (?,?,?,?,?)");
ps.setString(1, player);
ps.setString(2, groupName);
ps.setString(3, addedBy);
ps.setLong(4, now);
ps.setLong(5, expiry);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
Player p = plugin.getServer().getPlayerExact(player);
if (p != null) sendResourcePack(p);
return true;
}
public boolean removePlayer(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
int rows = ps.executeUpdate();
ps.close();
return rows > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public List<Map<String, Object>> getPlayerGroups(String player) {
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM user_group_member WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
long now = System.currentTimeMillis();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("group_name", rs.getString("group_name"));
row.put("added_by", rs.getString("added_by"));
long expiry = rs.getLong("expiry_time");
row.put("expiry_time", expiry);
row.put("expired", expiry > 0 && expiry <= now);
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public UserGroupConfig getHighestGroup(String player) {
List<Map<String, Object>> groups = getPlayerGroups(player);
long now = System.currentTimeMillis();
UserGroupConfig best = null;
for (Map<String, Object> g : groups) {
long expiry = (long) g.get("expiry_time");
if (expiry > 0 && expiry <= now) continue;
String name = (String) g.get("group_name");
UserGroupConfig cfg = groupConfigs.get(name);
if (cfg != null && (best == null || cfg.priority > best.priority)) {
best = cfg;
}
}
return best;
}
public boolean isPlayerInGroup(String player, String groupName) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM user_group_member WHERE player_name=? AND group_name=?");
ps.setString(1, player);
ps.setString(2, groupName);
boolean exists = ps.executeQuery().next();
ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
// ===== 过期自动移除 =====
private void startExpiryChecker() {
new BukkitRunnable() {
@Override
public void run() {
checkExpiry();
}
}.runTaskTimerAsynchronously(plugin, 1200L, 1200L); // 每分钟检查
}
private void checkExpiry() {
try {
long now = System.currentTimeMillis();
PreparedStatement ps = db.prepareStatement(
"SELECT player_name, group_name FROM user_group_member "
+ "WHERE expiry_time > 0 AND expiry_time <= ?");
ps.setLong(1, now);
ResultSet rs = ps.executeQuery();
List<String[]> expired = new ArrayList<>();
while (rs.next()) {
expired.add(new String[]{
rs.getString("player_name"),
rs.getString("group_name")});
}
rs.close(); ps.close();
for (String[] e : expired) {
PreparedStatement del = db.prepareStatement(
"DELETE FROM user_group_member WHERE player_name=? AND group_name=?");
del.setString(1, e[0]);
del.setString(2, e[1]);
del.executeUpdate();
del.close();
plugin.getLogger().info("[UserGroup] 过期移除: " + e[0] + " -> " + e[1]);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
// ===== 资源包分发 =====
public void sendResourcePack(Player p) {
UserGroupConfig cfg = getHighestGroup(p.getName());
if (cfg == null || cfg.packUrl == null || cfg.packUrl.isEmpty()) return;
String url = cfg.packUrl;
String prompt = cfg.packPrompt != null
? cfg.packPrompt : "Custom Resource Pack";
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
try {
p.setResourcePack(url, new byte[0], prompt, false);
plugin.getLogger().info("[UserGroup] 资源包已发送: "
+ p.getName() + " -> " + cfg.displayName);
} catch (Exception e) {
plugin.getLogger().warning(
"[UserGroup] 资源包发送失败: " + e.getMessage());
}
}, 40L);
}
public Map<String, UserGroupConfig> getGroupConfigs() {
return groupConfigs;
}
// ===== 配置类 =====
public static class UserGroupConfig {
public String name;
public String displayName = "";
public String displayColor = "§f";
public String displayEmoji = "";
public int priority = 0;
public boolean permanent = true;
public int durationMinutes = 0;
public String packUrl = "";
public String packHash = "";
public String packPrompt = "Custom Resource Pack";
public int packSize = 0;
public String localFile = "";
}
}
@@ -1,158 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
public class VerificationManager {
private final Main plugin;
public VerificationManager(Main plugin) {
this.plugin = plugin;
}
public boolean isBedrockPlayer(Player p) {
String uuid = p.getUniqueId().toString();
return uuid.contains("f851ba4")
|| uuid.startsWith("00000000-0000-0000");
}
public boolean isOnlineMode() {
return Bukkit.getServer().getOnlineMode();
}
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
"Sdf1_login-PremiumVerify");
t.setDaemon(true);
t.start();
}
private void doVerify(Player player,
Consumer<Boolean> callback) {
String name = player.getName();
String uuid = player.getUniqueId().toString();
plugin.getLogger().info(
"[Sdf1_login] ====== 验证开始 ======");
plugin.getLogger().info(
"[Sdf1_login] 玩家: " + name);
plugin.getLogger().info(
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 验证异常: "
+ e.getMessage());
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}
@@ -1,163 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class WelcomeManager {
private final Main plugin;
private final Random random = new Random();
private final List<String> registerMsgs =
new ArrayList<>();
private final List<String> loginMsgs =
new ArrayList<>();
public WelcomeManager(Main plugin) {
this.plugin = plugin;
loadMessages();
}
public void loadMessages() {
registerMsgs.clear();
loginMsgs.clear();
File f = new File(
plugin.getDataFolder(),
"欢迎仪式.txt");
if (!f.exists()) {
writeDefault(f);
}
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
String section = "";
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("\uFEFF"))
line = line.substring(1);
String t = line.trim();
if (t.isEmpty()
|| t.startsWith("#"))
continue;
if (t.equals("注册")
|| t.equals("注册:")) {
section = "register";
continue;
}
if (t.equals("登录")
|| t.equals("登录:")) {
section = "login";
continue;
}
// 支持 = 和 : 两种分隔
if (section.isEmpty()) {
if (t.startsWith("注册")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
registerMsgs.add(
colorize(v));
continue;
}
if (t.startsWith("登录")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
loginMsgs.add(
colorize(v));
continue;
}
continue;
}
if (!t.isEmpty()) {
if (section.equals("register"))
registerMsgs.add(
colorize(t));
else
loginMsgs.add(
colorize(t));
}
}
} catch (Exception e) {
plugin.getLogger().severe(
"[欢迎] 加载失败: "
+ e.getMessage());
}
plugin.getLogger().info(
"[欢迎] 加载完成: 注册="
+ registerMsgs.size()
+ " 登录=" + loginMsgs.size());
}
public void onRegister(Player p) {
sendRandom(p, registerMsgs, "注册");
}
public void onLogin(Player p) {
sendRandom(p, loginMsgs, "登录");
}
private void sendRandom(Player p,
List<String> msgs, String tag) {
if (msgs.isEmpty()) return;
String msg = msgs.get(
random.nextInt(msgs.size()));
msg = msg.replace("{player}",
p.getName());
final String finalMsg = msg;
Bukkit.getScheduler()
.runTaskLater(plugin,
() -> Bukkit
.broadcastMessage(
finalMsg), 10L);
}
private String colorize(String msg) {
return msg.replace("&", "\u00a7");
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets
.UTF_8))) {
pw.println("# 欢迎仪式配置");
pw.println("# &为颜色代码");
pw.println("# {player} = 玩家名");
pw.println();
pw.println("注册:");
pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]");
pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]");
pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]");
pw.println();
pw.println("登录:");
pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]");
pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]");
pw.println("&b&l{player} &b回来了!&7[登录]");
} catch (IOException ignored) {
}
}
}
@@ -1,104 +0,0 @@
name: Sdf1_login
version: 1.0
main: Sdf1_login.Main
api-version: '1.21'
commands:
sdf1_login:
description: Sdf1_login主命令
usage: /sdf1_login
reg:
description: 注册
usage: /reg <密码>
login:
description: 登录
usage: /login <密码>
l:
description: 登录快捷版
usage: /l <密码>
注册:
description: 注册(中文)
usage: /注册 <密码>
登录:
description: 登录(中文)
usage: /登录 <密码>
玩家信息:
description: 打开面板(中文)
usage: /玩家信息
改密码:
description: 修改密码(中文)
usage: /改密码 <旧密码> <新密码> <确认新密码>
设置邮箱:
description: 设置邮箱(中文)
usage: /设置邮箱 <邮箱地址>
撤销:
description: 撤销操作(中文)
usage: /撤销
删除:
description: 删除玩家(中文)
usage: /删除 <玩家> <密码>
找回密码:
description: 找回密码(中文)
usage: /找回密码
签到:
description: 每日签到(中文)
usage: /签到
#oa:
# description: 申请删除账号
# usage: /oa [玩家名]
删除账号:
description: 申请删除账号(中文)
usage: /删除账号 [玩家名]
通过:
description: 批准OA请求(中文)
usage: /通过 <编号>
拒绝:
description: 拒绝OA请求(中文)
usage: /拒绝 <编号> [理由]
accept:
description: 批准OA请求
usage: /accept <编号>
deny:
description: 拒绝OA请求
usage: /deny <编号> [理由]
email:
description: 绑定邮箱
usage: /email <邮箱>
绑定:
description: 绑定邮箱(中文)
usage: /绑定 <邮箱>
绑定邮箱:
description: 绑定邮箱
usage: /绑定邮箱
chat:
description: 聊天过滤管理
usage: /chat
recycle:
description: 打开垃圾回收站
usage: /recycle
垃圾清理:
description: 手动清理掉落物
usage: /垃圾清理
menu:
description: 雪球菜单开关控制
usage: /menu <开/关>
pvp :
description: PVP榜单
usage: /pvp
cypay :
description: 草原探险债券
usage: /cypay
printer :
description: 打印债券流水
usage: /printer
shop :
description: 游戏商人
usage: /shop
商店:
description: 打开商店
usage: /商店
#testorder:
# description: 测试订单中心
# usage: /testorder
protect:
description: 区域保护
usage: /protect
@@ -1,305 +0,0 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class CheckInManager {
private final Main plugin;
public CheckInManager(Main plugin) {
this.plugin = plugin;
}
// ===== 今日是否已签到 =====
public boolean isCheckedInToday(
String name) {
String last = (String) plugin.getDb()
.getField(name,
"last_checkin_date");
String today = new SimpleDateFormat(
"yyyy-MM-dd")
.format(new Date());
return today.equals(last);
}
// ===== 签到 =====
// ===== 签到 =====
public String checkIn(Player p) {
String name = p.getName();
if (isCheckedInToday(name)) {
return plugin.getConfig2()
.msg("checkin_already");
}
Object streakObj = plugin.getDb()
.getField(name, "checkin_streak");
int streak = streakObj != null
? ((Number) streakObj).intValue()
: 0;
String lastDate = (String) plugin.getDb()
.getField(name, "last_checkin_date");
String today = new SimpleDateFormat("yyyy-MM-dd")
.format(new Date());
if (lastDate != null && !lastDate.isEmpty()) {
try {
java.util.Date lastD =
new SimpleDateFormat("yyyy-MM-dd")
.parse(lastDate);
java.util.Date todayD =
new SimpleDateFormat("yyyy-MM-dd")
.parse(today);
long diff = todayD.getTime()
- lastD.getTime();
if (diff > 86400000L * 2) {
// 间隔超过2天,重置连续天数
streak = 1;
} else if (diff > 86400000L) {
// 间隔1-2天(连续签到),天数+1
streak++;
}
// 间隔<=1天(同一天),不改变连续天数
} catch (Exception ignored) {
}
} else {
// 首次签到
streak = 1;
}
double multi = 1.0;
if (streak >= 21) multi = 1.5;
else if (streak >= 14) multi = 1.3;
else if (streak >= 7) multi = 1.1;
int basePoints =
plugin.getConfig2().checkinPoints;
int points = (int) (basePoints * multi);
plugin.getDb().addPoints(name, points);
plugin.getDb().setField(name,
"checkin_streak", streak);
plugin.getDb().setField(name,
"last_checkin_date", today);
Object totalObj = plugin.getDb()
.getField(name, "total_checkin_days");
int total = totalObj != null
? ((Number) totalObj).intValue()
: 0;
plugin.getDb().setField(name,
"total_checkin_days", total + 1);
// ★ 债券盲盒奖励(6参数版,写入流水)★
int bondMin =
plugin.getConfig2().checkinBondMin;
int bondMax =
plugin.getConfig2().checkinBondMax;
if (bondMin <= 0) bondMin = 1;
if (bondMax < bondMin) bondMax = bondMin;
int bondReward = bondMin
+ new java.util.Random()
.nextInt(bondMax - bondMin + 1);
plugin.getBonds().addBonds(name, bondReward,
"daily_sign", "", "签到系统",
"每日签到连续" + streak + "天");
return plugin.getConfig2()
.msg("checkin_success")
.replace("{points}",
String.valueOf(points))
.replace("{streak}",
String.valueOf(streak))
.replace("{multi}",
String.valueOf(multi))
+ " §e+" + bondReward + "债券";
}
// ===== 获取连续天数 =====
public int getStreak(String name) {
Object val = plugin.getDb()
.getField(name,
"checkin_streak");
if (val instanceof Number) {
return ((Number) val)
.intValue();
}
return 0;
}
// 标准化日期
private String normalizeDate(String input) {
String s = input.trim().replace("/", "-");
if (s.matches("\\d{8}")) {
return s.substring(0, 4) + "-"
+ s.substring(4, 6) + "-"
+ s.substring(6, 8);
}
return s;
}
// ===== 补签入口 =====
public String backCheckIn(
Player p, String dateStr) {
String name = p.getName();
String normalized =
normalizeDate(dateStr);
// 严格日期验证
java.text.SimpleDateFormat sdf =
new java.text.SimpleDateFormat(
"yyyy-MM-dd");
sdf.setLenient(false);
java.util.Date target;
try {
target = sdf.parse(normalized);
} catch (Exception ex) {
return "§c无效日期: " + dateStr;
}
String reformat = sdf.format(target);
if (!reformat.equals(normalized)) {
return "§c无效日期: " + dateStr;
}
String today = sdf.format(new Date());
if (normalized.compareTo(today) >= 0) {
return "§c今天不需要补签";
}
String threeDaysAgo = getPastDate(3);
if (normalized.compareTo(
threeDaysAgo) < 0) {
return "§c只能补签3天内"
+ "(最多到 " + threeDaysAgo + ")";
}
Object val = plugin.getDb()
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
boolean alreadyBacked = false;
if (!existing.isEmpty()) {
for (String d : existing.split(",")) {
if (normalizeDate(d.trim())
.equals(normalized)) {
alreadyBacked = true;
break;
}
}
}
if (alreadyBacked) {
return "§c该日期已补签: " + normalized;
}
Object normalCheck =
plugin.getDb().getField(
name, "last_checkin_date");
String lastCheckin = normalCheck != null
? normalCheck.toString() : "";
if (lastCheckin.equals(normalized)) {
return "§c该日期已签到: " + normalized;
}
// ★ 补签消耗:3债券 + 10积分
// 积分不够时:用 9债券(3倍)代替 ★
int bondCost = 3;
int pointCost = 10;
int bondAltCost = 9; // 3倍债券替代
int myBonds = plugin.getBonds()
.getBonds(name);
Object ptsObj = plugin.getDb()
.getField(name, "points");
int myPoints = ptsObj != null
? ((Number) ptsObj).intValue()
: 0;
// 方案A:3债券 + 10积分
boolean canA = myBonds >= bondCost
&& myPoints >= pointCost;
// 方案B:9债券(积分不够时)
boolean canB = myBonds >= bondAltCost
&& myPoints < pointCost;
if (!canA && !canB) {
if (myPoints < pointCost
&& myBonds < bondAltCost) {
return "§c补签费用不足"
+ "(需3债券+10积分,"
+ "或9债券代替)"
+ " 当前: "
+ myBonds + "债券 "
+ myPoints + "积分";
}
if (myBonds < bondCost) {
return "§c债券不足(需"
+ bondCost + "枚)";
}
return "§c积分不足(需"
+ pointCost + "分)";
}
// 执行扣款
if (canA) {
// 方案A:3债券 + 10积分
plugin.getBonds().deductBonds(
name, bondCost,
"admin_deduct", "",
"补签系统", "补签扣款");
plugin.getDb().deductPoints(
name, pointCost);
} else {
// 方案B:9债券代替
plugin.getBonds().deductBonds(
name, bondAltCost,
"admin_deduct", "",
"补签系统", "补签扣款(债券替代)");
}
doBackCheckIn(p, normalized);
String costMsg = canA
? bondCost + "债券+" + pointCost + "积分"
: bondAltCost + "债券(替代)";
return "§a补签成功!日期: "
+ normalized
+ " §7消耗: " + costMsg;
}
// ===== 执行补签 =====
// ===== 执行补签 =====
public void doBackCheckIn(
Player p, String dateStr) {
String name = p.getName();
Object val = plugin.getDb()
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
String updated = existing.isEmpty()
? dateStr
: existing + "," + dateStr;
plugin.getDb().setField(name,
"backcheck_dates", updated);
int streak = getStreak(name);
plugin.getDb().setField(name,
"checkin_streak", streak + 1);
// ★ 补签不发放奖励,只记录
}
// ===== 获取N天前日期 =====
private String getPastDate(
int daysAgo) {
Calendar cal =
Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH,
-daysAgo);
return new SimpleDateFormat(
"yyyy-MM-dd")
.format(cal.getTime());
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,138 +0,0 @@
package Sdf1_login;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class QuestStorage {
private Connection conn;
public QuestStorage(File dataFolder) {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
dataFolder,
"quest_data.db");
conn = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getPath());
initTable();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initTable()
throws SQLException {
Statement stmt =
conn.createStatement();
stmt.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_conditions ("
+ "player TEXT NOT NULL, "
+ "condition_key TEXT NOT NULL, "
+ "value TEXT DEFAULT '1', "
+ "timestamp INTEGER, "
+ "PRIMARY KEY "
+ "(player, condition_key))");
stmt.close();
}
/**
* 标记条件已完成(value = "1")
*/
public void setCondition(
String player,
String condition) {
setConditionRaw(
player, condition, "1");
}
/**
* 写入任意值(计数器等)
*/
public void setConditionRaw(
String player,
String condition,
String value) {
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO "
+ "quest_conditions "
+ "(player, "
+ "condition_key, "
+ "value, timestamp) "
+ "VALUES "
+ "(?, ?, ?, ?)");
ps.setString(1, player);
ps.setString(2, condition);
ps.setString(3, value);
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 查询条件值
*/
public Object getCondition(
String player,
String condition) {
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value FROM "
+ "quest_conditions "
+ "WHERE player = ? "
+ "AND "
+ "condition_key = ?");
ps.setString(1, player);
ps.setString(2, condition);
ResultSet rs =
ps.executeQuery();
if (rs.next()) {
String val =
rs.getString("value");
rs.close();
ps.close();
// 尝试解析为数字
try {
if (val.contains(".")) {
return Double.parseDouble(
val);
}
return Long.parseLong(val);
} catch (NumberFormatException e) {
return val;
}
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public void close() {
try {
if (conn != null
&& !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
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
-569
View File
@@ -1,569 +0,0 @@
# Sdf1系列登录插件使用说明
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
## 1. 功能介绍
| 名字 | 描述/作用 | 施工状态 |
| :--- | :--- | :--- |
| 登录 | 用户登录/注册 | 已完成 |
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
| 工单系统 | 用户提交工单 | 已完成 |
| 签到系统 | 签到积分换奖励 | 已完成 |
| 新人礼包 | 新玩家7日礼包 | 已完成 |
| 挂机检测 | 挂机检测踢出 | 已完成 |
| 自定义菜单 | 可以自己diy指令 | 已完成 |
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
## 2.安装说明:
| 前置插件 | 选装类型 | 功能作用 |
| :--- | :--- | :--- |
| [CY_beibao](https://gitee.com/nihaoshidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
| EssentialsX | 可选 | 主流经济系统
| [sdf1](https://gitee.com/nihaoshidifu/sdf1_plugn) | 可选,推荐 | CDK兑换系统
### 2.1 下载
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
### 2.2. 配置文件
#### 2.2.1 消息.txt
此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)<br>
同时兼容`<b> </b>`标签,对段落加粗,`\n 和 <br>`进行段落换行
```txt
# ==========================================
# Sdf1_login 消息配置文件
# ==========================================
# 颜色代码使用 § 符号
# §0黑色 §1深蓝 §2深绿 §3深青
# §4深红 §5深紫 §6金色 §7灰色
# §8深灰 §9蓝色 §a绿色 §b浅蓝
# §c红色 §d粉色 §e黄色 §f白色
# §l粗体 §n下划线 §o斜体 §r重置
#
# 占位符用 {xxx} 表示,运行时自动替换
# 支持 <b>粗体</b> 标签
# 支持 \n 或 <br> 换行
# 修改后执行 /sdf1_login reload 重载
# ==========================================
# ---- 登录注册相关 ----
# login_timeout: 登录超时被踢出时的提示
login_timeout=§c登录超时,请重新登录
# not_registered: 未注册玩家收到的提示
not_registered=§e您尚未注册,请使用 /reg <密码>
# not_logged_in: 已注册但未登录的提示
not_logged_in=§e您尚未登录,请使用 /login <密码>
# already_registered: 已注册玩家再次注册时的提示
already_registered=§c您已注册过账号
# already_logged_in: 已登录玩家再次登录时的提示
already_logged_in=§c您已登录
# reg_success: 注册成功提示
reg_success=§a注册成功!欢迎来到服务器
# login_success: 登录成功提示
login_success=§a登录成功!
# login_failed: 密码错误提示
login_failed=§c密码错误,请重试
# ---- 密码相关 ----
# password_wrong: 旧密码验证失败
password_wrong=§c密码错误
# password_changed: 密码修改成功
password_changed=§a密码修改成功!
# password_format_error: 密码格式不符合要求
password_format_error=§c密码需6位以上,含大小写字母和数字
# password_same: 新密码与旧密码相同
password_same=§c新密码不能与旧密码相同
# reset_no_email: 找回密码时未绑定邮箱
reset_no_email=§c您未绑定邮箱,无法找回密码
# reset_sent: 临时密码已发送到邮箱
reset_sent=§a临时密码已发送到您的邮箱,请查收
# ---- 签到积分相关 ----
# checkin_already: 今日已签到
checkin_already=§c您今日已签到
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
# points_insufficient: 积分不足
points_insufficient=§c积分不足,当前: {points}
# points_purchase_success: 积分购买成功
points_purchase_success=§a购买成功!消耗 {count} 积分
# ---- 挂机检测相关 ----
# afk_set_enabled: 挂机检测开启
afk_set_enabled=§a挂机检测已开启
# afk_set_disabled: 挂机检测关闭
afk_set_disabled=§c挂机检测已关闭
# afk_kick: 因挂机被踢出
afk_kick=§c因挂机过久被踢出
# ---- 邮箱相关 ----
# email_set: 邮箱设置成功
email_set=§a邮箱设置成功:{email}
# email_format_error: 邮箱格式错误
email_format_error=§c邮箱格式不正确
# ---- 邀请相关 ----
# invite_code_generated: 邀请码生成
invite_code_generated=§a邀请码:{code}
# invite_used: 使用邀请码成功
invite_used=§a邀请成功,邀请人:{player}
# invite_self: 使用自己的邀请码
invite_self=§c不能使用自己的邀请码
# invite_not_found: 邀请码不存在
invite_not_found=§c邀请码不存在
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
# ---- 新人礼包相关 ----
# gift_not_ready: 礼包条件未满足
gift_not_ready=§c礼包条件未满足
# gift_claimed: 礼包领取成功
gift_claimed=§a礼包领取成功!
# ---- 审批与IP限制 ----
# need_approval: 注册需审批
need_approval=§e注册需审批 (工单#{id})
# ip_max_accounts: IP注册已达上限
ip_max_accounts=§cIP注册已达上限
# ---- 管理员操作 ----
# admin_delete_confirm: 管理员删除账号确认提示
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
# ---- 聊天过滤 ----
# chat_muted: 禁言玩家尝试聊天
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
# chat_url_blocked: 发送违规链接被拦截
# {url}=检测到的链接地址
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
# chat_url_violation: 当前违规次数
# {count}=当前违规次数
chat_url_violation=§7当前违规次数: §e{count}
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
# chat_url_broadcast: 全服通报,{player}=玩家名
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
# ---- 通用 ----
# 未知参数: 未知命令或权限不足时的提示
未知参数=§c未知参数或权限不足
# only_player: 仅玩家可用的命令
# only_player=§c此命令仅玩家可用
```
### 2.2.2 挂机白名单.txt
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
### 2.2.3 SMTP设置.txt
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
```txt
# ===== SMTP 邮件服务器配置 =====
smtp地址=smtp.example.com
smtp端口=465
smtp账号=mail@example.com
smtp密码=example.com
发件人名称=Sdf1_login
smtp加密=true
验证码接收邮箱=example@example.com
```
### 2.2.4 插件设置.txt
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
```txt
# ===== Sdf1_login 插件设置 =====
管理标签=admin
管理密码=qweasd
每IP最大账号数=3
审批模式=manual
自动审批延迟分钟=30
登录超时秒数=60
挂机踢出=false
挂机超时秒数=300
服务商积分倍率=2.0
基础经济奖励=100
经济奖励每评分=50.0
报单人积分奖励=10
# ---- 签到盲盒奖励 ----
签到固定金额=0
签到最小金额=10
签到最大金额=100
签到给积分=true
签到积分数量=10
```
### 2.2.5 chat.txt
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
可用的处罚规则:
- warn: 警告
- mute: 禁言
- kick: 踢出
- ban: 封禁
- banip:封禁IP
```txt
# Sdf1_chat 聊天过滤配置
通知管理员: true
全服通报: true
禁言时长: 300
白名单:
*.minecraft.net
baidu.com
github.com
处罚规则:
1:warn:0
2:warn:0
3:mute:300
5:mute:600
8:kick:0
10:ban:0
白名单玩家:
```
### 2.2.6 radio_config.txt
```txt
# ============ Sdf1 Radio Config ============
#
# resource-pack-url
# 填外部地址(HTTP或HTTPS均可)
# 例: https://example.com/radiopack.zip
# 留空 = 使用内置HTTP服务
#
# http-port
# 内置HTTP端口(仅resource-pack-url为空时生效)
#
# ============ Settings ===============
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
http-port=443
```
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
### 2.2.7 菜单.txt
- 自定义菜单配置文件
```json
# 服务器菜单
# 使用类json格式
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
{
标题:主城传送
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
权限类型:玩家
图标:COMPASS
}
{
标题:随机传送
指令:/rtp
权限类型:玩家
图标:ENDER_PEARL
}
{
标题:擂台比武
指令:/dom tp PVP
权限类型:玩家
图标:netherite_spear
}
{
标题:领地操作
指令:/dom
权限类型:玩家
图标:paper
}
{
标题:逛商店
指令:/dom tp shop
权限类型:玩家
图标:honeycomb
}
{
标题:传送开关
指令:/tptoggle
权限类型:玩家
图标:redstone_torch
}
{
标题:返回上一个位置
指令:/back
权限类型:玩家
图标:red_bed
}
{
标题:前往小黑塔
指令:/dom tp 小黑塔
权限类型:玩家
图标:dragon_head
}
{
标题:云背包
指令:/cy
权限类型:玩家
图标:potato
}
{
标题:自杀
指令:/suicide
权限类型:玩家
图标:wooden_axe
}
{
标题:游戏商人
指令:/shop
权限类型:玩家
图标:shield
格子:28
}
```
### 2.2.8 商品配置
- 配置此处的文件,负责展示游戏内所销售的商品。<br>
- 一个文件名,对应一个商品分类,使用md文件格式
```
# 建筑材料
| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 |
| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 |
| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 |
| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 |
| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 |
| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 |
| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 |
| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 |
```
## 2.3 商城部分介绍
1. 自助退款,买多买错,不用慌,一件退单很安心<br>
2. 购物车,买的多怕买错,先加购物车,后续统一结算<br>
3. 害怕订单找不到?一键查看历史订单<br>
![](img/shop4.png)
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
![](img/shop7.png)<br>
![](img/shop6.png)
5. 想查看玩家消费记录?小手一点,明明白白<br>
![](img/shop5.png)
## 2.4 打印流水部分介绍
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
## 3. 指令参数及介绍
- Sdf1_login 主指令<br>
- Sdf1_login reload 重载配置文件<br>
- reg 注册账号<br>
- login 登录账号<br>
- l 登录账号<br>
- sdf1_login del 删除玩家账号<br>
- sdf1_login radio 停止播放广播<br>
- sdf1_login radio reload 重载广播并重新打包资源包<br>
- menu 开/关 是否开启菜单<br>
- printer 打印流水数据,支持不带参数&带玩家名为参数<br>
- sdf1_login printer 作用同printer<br>
- sdf1_login shop 商城
- protect 区域防护
## 4. 自定义菜单物品
- 打开主界面,选择"我的"进入<br>
![](img/menu1.png)
- 将你的自定义物品与雪球交换<br>
![](img/menu2.png)
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
## 5. pvp战神榜
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
![](img/pvp1.png)
- 并且有着网游同款的击杀通报<br>
![](img/pvp3.png)
- 播报逻辑是:<br>
- 3杀<br>
- 5杀<br>
- 5杀以上每杀一次都是一条播报<br>
- 终结者通报,触发连杀的玩家被终结也会有全服通报
## 6. 区域防护
- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。<br>
- 还能够保护指定区域的玩家不受任何伤害。<br>
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
| :---: | :---: | :---: | :---: | :---: |
| protect | 无 | 无 | 无 | 帮助信息 |
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
示范:
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<br>
- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写
| 参数 | 效果 |
| :---: | :---: |
| 和平模式 | 所有敌对生物全部进虚空 |
| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 |
| 清除XX效果 | 立即清除玩家身上的效果
| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果
## 生物属性对照表
| 中文名称 | 类型 | 附加属性 |
| :---: | :----: | :----: |
| 溺尸 | 近战/远程 | 亡灵、水生 |
| 骷髅 | 远程 | 亡灵 |
| 流浪者 | 远程 | 亡灵、寒冷群系变种 |
| 沼骸 | 远程 | 亡灵、沼泽变种 |
| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 |
| 掠夺者 | 远程 | 灾厄村民、袭击成员 |
| 卫道士 | 近战 | 灾厄村民、袭击成员 |
| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 |
| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 |
| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 |
| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 |
| 守卫者 | 近战/激光远程 | 海底神殿、水下 |
配置文件解析:<br>
- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的<!--- --->注释`和`Java/* */块注释`
```txt
# 区域: 主城区
起点: world,-130,-713
终点: world,-427,-560
高度范围: 0-255
没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个)
// 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块
// 禁止破坏方块 # 同上
禁止PVP # 玩家之间不能PVP
禁止摔伤 # 摔落无法造成任何伤害
禁止饥饿 # 不会消耗饱食度
禁止一切伤害 # 任何怪物都无法造成伤害
# 禁止丢弃物品
# 禁止末影珍珠
# 禁止使用弓箭
# 禁止骑乘
// 和平模式 // 所有的敌对生物全部传送进虚空
禁止袭击
禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外
// 清除所有效果 # 清理所有负面效果
效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间
效果 生命提升 255 999999
效果 抗性提升 255 999999
效果 抗火 255 999999
# 禁止药水
禁止爆炸
禁止使用物品: TNT
# 效果: 夜视 1 999
进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示
离开提示: 已离开保护区
没收提示: 你的违禁品已被没收
通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域
# 惩罚命令: ban {player} 60s 违规
```
## 运行截图
主界面
![](img/login.png)
邀请数据
![](img/login2.png)
积分商店
![](img/login3.png)
任务中心
![](img/login4.png)
垃圾箱
![](img/login5.png)
管理员面板
![](img/login6.png)
管理面板-用户管理
![](img/login7.png)
工单中心
![](img/login8.png)
工单管理
![](img/login9.png)
我的工单
![](img/login10.png)
提交工单
![](img/login11.png)
服务商列表
![](img/login12.png)
服务商抢单大厅
![](img/login13.png)
服务商-接单界面
![](img/login14.png)
管理员-工单详情页面
![](img/login15.png)
商城主界面
![](img/shop1.png)
商城购物车
![](img/cart1.png)
商城退单
![](img/shop2.png)
## 4. 下载地址
- 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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sdf1</groupId>
<artifactId>Sdf1_login</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.4-R0.1-SNAPSHOT</version>
<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>
<version>3.46.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>Sdf1_login</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,197 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
public class AFKManager {
// 玩家最后活动时间(空闲才开始计时)
private final Map<UUID, Long> lastAction =
new HashMap<>();
// 每个玩家当前的警告阶段
private final Map<UUID, Integer> lastStage =
new HashMap<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
new HashMap<>();
private final ConfigManager config;
private final JavaPlugin plugin;
public AFKManager(ConfigManager config,
JavaPlugin plugin) {
this.config = config;
this.plugin = plugin;
}
public void startCheck() {
new BukkitRunnable() {
public void run() {
tick();
}
}.runTaskTimer(plugin, 20L, 20L);
}
private void tick() {
if (!config.afkEnabled) return;
long now = System.currentTimeMillis();
for (Player p : Bukkit.getOnlinePlayers()) {
String name = p.getName();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
continue;
}
int stage = lastStage.getOrDefault(uuid, 0);
// 阶段3:最后10秒
if (remain <= 10) {
if (stage < 3) {
lastStage.put(uuid, 3);
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + remain + " 秒踢出",
0, 22, 3);
playSound(p);
continue;
}
// 阶段2:1分钟~10秒
if (remain <= 60) {
if (stage < 2) {
lastStage.put(uuid, 2);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + fmtSec(remain) + " 踢出",
5, 25, 5);
playSound(p);
continue;
}
// 阶段1:3分钟提醒
if (remain <= 180 && stage < 1) {
lastStage.put(uuid, 1);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§f还剩 " + fmtSec(remain) + " 踢出",
10, 60, 20);
for (int i = 0; i < 3; i++) {
final int delay = i * 10;
Bukkit.getScheduler().runTaskLater(
plugin, () -> {
if (p.isOnline()) playSound(p);
}, delay);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
private void playSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
private String fmtSec(long sec) {
if (sec < 60) return sec + "秒";
long m = sec / 60;
long s = sec % 60;
return m + "分" + (s > 0 ? s + "秒" : "");
}
}
@@ -1,146 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AccountRequestManager {
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
public static class Request {
public final int id;
public final String requester;
public final String target;
public final String ip;
public final long time;
public String status;
public String denyReason;
public Request(int id, String requester, String target, String ip) {
this.id = id;
this.requester = requester;
this.target = target;
this.ip = ip;
this.time = System.currentTimeMillis();
this.status = "PENDING";
this.denyReason = "";
}
}
private final Main plugin;
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
private int nextId = 1;
private ApprovalMode mode = ApprovalMode.MANUAL;
private int autoDelayMinutes = 60;
public AccountRequestManager(Main plugin) {
this.plugin = plugin;
}
public ApprovalMode getMode() { return mode; }
public void setMode(String m) {
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
}
public void setAutoDelayMinutes(int min) {
this.autoDelayMinutes = min;
}
public Request createRequest(String requester, String target, String ip) {
int id = nextId++;
Request req = new Request(id, requester, target, ip);
requests.put(id, req);
pendingByPlayer.put(requester, id);
if (mode == ApprovalMode.AUTO) {
scheduleAutoApprove(id);
} else if (mode == ApprovalMode.HYBRID) {
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
if (ip != null && ip.equals(targetIp)) {
scheduleAutoApprove(id);
}
}
return req;
}
private void scheduleAutoApprove(int id) {
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
Request req = requests.get(id);
if (req == null) return;
String requesterName = req.requester;
String targetName = req.target;
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Request r = requests.get(id);
if (r != null && "PENDING".equals(r.status)) {
approveRequest(id);
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
Player requester = Bukkit.getPlayer(requesterName);
if (requester != null && requester.isOnline()) {
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
}
}
}, delayTicks);
}
public boolean approveRequest(int id) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "APPROVED";
plugin.getDb().deleteUser(req.target);
pendingByPlayer.remove(req.requester);
// 清背包+传送出生点+踢出
Player target = Bukkit.getPlayer(req.target);
if (target != null && target.isOnline()) {
target.getInventory().clear();
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
target.teleport(target.getWorld().getSpawnLocation());
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
}
return true;
}
public boolean denyRequest(int id, String reason) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "DENIED";
req.denyReason = reason;
pendingByPlayer.remove(req.requester);
return true;
}
public List<Request> getPendingRequests() {
List<Request> list = new ArrayList<>();
for (Request r : requests.values()) {
if ("PENDING".equals(r.status)) list.add(r);
}
return list;
}
public Request getRequest(int id) {
return requests.get(id);
}
public boolean hasPending(String player) {
return pendingByPlayer.containsKey(player);
}
public void cancelPending(String player) {
Integer id = pendingByPlayer.remove(player);
if (id != null) {
Request r = requests.get(id);
if (r != null) r.status = "CANCELLED";
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,697 +0,0 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.File;
import java.sql.*;
import java.util.*;
public class BondManager {
private final Main plugin;
private Connection db;
// ===== 交易类型常量 =====
public static final String TX_ADMIN_GIVE = "admin_give";
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
public static final String TX_TRANSFER_OUT = "transfer_out";
public static final String TX_TRANSFER_IN = "transfer_in";
public static final String TX_REDEEM = "redeem";
public static final String TX_SHOP_BUY = "shop_buy";
public static final String TX_DAILY_SIGN = "daily_sign";
public static final String TX_FREEZE = "freeze";
public static final String TX_UNFREEZE = "unfreeze";
public static final int MAX_WATER_DAYS = 14;
public static final int ADMIN_MAX_WATER_DAYS = 30;
public BondManager(Main plugin) {
this.plugin = plugin;
initDB();
}
// ======================== 数据库初始化 ========================
private void initDB() {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
plugin.getDataFolder(), "bond.db");
db = DriverManager.getConnection(
"jdbc:sqlite:" + dbFile.getAbsolutePath());
Statement st = db.createStatement();
st.execute("PRAGMA journal_mode=WAL");
// 债券余额(含冻结状态)
st.execute("CREATE TABLE IF NOT EXISTS bonds ("
+ "player_name TEXT PRIMARY KEY,"
+ "amount INTEGER DEFAULT 0,"
+ "status TEXT DEFAULT 'normal')");
// 兼容旧表
try {
st.execute("ALTER TABLE bonds "
+ "ADD COLUMN status "
+ "TEXT DEFAULT 'normal'");
} catch (SQLException ignored) {}
// 旧流水(保留兼容)
st.execute("CREATE TABLE IF NOT EXISTS bond_log ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "reason TEXT DEFAULT '',"
+ "time INTEGER DEFAULT 0)");
// 新交易流水表
st.execute("CREATE TABLE IF NOT EXISTS "
+ "bond_transaction ("
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "player_name TEXT NOT NULL,"
+ "type TEXT NOT NULL,"
+ "amount INTEGER NOT NULL,"
+ "target_player TEXT DEFAULT '',"
+ "operator TEXT DEFAULT '',"
+ "reason TEXT DEFAULT '',"
+ "balance_after INTEGER DEFAULT 0,"
+ "time INTEGER DEFAULT 0)");
try {
st.execute("CREATE INDEX "
+ "IF NOT EXISTS idx_tx_pt "
+ "ON bond_transaction("
+ "player_name, time)");
} catch (SQLException ignored) {}
try {
st.execute("ALTER TABLE bond_transaction "
+ "ADD COLUMN balance_before "
+ "INTEGER DEFAULT 0");
} catch (SQLException ignored) {}
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[Bond] DB init failed: "
+ e.getMessage(), e);
}
}
// ======================== 账户状态 ========================
public boolean isFrozen(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return "frozen".equals(s);
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return false;
}
public String getAccountStatus(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT status FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String s = rs.getString("status");
rs.close(); ps.close();
return s != null ? s : "normal";
}
rs.close(); ps.close();
} catch (SQLException ignored) {}
return "normal";
}
public void freezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='frozen' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
public void unfreezeAccount(String player) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET status='normal' "
+ "WHERE player_name=?");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
}
private void ensureAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT OR IGNORE INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,0,'normal')");
ps.setString(1, player);
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
// ======================== 余额查询 ========================
public int getBonds(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT amount FROM bonds "
+ "WHERE player_name=?");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
int v = 0;
if (rs.next()) v = rs.getInt("amount");
rs.close(); ps.close();
return v;
} catch (SQLException e) { return 0; }
}
/** 该玩家是否在 bond 表中有真实记录 */
public boolean hasBondAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM bonds "
+ "WHERE player_name=? "
+ "AND amount > 0");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next();
rs.close(); ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
// ======================== 增加债券 ========================
public int addBonds(String player, int amount) {
return addBondsInternal(player, amount,
TX_ADMIN_GIVE, "", "",
"管理员发放");
}
public int addBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return -1;
return addBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
private int addBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
int after = before + amount;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status)"
+ " VALUES (?,?, 'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, after);
ps.setInt(3, after);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, amount);
return after;
}
// ======================== 扣除债券 ========================
private boolean deductBondsInternal(String player,
int amount,
String type,
String targetPlayer,
String operator,
String reason) {
int before = getBonds(player);
if (before < amount) return false;
int after = before - amount;
try {
PreparedStatement ps = db.prepareStatement(
"UPDATE bonds SET amount=? "
+ "WHERE player_name=?");
ps.setInt(1, after);
ps.setString(2, player);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
logTransaction(player, type, amount,
targetPlayer, operator, reason,
before, after);
logLegacy(player, -amount);
return true;
}
// ======================== 流水写入 ========================
private void logTransaction(String player,
String type,
int amount,
String targetPlayer,
String operator,
String reason,
int balanceBefore,
int balanceAfter) {
try {
// 合并模式:shop_buy 30秒内同商品合并
if ("shop_buy".equals(type)
&& reason != null
&& !reason.isEmpty()) {
long mergeWindow =
System.currentTimeMillis() - 30000L;
PreparedStatement chk =
db.prepareStatement(
"SELECT id, amount "
+ "FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND type=? "
+ "AND reason=? "
+ "AND time > ? "
+ "ORDER BY time DESC "
+ "LIMIT 1");
chk.setString(1, player);
chk.setString(2, type);
chk.setString(3, reason);
chk.setLong(4, mergeWindow);
ResultSet rs = chk.executeQuery();
if (rs.next()) {
int existId = rs.getInt("id");
int existAmt = rs.getInt("amount");
rs.close();
chk.close();
PreparedStatement upd =
db.prepareStatement(
"UPDATE bond_transaction "
+ "SET amount=?, "
+ "balance_after=?, "
+ "time=? "
+ "WHERE id=?");
upd.setInt(1, existAmt + amount);
upd.setInt(2, balanceAfter);
upd.setLong(3,
System.currentTimeMillis());
upd.setInt(4, existId);
upd.executeUpdate();
upd.close();
return;
}
rs.close();
chk.close();
}
// 原始插入
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_transaction "
+ "(player_name, type, amount,"
+ " target_player, operator,"
+ " reason, balance_before,"
+ " balance_after, time)"
+ " VALUES (?,?,?,?,?,?,?,?,?)");
ps.setString(1, player);
ps.setString(2, type);
ps.setInt(3, amount);
ps.setString(4,
targetPlayer != null ? targetPlayer : "");
ps.setString(5,
operator != null ? operator : "");
ps.setString(6,
reason != null ? reason : "");
ps.setInt(7, balanceBefore);
ps.setInt(8, balanceAfter);
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// ======================== 扣除债券 ========================
/** 完整版:含冻结检查 + 流水记录 */
public boolean deductBonds(String player, int amount,
String type,
String targetPlayer,
String operator,
String reason) {
if (isFrozen(player)) return false;
return deductBondsInternal(player, amount,
type, targetPlayer, operator, reason);
}
public boolean checkoutCart(String player,
String[] itemNames,
int[] subtotals) {
int total = 0;
for (int s : subtotals) total += s;
if (isFrozen(player)) return false;
int before = getBonds(player);
if (before < total) return false;
int after = before - total;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,?,'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, after);
ps.setInt(3, after);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
int running = before;
for (int i = 0; i < itemNames.length; i++) {
int next = running - subtotals[i];
logTransaction(player, "shop_buy", subtotals[i],
"", "商店系统",
"购物车购买-" + itemNames[i],
running, next);
running = next;
}
logLegacy(player, -total);
return true;
}
public boolean setBonds(String player, int amount) {
ensureAccount(player);
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,?, 'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, amount);
ps.setInt(3, amount);
ps.executeUpdate(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return true;
}
// ======================== 转账 ========================
public TransferResult transfer(String from,
String to,
int amount) {
if (from.equalsIgnoreCase(to))
return new TransferResult(false,
"不能给自己转账");
if (isFrozen(from))
return new TransferResult(false,
"你的账户已被冻结");
if (isFrozen(to))
return new TransferResult(false,
to + " 的账户已被冻结");
if (isSameIP(from, to))
return new TransferResult(false,
"不能与同IP玩家转账");
if (!deductBondsInternal(from, amount,
TX_TRANSFER_OUT, to, from,
"转账给" + to))
return new TransferResult(false, "债券不足");
addBondsInternal(to, amount,
TX_TRANSFER_IN, from, from,
"收到来自" + from + "的转账");
return new TransferResult(true, "转账成功");
}
// ======================== 同IP检测 ========================
public boolean isSameIP(String p1, String p2) {
String ip1 = getPlayerIPAny(p1);
String ip2 = getPlayerIPAny(p2);
if (ip1 == null || ip2 == null) return false;
return ip1.equals(ip2);
}
/** 获取同IP下的所有其他在线玩家 */
public List<String> getSameIPPlayers(String player) {
String ip = getPlayerIPAny(player);
if (ip == null) return Collections.emptyList();
List<String> list = new ArrayList<>();
for (Player p :
plugin.getServer().getOnlinePlayers()) {
String pIp = plugin.getPlayerIP(p);
if (pIp != null && pIp.equals(ip)
&& !p.getName()
.equalsIgnoreCase(player)) {
list.add(p.getName());
}
}
return list;
}
private String getPlayerIPAny(String player) {
// 在线:直接取
Player p = plugin.getServer()
.getPlayerExact(player);
if (p != null) {
String ip = plugin.getPlayerIP(p);
if (ip != null) return ip;
}
// 离线:查主库
try {
Object v = plugin.getDb()
.getField(player, "last_login_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
try {
Object v = plugin.getDb()
.getField(player, "register_ip");
if (v != null) return v.toString();
} catch (Exception ignored) {}
return null;
}
// ======================== 流水查询 ========================
public List<Map<String, Object>> getTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
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_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
// 放在 getTransactions 方法后面:
/**
* 获取该玩家全部流水(不限时间,供打印使用)
*/
public List<Map<String, Object>>
getAllTransactions(String player) {
List<Map<String, Object>> list =
new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "ORDER BY time ASC");
ps.setString(1, player);
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_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
// 放在 getAllTransactions 方法后面:
/**
* 获取所有有流水记录的玩家名(去重)
*/
public List<String> getAllPlayerNames() {
List<String> list = new ArrayList<>();
try {
Statement st = db.createStatement();
ResultSet rs = st.executeQuery(
"SELECT DISTINCT player_name "
+ "FROM bond_transaction "
+ "ORDER BY player_name ASC");
while (rs.next()) {
list.add(rs.getString("player_name"));
}
rs.close(); st.close();
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
public List<Map<String, Object>>
getTransferTransactions(
String player, int days) {
days = Math.min(days, MAX_WATER_DAYS);
long cutoff = System.currentTimeMillis()
- (long) days * 24 * 60 * 60 * 1000;
List<Map<String, Object>> list = new ArrayList<>();
try {
PreparedStatement ps = db.prepareStatement(
"SELECT * FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND time>=? "
+ "AND (type=? OR type=?) "
+ "ORDER BY time DESC");
ps.setString(1, player);
ps.setLong(2, cutoff);
ps.setString(3, TX_TRANSFER_OUT);
ps.setString(4, TX_TRANSFER_IN);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("id", rs.getInt("id"));
row.put("type", rs.getString("type"));
row.put("amount",
rs.getInt("amount"));
row.put("target_player",
rs.getString("target_player"));
row.put("reason",
rs.getString("reason"));
row.put("balance_after",
rs.getInt("balance_after"));
row.put("balance_before", rs.getInt("balance_before"));
row.put("time", rs.getLong("time"));
list.add(row);
}
rs.close(); ps.close();
} catch (SQLException e) { e.printStackTrace(); }
return list;
}
private void logLegacy(String player, int amount) {
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_log "
+ "(player_name,amount,time) "
+ "VALUES (?,?,?)");
ps.setString(1, player);
ps.setInt(2, amount);
ps.setLong(3,
System.currentTimeMillis());
ps.executeUpdate(); ps.close();
} catch (SQLException ignored) {}
}
public Map<String, Object> getLog(
String player, int limit) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("balance", getBonds(player));
return info;
}
// ======================== 转账结果 ========================
public static class TransferResult {
public final boolean success;
public final String message;
public TransferResult(boolean success,
String message) {
this.success = success;
this.message = message;
}
}
}

Some files were not shown because too many files have changed in this diff Show More