Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebcb11338c | ||
|
|
8d4731a873 | ||
|
|
203c9d423a | ||
|
|
2c7f765fb0 | ||
|
|
8072855d94 | ||
|
|
873aca91bf |
+26
-1
@@ -42,5 +42,30 @@ build/
|
|||||||
txt/
|
txt/
|
||||||
*.xlsx
|
*.xlsx
|
||||||
*.patch
|
*.patch
|
||||||
|
backup/
|
||||||
新建*.txt
|
新建*.txt
|
||||||
群戏归类.txt
|
群戏归类.txt
|
||||||
|
|
||||||
|
# Windows 保留设备名(无法用普通 rm 删除)
|
||||||
|
nul
|
||||||
|
con
|
||||||
|
prn
|
||||||
|
aux
|
||||||
|
com1
|
||||||
|
com2
|
||||||
|
com3
|
||||||
|
com4
|
||||||
|
com5
|
||||||
|
com6
|
||||||
|
com7
|
||||||
|
com8
|
||||||
|
com9
|
||||||
|
lpt1
|
||||||
|
lpt2
|
||||||
|
lpt3
|
||||||
|
lpt4
|
||||||
|
lpt5
|
||||||
|
lpt6
|
||||||
|
lpt7
|
||||||
|
lpt8
|
||||||
|
lpt9
|
||||||
Generated
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[01:19] Web端交易三个核心问题修复
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. 商品ID无效(ACACIA_BOAT)- 已修复
|
||||||
|
- PHP shop.php 传 item_id="ACACIA_BOAT"(Material名称)
|
||||||
|
- Java dispatchItemByMaterialOrId() 已支持Material匹配+ID fallback
|
||||||
|
- displayName获取已修复:优先从ItemMeta.getDisplayName()读取,而不是直接用itemId
|
||||||
|
|
||||||
|
2. 扣款变给钱(bond_log符号反了)- 已修复
|
||||||
|
- BondManager.deductBondsInternal(): logLegacy(player, -amount) → logLegacy(player, amount)
|
||||||
|
- bond_log表的amount字段现在记录正数,UI根据type判断加减
|
||||||
|
|
||||||
|
3. 交易轮询慢(30-90秒)- 已修复
|
||||||
|
- PHP shop.php 和 cdk.php 在购买/兑换成功后立即POST到Java notify_sync回调
|
||||||
|
- Java收到notify_sync后调用requestImmediateTransactionPull()
|
||||||
|
- 最小间隔5秒,避免过度频繁
|
||||||
|
|
||||||
|
4. 回调服务器并发问题 - 已修复
|
||||||
|
- handleCallback()分离notify_sync和register_callback两种请求
|
||||||
|
- notify_sync不阻塞注册回调处理
|
||||||
|
|
||||||
|
5. detail JSON解析 - 已修复
|
||||||
|
- 支持转义引号\"的嵌套JSON解析
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
下一步:
|
||||||
|
1. 用IDEA重新编译打包(Maven → Lifecycle → package)
|
||||||
|
2. 将Sdf1_login.jar复制到MC服务器plugins目录覆盖
|
||||||
|
3. 重启服务器测试
|
||||||
|
4. 验证:Web购物→立即到账、CDK兑换→余额同步、扣款方向正确
|
||||||
@@ -565,5 +565,7 @@ http-port=443
|
|||||||
|
|
||||||
|
|
||||||
## 4. 下载地址
|
## 4. 下载地址
|
||||||
|
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
|
||||||
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
- 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)
|
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||||
|
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||||
|
|||||||
+571
@@ -0,0 +1,571 @@
|
|||||||
|
# Sdf1系列登录插件使用说明
|
||||||
|
|
||||||
|
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
|
||||||
|
|
||||||
|
## 1. 功能介绍
|
||||||
|
|
||||||
|
| 名字 | 描述/作用 | 施工状态 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| 登录 | 用户登录/注册 | 已完成 |
|
||||||
|
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||||
|
| 工单系统 | 用户提交工单 | 已完成 |
|
||||||
|
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||||
|
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||||
|
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||||
|
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||||
|
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||||
|
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||||
|
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||||
|
|
||||||
|
|
||||||
|
## 2.安装说明:
|
||||||
|
|
||||||
|
| 前置插件 | 选装类型 | 功能作用 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||||
|
| EssentialsX | 可选 | 主流经济系统
|
||||||
|
| [sdf1](https://github.com/ypsdf1/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>
|
||||||
|

|
||||||
|
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||||
|
<br>
|
||||||
|

|
||||||
|
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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>
|
||||||
|

|
||||||
|
- 将你的自定义物品与雪球交换<br>
|
||||||
|

|
||||||
|
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
|
||||||
|
|
||||||
|
## 5. pvp战神榜
|
||||||
|
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
|
||||||
|

|
||||||
|
- 并且有着网游同款的击杀通报<br>
|
||||||
|

|
||||||
|
- 播报逻辑是:<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 违规
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行截图
|
||||||
|
|
||||||
|
主界面
|
||||||
|

|
||||||
|
|
||||||
|
邀请数据
|
||||||
|

|
||||||
|
|
||||||
|
积分商店
|
||||||
|

|
||||||
|
|
||||||
|
任务中心
|
||||||
|

|
||||||
|
|
||||||
|
垃圾箱
|
||||||
|

|
||||||
|
|
||||||
|
管理员面板
|
||||||
|

|
||||||
|
|
||||||
|
管理面板-用户管理
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
工单中心
|
||||||
|

|
||||||
|
|
||||||
|
工单管理
|
||||||
|

|
||||||
|
|
||||||
|
我的工单
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
提交工单
|
||||||
|

|
||||||
|
|
||||||
|
服务商列表
|
||||||
|

|
||||||
|
|
||||||
|
服务商抢单大厅
|
||||||
|

|
||||||
|
|
||||||
|
服务商-接单界面
|
||||||
|

|
||||||
|
|
||||||
|
管理员-工单详情页面
|
||||||
|

|
||||||
|
|
||||||
|
商城主界面
|
||||||
|

|
||||||
|
|
||||||
|
商城购物车
|
||||||
|

|
||||||
|
|
||||||
|
商城退单
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 4. 下载地址
|
||||||
|
- 官方仓库:[https://git.ypshidifu.cn/youpaishidifu/Sdf1_login](https://git.ypshidifu.cn/youpaishidifu/Sdf1_login) (推荐)
|
||||||
|
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
||||||
|
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||||
|
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>25</java.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -55,10 +55,11 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.14.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>21</source>
|
<source>25</source>
|
||||||
<target>21</target>
|
<target>25</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
@@ -1494,7 +1494,12 @@ public class AreaProtection implements Listener {
|
|||||||
// 没名字或不在白名单,传送虚空
|
// 没名字或不在白名单,传送虚空
|
||||||
Location voidLoc = ent.getLocation().clone();
|
Location voidLoc = ent.getLocation().clone();
|
||||||
voidLoc.setY(ent.getWorld().getMinHeight() - 50);
|
voidLoc.setY(ent.getWorld().getMinHeight() - 50);
|
||||||
ent.teleport(voidLoc);
|
// 末影人受伤害会自动传送回来,所以直接移除实体
|
||||||
|
if ("ENDERMAN".equals(ent.getType().name())) {
|
||||||
|
ent.remove();
|
||||||
|
} else {
|
||||||
|
ent.teleport(voidLoc);
|
||||||
|
}
|
||||||
banished++;
|
banished++;
|
||||||
/* plugin.getLogger().info("[和平] "
|
/* plugin.getLogger().info("[和平] "
|
||||||
+ ent.getType().name()
|
+ ent.getType().name()
|
||||||
@@ -1518,7 +1523,12 @@ public class AreaProtection implements Listener {
|
|||||||
// 没有保护期且不在白名单,传送虚空
|
// 没有保护期且不在白名单,传送虚空
|
||||||
Location voidLoc = ent.getLocation().clone();
|
Location voidLoc = ent.getLocation().clone();
|
||||||
voidLoc.setY(ent.getWorld().getMinHeight() - 50);
|
voidLoc.setY(ent.getWorld().getMinHeight() - 50);
|
||||||
ent.teleport(voidLoc);
|
// 末影人受伤害会自动传送回来,直接移除
|
||||||
|
if ("ENDERMAN".equals(ent.getType().name())) {
|
||||||
|
ent.remove();
|
||||||
|
} else {
|
||||||
|
ent.teleport(voidLoc);
|
||||||
|
}
|
||||||
banished++;
|
banished++;
|
||||||
/* plugin.getLogger().info("[和平] "
|
/* plugin.getLogger().info("[和平] "
|
||||||
+ ent.getType().name()
|
+ ent.getType().name()
|
||||||
@@ -1544,7 +1554,8 @@ public class AreaProtection implements Listener {
|
|||||||
"ILLUSIONER", "VEX",
|
"ILLUSIONER", "VEX",
|
||||||
"PHANTOM", "SHULKER", "WARDEN", "BREEZE",
|
"PHANTOM", "SHULKER", "WARDEN", "BREEZE",
|
||||||
"PIGLIN", "PIGLIN_BRUTE", "ZOGLIN",
|
"PIGLIN", "PIGLIN_BRUTE", "ZOGLIN",
|
||||||
"ZOMBIFIED_PIGLIN", "ENDER_DRAGON"
|
"ZOMBIFIED_PIGLIN", "ENDER_DRAGON",
|
||||||
|
"ENDERMAN", "WITHER"
|
||||||
));
|
));
|
||||||
|
|
||||||
private boolean isHostile(String typeName) {
|
private boolean isHostile(String typeName) {
|
||||||
@@ -1628,15 +1639,42 @@ public class AreaProtection implements Listener {
|
|||||||
&& z >= minZ && z <= maxZ;
|
&& z >= minZ && z <= maxZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 白名单检查 ====================
|
// ===== 统一白名单检查(供所有事件处理器使用)=====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否在任意白名单中(全局白名单 + 区域白名单)
|
||||||
|
* 这是所有白名单判断的唯一入口
|
||||||
|
*/
|
||||||
private boolean isPlayerWhitelisted(String player,
|
private boolean isPlayerWhitelisted(String player,
|
||||||
AreaConfig ac) {
|
AreaConfig ac) {
|
||||||
if (globalPlayerWhitelist.contains(
|
String lowerName = player.toLowerCase();
|
||||||
player.toLowerCase()))
|
// 1. 全局白名单
|
||||||
|
if (globalPlayerWhitelist.contains(lowerName))
|
||||||
return true;
|
return true;
|
||||||
|
// 2. 区域白名单
|
||||||
|
if (ac != null) {
|
||||||
|
Set<String> areaList =
|
||||||
|
areaPlayerWhitelist.get(ac.name);
|
||||||
|
if (areaList != null
|
||||||
|
&& areaList.contains(lowerName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速检查:仅全局白名单(不带 AreaConfig)
|
||||||
|
*/
|
||||||
|
private boolean isGlobalWhite(String player) {
|
||||||
|
return globalPlayerWhitelist.contains(player.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速检查:仅区域白名单
|
||||||
|
*/
|
||||||
|
private boolean isAreaWhite(String player, String areaName) {
|
||||||
Set<String> areaList =
|
Set<String> areaList =
|
||||||
areaPlayerWhitelist.get(ac.name);
|
areaPlayerWhitelist.get(areaName);
|
||||||
return areaList != null
|
return areaList != null
|
||||||
&& areaList.contains(player.toLowerCase());
|
&& areaList.contains(player.toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -2403,14 +2441,15 @@ public class AreaProtection implements Listener {
|
|||||||
private boolean isPlayerExemptFromModeChange(
|
private boolean isPlayerExemptFromModeChange(
|
||||||
Player p, AreaConfig ac) {
|
Player p, AreaConfig ac) {
|
||||||
// if (p.isOp()) return true;
|
// if (p.isOp()) return true;
|
||||||
Set<String> global = globalWhitelist.get("global");
|
String lower = p.getName().toLowerCase();
|
||||||
if (global != null
|
// 1. 全局白名单
|
||||||
&& global.contains(p.getName().toLowerCase())) {
|
if (globalPlayerWhitelist.contains(lower)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Set<String> areaWl = areaWhitelist.get(ac.name);
|
// 2. 区域白名单
|
||||||
|
Set<String> areaWl = areaPlayerWhitelist.get(ac.name);
|
||||||
if (areaWl != null
|
if (areaWl != null
|
||||||
&& areaWl.contains(p.getName().toLowerCase())) {
|
&& areaWl.contains(lower)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ac.modeExempt.contains(p.getName())) {
|
if (ac.modeExempt.contains(p.getName())) {
|
||||||
@@ -4438,20 +4477,11 @@ public class AreaProtection implements Listener {
|
|||||||
if (!(e.getDamager() instanceof Player)) return;
|
if (!(e.getDamager() instanceof Player)) return;
|
||||||
Player p = (Player) e.getDamager();
|
Player p = (Player) e.getDamager();
|
||||||
|
|
||||||
Set<String> global = globalWhitelist.get("global");
|
// ★ 统一白名单检查(globalPlayerWhitelist + areaPlayerWhitelist)
|
||||||
if (global != null
|
|
||||||
&& global.contains(p.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaConfig ac = findFrameArea(entity);
|
AreaConfig ac = findFrameArea(entity);
|
||||||
if (ac == null || !ac.denyItemFrame) return;
|
if (ac == null || !ac.denyItemFrame) return;
|
||||||
|
|
||||||
Set<String> areaWl = areaWhitelist.get(ac.name);
|
if (isPlayerWhitelisted(p.getName(), ac)) return;
|
||||||
if (areaWl != null
|
|
||||||
&& areaWl.contains(p.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setCancelled(true);
|
e.setCancelled(true);
|
||||||
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
||||||
@@ -4487,22 +4517,12 @@ public class AreaProtection implements Listener {
|
|||||||
|
|
||||||
Player p = e.getPlayer();
|
Player p = e.getPlayer();
|
||||||
|
|
||||||
// 全局白放行
|
// ★ 使用统一的白名单检查方法(globalPlayerWhitelist + areaPlayerWhitelist)
|
||||||
Set<String> global = globalWhitelist.get("global");
|
|
||||||
if (global != null
|
|
||||||
&& global.contains(p.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaConfig ac = findFrameArea(clicked);
|
AreaConfig ac = findFrameArea(clicked);
|
||||||
if (ac == null || !ac.denyItemFrame) return;
|
if (ac == null || !ac.denyItemFrame) return;
|
||||||
|
|
||||||
// 区域白放行
|
// 统一白名单检查
|
||||||
Set<String> areaWl = areaWhitelist.get(ac.name);
|
if (isPlayerWhitelisted(p.getName(), ac)) return;
|
||||||
if (areaWl != null
|
|
||||||
&& areaWl.contains(p.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非白名单 → 拦截
|
// 非白名单 → 拦截
|
||||||
// 记录当前旋转角度
|
// 记录当前旋转角度
|
||||||
@@ -4553,22 +4573,13 @@ public class AreaProtection implements Listener {
|
|||||||
if (!(e.getDamager() instanceof Player)) return;
|
if (!(e.getDamager() instanceof Player)) return;
|
||||||
Player p = (Player) e.getDamager();
|
Player p = (Player) e.getDamager();
|
||||||
|
|
||||||
// 全局白放行
|
|
||||||
Set<String> gwl = globalWhitelist.get("global");
|
|
||||||
if (gwl != null
|
|
||||||
&& gwl.contains(p.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaConfig ac = findFrameArea(entity);
|
AreaConfig ac = findFrameArea(entity);
|
||||||
if (ac == null || !ac.denyItemFrame) return;
|
if (ac == null) return;
|
||||||
|
|
||||||
// 区域白放行
|
// ★ 使用统一的白名单检查方法
|
||||||
Set<String> awl = areaWhitelist.get(ac.name);
|
if (isPlayerWhitelisted(p.getName(), ac)) return;
|
||||||
if (awl != null
|
|
||||||
&& awl.contains(p.getName().toLowerCase())) {
|
if (!ac.denyItemFrame) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setCancelled(true);
|
e.setCancelled(true);
|
||||||
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
p.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
||||||
@@ -4627,22 +4638,11 @@ public class AreaProtection implements Listener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局白放行
|
// ★ 统一白名单检查(globalPlayerWhitelist + areaPlayerWhitelist)
|
||||||
Set<String> gwl = globalWhitelist.get("global");
|
|
||||||
if (gwl != null
|
|
||||||
&& gwl.contains(shooter.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaConfig ac = findFrameArea(hit);
|
AreaConfig ac = findFrameArea(hit);
|
||||||
if (ac == null || !ac.denyItemFrame) return;
|
if (ac == null || !ac.denyItemFrame) return;
|
||||||
|
|
||||||
// 区域白放行
|
if (isPlayerWhitelisted(shooter.getName(), ac)) return;
|
||||||
Set<String> awl = areaWhitelist.get(ac.name);
|
|
||||||
if (awl != null
|
|
||||||
&& awl.contains(shooter.getName().toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setCancelled(true);
|
e.setCancelled(true);
|
||||||
shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
shooter.sendMessage("§c§l[区域防护] §f禁止破坏展示框");
|
||||||
|
|||||||
@@ -5,12 +5,33 @@ import org.bukkit.entity.Player;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
public class BondManager {
|
public class BondManager {
|
||||||
|
|
||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
private Connection db;
|
private Connection db;
|
||||||
|
|
||||||
|
// ===== 交易监听器 =====
|
||||||
|
public interface TransactionListener {
|
||||||
|
void onTransactionLogged(String playerName, String type, int amount);
|
||||||
|
}
|
||||||
|
private final List<TransactionListener> transactionListeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public void addTransactionListener(TransactionListener listener) {
|
||||||
|
transactionListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyTransactionListeners(String playerName, String type, int amount) {
|
||||||
|
for (TransactionListener listener : transactionListeners) {
|
||||||
|
try {
|
||||||
|
listener.onTransactionLogged(playerName, type, amount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[BondManager] 交易监听器异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 交易类型常量 =====
|
// ===== 交易类型常量 =====
|
||||||
public static final String TX_ADMIN_GIVE = "admin_give";
|
public static final String TX_ADMIN_GIVE = "admin_give";
|
||||||
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
|
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
|
||||||
@@ -277,7 +298,8 @@ public class BondManager {
|
|||||||
logTransaction(player, type, amount,
|
logTransaction(player, type, amount,
|
||||||
targetPlayer, operator, reason,
|
targetPlayer, operator, reason,
|
||||||
before, after);
|
before, after);
|
||||||
logLegacy(player, -amount);
|
// 记录扣除流水,use positive amount, UI uses type to determine sign
|
||||||
|
logLegacy(player, amount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +385,8 @@ public class BondManager {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
// ★ 通知监听器:交易已记录
|
||||||
|
notifyTransactionListeners(player, type, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -696,4 +720,41 @@ public class BondManager {
|
|||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== 同步用:获取指定时间之后的交易 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定时间之后的所有交易记录(用于推送到Web端)
|
||||||
|
* 使用时间而非ID追踪,确保shop_buy合并后的UPDATE记录也能被同步
|
||||||
|
* @param afterTime 上次同步的最晚时间(毫秒),0表示从头开始
|
||||||
|
* @return 交易记录列表
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT * FROM bond_transaction "
|
||||||
|
+ "WHERE time > ? "
|
||||||
|
+ "ORDER BY time ASC "
|
||||||
|
+ "LIMIT 200");
|
||||||
|
ps.setLong(1, afterTime);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("id", rs.getInt("id"));
|
||||||
|
row.put("player_name", rs.getString("player_name"));
|
||||||
|
row.put("type", rs.getString("type"));
|
||||||
|
row.put("amount", rs.getInt("amount"));
|
||||||
|
row.put("target_player", rs.getString("target_player"));
|
||||||
|
row.put("operator", rs.getString("operator"));
|
||||||
|
row.put("reason", rs.getString("reason"));
|
||||||
|
row.put("balance_before", rs.getInt("balance_before"));
|
||||||
|
row.put("balance_after", rs.getInt("balance_after"));
|
||||||
|
row.put("time", rs.getLong("time"));
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
rs.close(); ps.close();
|
||||||
|
} catch (SQLException e) { e.printStackTrace(); }
|
||||||
|
return list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,6 +356,9 @@ public class ChatInputManager {
|
|||||||
state.targetPlayer)) {
|
state.targetPlayer)) {
|
||||||
db.deleteUser(
|
db.deleteUser(
|
||||||
state.targetPlayer);
|
state.targetPlayer);
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
plugin.webManager.deleteWebUser(state.targetPlayer);
|
||||||
|
}
|
||||||
p.sendMessage("§a已删除 "
|
p.sendMessage("§a已删除 "
|
||||||
+ state.targetPlayer);
|
+ state.targetPlayer);
|
||||||
Player tgt =
|
Player tgt =
|
||||||
|
|||||||
@@ -390,6 +390,23 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新玩家密码凭证(salt + hash),用于Web重新注册时同步
|
||||||
|
*/
|
||||||
|
public void updatePassword(String name, String hash, String salt) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"UPDATE users SET password_hash = ?, password_salt = ? WHERE player_name = ?");
|
||||||
|
ps.setString(1, hash);
|
||||||
|
ps.setString(2, salt);
|
||||||
|
ps.setString(3, name);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 临时密码管理 ==========
|
// ========== 临时密码管理 ==========
|
||||||
|
|
||||||
public void clearTempPassword(String name) {
|
public void clearTempPassword(String name) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.bukkit.util.io.BukkitObjectInputStream;
|
|||||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +50,11 @@ public class GUIManager implements Listener {
|
|||||||
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
||||||
private static final int UMGMT_PAGE_SIZE = 45;
|
private static final int UMGMT_PAGE_SIZE = 45;
|
||||||
|
|
||||||
|
// ★ 宝箱物品拦截计数(事不过三)
|
||||||
|
private final ConcurrentHashMap<String, Integer>
|
||||||
|
treasureMenuBlockCount =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public GUIManager(Main plugin) {
|
public GUIManager(Main plugin) {
|
||||||
@@ -393,6 +399,8 @@ public class GUIManager implements Listener {
|
|||||||
|
|
||||||
public void openSubMenu(
|
public void openSubMenu(
|
||||||
Player p, String fileName) {
|
Player p, String fileName) {
|
||||||
|
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
|
||||||
|
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
|
||||||
String menuTitle =
|
String menuTitle =
|
||||||
"§6§l" + fileName
|
"§6§l" + fileName
|
||||||
.replace(".txt", "");
|
.replace(".txt", "");
|
||||||
@@ -400,21 +408,153 @@ public class GUIManager implements Listener {
|
|||||||
null, 54, menuTitle);
|
null, 54, menuTitle);
|
||||||
fillBg(g);
|
fillBg(g);
|
||||||
|
|
||||||
java.io.File f = new java.io.File(
|
java.io.File dir = plugin.getDataFolder();
|
||||||
plugin.getDataFolder(), fileName);
|
java.io.File f = null;
|
||||||
if (!f.exists()) {
|
String cleanName = fileName.trim();
|
||||||
p.sendMessage("§c文件不存在: "
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] openSubMenu: searching '"
|
||||||
|
+ cleanName + "' in '"
|
||||||
|
+ dir.getAbsolutePath() + "'");
|
||||||
|
|
||||||
|
// ★ 第1步:listFiles遍历匹配
|
||||||
|
java.io.File[] allTxt = dir.listFiles(
|
||||||
|
(d, n) -> n.toLowerCase()
|
||||||
|
.endsWith(".txt"));
|
||||||
|
if (allTxt != null) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 目录.txt文件数: "
|
||||||
|
+ allTxt.length);
|
||||||
|
for (java.io.File ff : allTxt) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] > " + ff.getName()
|
||||||
|
+ " size=" + ff.length());
|
||||||
|
if (f == null && ff.getName()
|
||||||
|
.equalsIgnoreCase(cleanName)) {
|
||||||
|
f = ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] listFiles返回null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 第2步:直接构造路径
|
||||||
|
if (f == null) {
|
||||||
|
f = new java.io.File(dir, cleanName);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 回退构造路径: "
|
||||||
|
+ f.getAbsolutePath()
|
||||||
|
+ " exists=" + f.exists()
|
||||||
|
+ " size=" + f.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
|
||||||
|
List<String> lines = null;
|
||||||
|
if (f != null) {
|
||||||
|
// 方法1:NIO UTF-8
|
||||||
|
try {
|
||||||
|
lines = java.nio.file.Files
|
||||||
|
.readAllLines(f.toPath(),
|
||||||
|
java.nio.charset
|
||||||
|
.StandardCharsets
|
||||||
|
.UTF_8);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] UTF-8读取: "
|
||||||
|
+ lines.size() + "行");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] UTF-8失败: "
|
||||||
|
+ t.getClass().getSimpleName()
|
||||||
|
+ ": " + t.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:NIO GBK
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
|
try {
|
||||||
|
lines = java.nio.file.Files
|
||||||
|
.readAllLines(f.toPath(),
|
||||||
|
java.nio.charset
|
||||||
|
.Charset
|
||||||
|
.forName("GBK"));
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] GBK读取: "
|
||||||
|
+ lines.size() + "行");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] GBK失败: "
|
||||||
|
+ t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法3:FileInputStream原始字节
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
|
try {
|
||||||
|
java.io.FileInputStream fis =
|
||||||
|
new java.io.FileInputStream(f);
|
||||||
|
byte[] raw = fis.readAllBytes();
|
||||||
|
fis.close();
|
||||||
|
if (raw.length > 0) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 原始字节: "
|
||||||
|
+ raw.length + "B");
|
||||||
|
// 尝试UTF-8解码
|
||||||
|
String content = new String(raw,
|
||||||
|
java.nio.charset
|
||||||
|
.StandardCharsets
|
||||||
|
.UTF_8);
|
||||||
|
java.util.List<String> rawLines =
|
||||||
|
java.util.Arrays.asList(
|
||||||
|
content.split("\\r?\\n"));
|
||||||
|
lines = rawLines;
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 原始读取: "
|
||||||
|
+ lines.size() + "行");
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Menu] 原始读取失败: "
|
||||||
|
+ t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 打印前5行调试
|
||||||
|
if (lines != null && !lines.isEmpty()) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] 最终行数: "
|
||||||
|
+ lines.size());
|
||||||
|
for (int i = 0;
|
||||||
|
i < Math.min(5, lines.size());
|
||||||
|
i++) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Menu] L" + (i + 1)
|
||||||
|
+ ": [" + lines.get(i)
|
||||||
|
+ "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
|
p.sendMessage("§c§l子菜单读取失败§r §7"
|
||||||
+ fileName);
|
+ fileName);
|
||||||
|
p.sendMessage("§7文件: "
|
||||||
|
+ (f != null
|
||||||
|
? f.getAbsolutePath()
|
||||||
|
+ " (" + f.length() + "B)"
|
||||||
|
: "null"));
|
||||||
|
p.sendMessage("§7存在: "
|
||||||
|
+ (f != null ? f.exists() : "N/A"));
|
||||||
|
if (allTxt != null) {
|
||||||
|
p.sendMessage("§7目录.txt文件:");
|
||||||
|
for (java.io.File ff : allTxt) {
|
||||||
|
p.sendMessage("§7 "
|
||||||
|
+ ff.getName()
|
||||||
|
+ " (" + ff.length()
|
||||||
|
+ "B)");
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
List<String> lines =
|
|
||||||
java.nio.file.Files
|
|
||||||
.readAllLines(
|
|
||||||
f.toPath(),
|
|
||||||
java.nio.charset
|
|
||||||
.StandardCharsets
|
|
||||||
.UTF_8);
|
|
||||||
List<String[]> items =
|
List<String[]> items =
|
||||||
new ArrayList<>();
|
new ArrayList<>();
|
||||||
StringBuilder buf =
|
StringBuilder buf =
|
||||||
@@ -478,11 +618,6 @@ public class GUIManager implements Listener {
|
|||||||
it.setItemMeta(im);
|
it.setItemMeta(im);
|
||||||
g.setItem(i, it);
|
g.setItem(i, it);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning(
|
|
||||||
"[Menu] 子菜单读取失败: "
|
|
||||||
+ e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
g.setItem(49, mkItem(
|
g.setItem(49, mkItem(
|
||||||
Material.ARROW,
|
Material.ARROW,
|
||||||
@@ -1571,6 +1706,11 @@ public class GUIManager implements Listener {
|
|||||||
Inventory topInv,
|
Inventory topInv,
|
||||||
ItemStack item, int removeSlot) {
|
ItemStack item, int removeSlot) {
|
||||||
|
|
||||||
|
// ★ 拦截宝箱自定义物品(事不过三原则)
|
||||||
|
if (handleTreasureItemBlock(p, item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int amount = item.getAmount();
|
int amount = item.getAmount();
|
||||||
removeSnowballFromInventory(p); //没收雪球菜单
|
removeSnowballFromInventory(p); //没收雪球菜单
|
||||||
// 1. 从来源移除
|
// 1. 从来源移除
|
||||||
@@ -1697,6 +1837,75 @@ public class GUIManager implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** ★ 检测宝箱自定义物品(lore 含 §0§k 或 CUSTOM 标记) */
|
||||||
|
boolean isTreasureCustomItem(ItemStack item) {
|
||||||
|
if (item == null) return false;
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta == null) return false;
|
||||||
|
List<String> lore = meta.getLore();
|
||||||
|
if (lore == null) return false;
|
||||||
|
for (String line : lore) {
|
||||||
|
if (line.contains("\u00a70\u00a7k")
|
||||||
|
|| line.contains("CUSTOM")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宝箱自定义物品拦截(事不过三原则)
|
||||||
|
* @param player 玩家
|
||||||
|
* @param item 要存入的物品
|
||||||
|
* @return true表示已拦截(调用方应return),false表示未拦截(继续原逻辑)
|
||||||
|
*/
|
||||||
|
public boolean handleTreasureItemBlock(Player player, ItemStack item) {
|
||||||
|
if (!isTreasureCustomItem(item)) {
|
||||||
|
return false; // 不是宝箱物品,不拦截
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = player.getUniqueId() + ":" + item.getType().name();
|
||||||
|
int count = treasureMenuBlockCount.getOrDefault(key, 0) + 1;
|
||||||
|
treasureMenuBlockCount.put(key, count);
|
||||||
|
|
||||||
|
if (count < 3) {
|
||||||
|
// 第1次或第2次:警告
|
||||||
|
player.sendMessage("§c§l[宝箱物品] §e该物品是宝箱自定义物品,无法存入菜单!");
|
||||||
|
player.sendMessage("§c§l[宝箱物品] §e第" + count + "/3次警告,第三次将没收物品!");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 第3次:没收物品
|
||||||
|
// 1. 从玩家背包移除该物品
|
||||||
|
boolean removed = false;
|
||||||
|
for (int i = 0; i < player.getInventory().getSize(); i++) {
|
||||||
|
ItemStack invItem = player.getInventory().getItem(i);
|
||||||
|
if (invItem != null && invItem.isSimilar(item)) {
|
||||||
|
player.getInventory().setItem(i, null);
|
||||||
|
removed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 如果背包没找到,可能在光标上(普通点击/拖拽场景)
|
||||||
|
if (!removed) {
|
||||||
|
org.bukkit.inventory.InventoryView view = player.getOpenInventory();
|
||||||
|
ItemStack cursor = view.getCursor();
|
||||||
|
if (cursor != null && cursor.isSimilar(item)) {
|
||||||
|
view.setCursor(null);
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. 清零计数器
|
||||||
|
treasureMenuBlockCount.remove(key);
|
||||||
|
// 4. 发送没收提示
|
||||||
|
player.sendMessage("§c§l[宝箱物品] §e你多次尝试存入宝箱物品,物品已被没收!");
|
||||||
|
player.sendMessage("§c§l[宝箱物品] §c事不过三,这是第三次警告!");
|
||||||
|
// 5. 记录日志
|
||||||
|
plugin.getLogger().warning("[宝箱物品] 玩家 " + player.getName() + " 第三次尝试存入宝箱物品,物品已没收: " + item.getType().name());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== 辅助方法 =====
|
// ===== 辅助方法 =====
|
||||||
|
|
||||||
// 只匹配带[菜单]的非雪球物品
|
// 只匹配带[菜单]的非雪球物品
|
||||||
|
|||||||
@@ -172,6 +172,16 @@ public class LoginManager {
|
|||||||
plugin.getDb().setField(name,
|
plugin.getDb().setField(name,
|
||||||
"last_online_check",
|
"last_online_check",
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
|
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
plugin.webManager.syncOnlinePlayers();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
// 登录成功关闭飞行
|
// 登录成功关闭飞行
|
||||||
p.setAllowFlight(false);
|
p.setAllowFlight(false);
|
||||||
p.setFlying(false);
|
p.setFlying(false);
|
||||||
|
|||||||
+336
-119
@@ -1,36 +1,12 @@
|
|||||||
package Sdf1_login;
|
package Sdf1_login;
|
||||||
|
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import org.bukkit.event.inventory.*;
|
|
||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import org.bukkit.NamespacedKey;
|
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.potion.PotionEffectType;
|
|
||||||
import org.bukkit.potion.PotionEffect;
|
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
|
||||||
|
|
||||||
|
|
||||||
import net.milkbowl.vault.economy.Economy;
|
import net.milkbowl.vault.economy.Economy;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.*;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
@@ -39,28 +15,26 @@ import org.bukkit.event.block.BlockBreakEvent;
|
|||||||
import org.bukkit.event.block.BlockPlaceEvent;
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
import org.bukkit.event.entity.EntityDamageEvent;
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
import org.bukkit.event.entity.EntityDeathEvent;
|
||||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.inventory.CraftItemEvent;
|
||||||
import org.bukkit.event.player.PlayerMoveEvent;
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
import org.bukkit.event.player.PlayerPickupItemEvent;
|
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
import org.bukkit.event.player.*;
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.potion.PotionEffectType;
|
||||||
import org.bukkit.command.CommandExecutor;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.command.TabCompleter;
|
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
import org.bukkit.event.entity.EntityDeathEvent;
|
|
||||||
import org.bukkit.event.player.PlayerFishEvent;
|
|
||||||
import org.bukkit.scheduler.BukkitTask;
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.io.File;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
|
||||||
public class Main extends JavaPlugin
|
public class Main extends JavaPlugin
|
||||||
@@ -100,7 +74,8 @@ public class Main extends JavaPlugin
|
|||||||
private final Map<String, Integer> treasureDiscardCount =
|
private final Map<String, Integer> treasureDiscardCount =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
// 在 private WelcomeManager welcome; 字段(若不存在)添加:
|
// 在 private WelcomeManager welcome; 字段(若不存在)添加:
|
||||||
private WelcomeManager welcome;
|
public WelcomeManager welcome;
|
||||||
|
public WebManager webManager;
|
||||||
private final Set<String> pendingAdminAuth =
|
private final Set<String> pendingAdminAuth =
|
||||||
new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||||
/*
|
/*
|
||||||
@@ -190,6 +165,9 @@ public class Main extends JavaPlugin
|
|||||||
//pvp
|
//pvp
|
||||||
private PVPManager pvpManager;
|
private PVPManager pvpManager;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static class PwdRollback {
|
private static class PwdRollback {
|
||||||
final String hash;
|
final String hash;
|
||||||
final String salt;
|
final String salt;
|
||||||
@@ -386,6 +364,10 @@ public class Main extends JavaPlugin
|
|||||||
chatFilter.loadConfig();
|
chatFilter.loadConfig();
|
||||||
welcome = new WelcomeManager(this);
|
welcome = new WelcomeManager(this);
|
||||||
|
|
||||||
|
// ===== 5.5 Web通信管理器 =====
|
||||||
|
webManager = new WebManager(this);
|
||||||
|
webManager.start();
|
||||||
|
|
||||||
// ===== 6. 垃圾箱 =====
|
// ===== 6. 垃圾箱 =====
|
||||||
garbage = new GarbageManager(this);
|
garbage = new GarbageManager(this);
|
||||||
garbage.init();
|
garbage.init();
|
||||||
@@ -493,6 +475,12 @@ public class Main extends JavaPlugin
|
|||||||
// 13 ====cypay债券====
|
// 13 ====cypay债券====
|
||||||
// 债券系统(独立DB)
|
// 债券系统(独立DB)
|
||||||
bondManager = new BondManager(this);
|
bondManager = new BondManager(this);
|
||||||
|
|
||||||
|
// ★ 注册交易监听器:交易发生后立即同步到Web端
|
||||||
|
if (webManager != null) {
|
||||||
|
webManager.registerTransactionListener(bondManager);
|
||||||
|
}
|
||||||
|
|
||||||
cdkManager = new CDKManager(this);
|
cdkManager = new CDKManager(this);
|
||||||
userGroupManager = new UserGroupManager(this);
|
userGroupManager = new UserGroupManager(this);
|
||||||
cypayCommand = new CypayCommand(this);
|
cypayCommand = new CypayCommand(this);
|
||||||
@@ -558,7 +546,6 @@ public class Main extends JavaPlugin
|
|||||||
updateChecker.checkOnEnable();
|
updateChecker.checkOnEnable();
|
||||||
|
|
||||||
getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈");
|
getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈");
|
||||||
getLogger().info("sdf1系列插件包含:\nsdf1自助兑奖插件\nCY_beibao云背包\nsdf1_login登陆插件\nSdf1_game娱乐游戏插件");
|
|
||||||
// 大字画
|
// 大字画
|
||||||
getLogger().info("\n" +
|
getLogger().info("\n" +
|
||||||
" ____ _ __ _ _ _ \n" +
|
" ____ _ __ _ _ _ \n" +
|
||||||
@@ -696,7 +683,9 @@ public class Main extends JavaPlugin
|
|||||||
areaProtection.stopEnforceTask();
|
areaProtection.stopEnforceTask();
|
||||||
areaProtection.saveWhitelists();
|
areaProtection.saveWhitelists();
|
||||||
areaProtection.getAlreadyForced().clear();
|
areaProtection.getAlreadyForced().clear();
|
||||||
|
}
|
||||||
|
if (webManager != null) {
|
||||||
|
webManager.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,13 +736,27 @@ public class Main extends JavaPlugin
|
|||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Map.Entry<UUID, Long> entry =
|
Map.Entry<UUID, Long> entry =
|
||||||
it.next();
|
it.next();
|
||||||
|
|
||||||
|
Player p = Bukkit.getPlayer(
|
||||||
|
entry.getKey());
|
||||||
|
if (p == null || !p.isOnline()) {
|
||||||
|
it.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 检查Web登录验证状态(在超时踢出前检查)
|
||||||
|
if (webManager != null && webManager.isWebLoginVerified(p.getName())) {
|
||||||
|
getLogger().info("[Web登录] 玩家 " + p.getName() + " 已通过Web密码验证(超时检查中),自动登录");
|
||||||
|
webManager.clearWebLoginVerified(p.getName());
|
||||||
|
autoLogin(p, "web_password");
|
||||||
|
it.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (now - entry.getValue()
|
if (now - entry.getValue()
|
||||||
> config.loginTimeout
|
> config.loginTimeout
|
||||||
* 1000L) {
|
* 1000L) {
|
||||||
Player p = Bukkit.getPlayer(
|
if (!loggedIn.contains(
|
||||||
entry.getKey());
|
|
||||||
if (p != null && p.isOnline()
|
|
||||||
&& !loggedIn.contains(
|
|
||||||
p.getName()))
|
p.getName()))
|
||||||
p.kickPlayer(config.msg(
|
p.kickPlayer(config.msg(
|
||||||
"login_timeout"));
|
"login_timeout"));
|
||||||
@@ -781,6 +784,16 @@ public class Main extends JavaPlugin
|
|||||||
it.remove();
|
it.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 检查Web登录验证状态(Java验证成功后立即记录)
|
||||||
|
if (webManager != null && webManager.isWebLoginVerified(p.getName())) {
|
||||||
|
getLogger().info("[Web登录] 玩家 " + p.getName() + " 已通过Web密码验证(登录提醒中),自动登录");
|
||||||
|
webManager.clearWebLoginVerified(p.getName());
|
||||||
|
autoLogin(p, "web_password");
|
||||||
|
it.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 已注册→提示登录,未注册→提示注册
|
// 已注册→提示登录,未注册→提示注册
|
||||||
if (db.userExists(p.getName()))
|
if (db.userExists(p.getName()))
|
||||||
p.sendMessage(config.msg(
|
p.sendMessage(config.msg(
|
||||||
@@ -1155,6 +1168,17 @@ public class Main extends JavaPlugin
|
|||||||
joinTime.remove(p.getUniqueId());
|
joinTime.remove(p.getUniqueId());
|
||||||
recordIPLogin(p);
|
recordIPLogin(p);
|
||||||
|
|
||||||
|
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
|
||||||
|
if (webManager != null) {
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
try {
|
||||||
|
webManager.syncOnlinePlayers();
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 只在有有效备份时恢复
|
// 只在有有效备份时恢复
|
||||||
restoreInventory(p);
|
restoreInventory(p);
|
||||||
|
|
||||||
@@ -1172,6 +1196,54 @@ public class Main extends JavaPlugin
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web端密码验证入口
|
||||||
|
* PHP后端只做消息转发,密码校验必须在Java本地完成(login.db)
|
||||||
|
*/
|
||||||
|
public String handleWebPasswordVerify(String playerName, String password) {
|
||||||
|
getLogger().info("[Web密码验证] 开始验证 player=" + playerName + " pwdLen=" + (password != null ? password.length() : 0));
|
||||||
|
if (db.userExists(playerName)) {
|
||||||
|
getLogger().info("[Web密码验证] 玩家存在,获取盐值");
|
||||||
|
String salt = (String) db.getField(playerName, "password_salt");
|
||||||
|
String storedHash = (String) db.getField(playerName, "password_hash");
|
||||||
|
getLogger().info("[Web密码验证] DB中存储的hash=" + storedHash + " salt=" + salt);
|
||||||
|
if (salt != null) {
|
||||||
|
getLogger().info("[Web密码验证] 盐值获取成功,saltLen=" + salt.length());
|
||||||
|
String hash = PasswordUtils.hash(password, salt);
|
||||||
|
getLogger().info("[Web密码验证] 计算hash=" + hash + " 存储hash=" + storedHash + " 是否匹配=" + hash.equals(storedHash));
|
||||||
|
// ★ 与 /login 保持一致:先查主密码,再查临时密码
|
||||||
|
if (db.checkPassword(playerName, hash)) {
|
||||||
|
getLogger().info("[Web密码验证] ★ 主密码验证成功");
|
||||||
|
return "\"success\"";
|
||||||
|
}
|
||||||
|
getLogger().info("[Web密码验证] 主密码不匹配,检查临时密码...");
|
||||||
|
if (db.checkPasswordOrTemp(playerName, hash)) {
|
||||||
|
getLogger().info("[Web密码验证] ★ 临时密码验证成功");
|
||||||
|
return "\"success\"";
|
||||||
|
}
|
||||||
|
getLogger().info("[Web密码验证] 临时密码也不匹配,密码验证失败");
|
||||||
|
} else {
|
||||||
|
getLogger().warning("[Web密码验证] 盐值为null!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getLogger().warning("[Web密码验证] 玩家不存在: " + playerName);
|
||||||
|
return "\"not_registered\"";
|
||||||
|
}
|
||||||
|
return "\"failed\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Web登录确认(Token验证通过后的自动登录)
|
||||||
|
*/
|
||||||
|
public boolean handleWebLoginConfirmation(String playerName) {
|
||||||
|
Player p = Bukkit.getPlayer(playerName);
|
||||||
|
if (p != null && p.isOnline()) {
|
||||||
|
autoLogin(p, "web_token");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void activateBeibao(Player p) {
|
private void activateBeibao(Player p) {
|
||||||
try {
|
try {
|
||||||
org.bukkit.plugin.Plugin cy =
|
org.bukkit.plugin.Plugin cy =
|
||||||
@@ -1653,6 +1725,24 @@ public class Main extends JavaPlugin
|
|||||||
Player p = e.getPlayer();
|
Player p = e.getPlayer();
|
||||||
String name = p.getName();
|
String name = p.getName();
|
||||||
String ip = getPlayerIP(p);
|
String ip = getPlayerIP(p);
|
||||||
|
|
||||||
|
// 登录时自动生成Web登录Token
|
||||||
|
if (webManager != null) {
|
||||||
|
webManager.autoGenerateWebLoginToken(p);
|
||||||
|
// 加入时同步PHP后端的注册数据到Java本地
|
||||||
|
if (!db.userExists(name)) {
|
||||||
|
webManager.syncUserOnJoin(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 检查本地Web登录验证状态(Java验证成功后立即记录)
|
||||||
|
if (webManager.isWebLoginVerified(name)) {
|
||||||
|
getLogger().info("[Web登录] 玩家 " + name + " 已通过Web密码验证,自动登录");
|
||||||
|
webManager.clearWebLoginVerified(name);
|
||||||
|
autoLogin(p, "web_password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 强制重发资源包(清掉客户端拒绝记录)
|
// 强制重发资源包(清掉客户端拒绝记录)
|
||||||
Bukkit.getScheduler()
|
Bukkit.getScheduler()
|
||||||
.runTaskLater(this, () -> {
|
.runTaskLater(this, () -> {
|
||||||
@@ -2239,6 +2329,63 @@ public class Main extends JavaPlugin
|
|||||||
// ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理
|
// ★ Sdf1_game宝箱物品不干预,由Sdf1_game自行处理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 防止雪球菜单被投掷出去(弱网兜底 + 防止快乐恶魂投喂)
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onProjectileLaunch(
|
||||||
|
org.bukkit.event.entity.ProjectileLaunchEvent e) {
|
||||||
|
if (e.isCancelled()) return;
|
||||||
|
if (!(e.getEntity()
|
||||||
|
instanceof org.bukkit.entity.Snowball))
|
||||||
|
return;
|
||||||
|
org.bukkit.entity.Snowball sb =
|
||||||
|
(org.bukkit.entity.Snowball) e.getEntity();
|
||||||
|
// 检查发射者(可能是玩家或生物)
|
||||||
|
Object shooter = sb.getShooter();
|
||||||
|
// 如果是玩家发射菜单雪球 → 取消并归还
|
||||||
|
if (shooter instanceof Player) {
|
||||||
|
Player p = (Player) shooter;
|
||||||
|
ItemStack main =
|
||||||
|
p.getInventory().getItemInMainHand();
|
||||||
|
ItemStack off =
|
||||||
|
p.getInventory().getItemInOffHand();
|
||||||
|
if (isMenuSnowball(main)
|
||||||
|
|| isMenuSnowball(off)) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
giveMenuSnowball(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 检查雪球的item meta是否携带菜单标记(兜底:恶魂投喂时雪球已被分离)
|
||||||
|
if (sb.hasMetadata("menu_snowball")) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 防止任何生物拾取菜单雪球掉落物(防恶魂投喂)
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onEntityPickupItem(
|
||||||
|
org.bukkit.event.entity.EntityPickupItemEvent e) {
|
||||||
|
if (e.isCancelled()) return;
|
||||||
|
ItemStack item = e.getItem().getItemStack();
|
||||||
|
if (isMenuSnowball(item)) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 防止玩家右键实体时交出菜单雪球(防恶魂投喂)
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerInteractEntity(
|
||||||
|
org.bukkit.event.player.PlayerInteractEntityEvent e) {
|
||||||
|
if (e.isCancelled()) return;
|
||||||
|
Player p = e.getPlayer();
|
||||||
|
ItemStack main = p.getInventory().getItemInMainHand();
|
||||||
|
ItemStack off = p.getInventory().getItemInOffHand();
|
||||||
|
if (isMenuSnowball(main) || isMenuSnowball(off)) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onPickup(PlayerPickupItemEvent e) {
|
public void onPickup(PlayerPickupItemEvent e) {
|
||||||
@@ -3008,62 +3155,6 @@ public class Main extends JavaPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== 二级菜单(.txt)—— 移到前面 =====
|
|
||||||
if (title.startsWith("§6§l")
|
|
||||||
&& !title.equals(GUIManager.T_MAIN)
|
|
||||||
&& !title.equals(GUIManager.T_ADMIN)
|
|
||||||
&& !title.equals(GUIManager.T_MY_INFO)
|
|
||||||
&& !title.equals(GUIManager.T_INVITE)
|
|
||||||
&& !title.equals(
|
|
||||||
GUIManager.T_TASK_CENTER)
|
|
||||||
&& !title.equals(
|
|
||||||
GUIManager.T_GIFT_STAGES)
|
|
||||||
&& !title.equals("§6§l任务面板")
|
|
||||||
&& !title.equals(
|
|
||||||
"§6§l垃圾回收站")) {
|
|
||||||
e.setCancelled(true);
|
|
||||||
if (slot == 49) {
|
|
||||||
gui.openMain(p);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ★ bounds检查 ★
|
|
||||||
if (slot < 45
|
|
||||||
&& slot < e.getInventory()
|
|
||||||
.getSize()
|
|
||||||
&& e.getCurrentItem() != null) {
|
|
||||||
ItemMeta subLm =
|
|
||||||
e.getCurrentItem().getItemMeta();
|
|
||||||
if (subLm != null
|
|
||||||
&& subLm.getLore() != null) {
|
|
||||||
for (String subLn
|
|
||||||
: subLm.getLore()) {
|
|
||||||
if (subLn.contains(
|
|
||||||
"§7指令: §f")) {
|
|
||||||
String cmd =
|
|
||||||
subLn.replace(
|
|
||||||
"§7指令: §f",
|
|
||||||
"").trim();
|
|
||||||
if (cmd.isEmpty()
|
|
||||||
|| cmd.equals("/")
|
|
||||||
|| cmd.equals(
|
|
||||||
"null")) {
|
|
||||||
p.sendMessage(
|
|
||||||
"§c指令为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
p.closeInventory();
|
|
||||||
if (cmd.startsWith("/"))
|
|
||||||
cmd = cmd.substring(1);
|
|
||||||
Bukkit.dispatchCommand(
|
|
||||||
p, cmd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== [DEBUG] 标题追踪 =====
|
// ===== [DEBUG] 标题追踪 =====
|
||||||
{
|
{
|
||||||
String dt = title;
|
String dt = title;
|
||||||
@@ -3114,7 +3205,8 @@ public class Main extends JavaPlugin
|
|||||||
"§7指令: §f")) {
|
"§7指令: §f")) {
|
||||||
String cmd = subLn
|
String cmd = subLn
|
||||||
.replace("§7指令: §f", "")
|
.replace("§7指令: §f", "")
|
||||||
.trim();
|
.trim()
|
||||||
|
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
|
||||||
if (cmd.isEmpty()
|
if (cmd.isEmpty()
|
||||||
|| cmd.equals("/")
|
|| cmd.equals("/")
|
||||||
|| cmd.equals("null")) {
|
|| cmd.equals("null")) {
|
||||||
@@ -3125,7 +3217,12 @@ public class Main extends JavaPlugin
|
|||||||
if (cmd.startsWith("/")) {
|
if (cmd.startsWith("/")) {
|
||||||
cmd = cmd.substring(1);
|
cmd = cmd.substring(1);
|
||||||
}
|
}
|
||||||
Bukkit.dispatchCommand(p, cmd);
|
// ★ 子菜单项如果是.txt文件,打开子菜单而非执行命令
|
||||||
|
if (cmd.endsWith(".txt")) {
|
||||||
|
gui.openSubMenu(p, cmd);
|
||||||
|
} else {
|
||||||
|
Bukkit.dispatchCommand(p, cmd);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3375,7 +3472,8 @@ public class Main extends JavaPlugin
|
|||||||
for (String ln : meta.getLore()) {
|
for (String ln : meta.getLore()) {
|
||||||
if (ln.contains("§7指令: §f")) {
|
if (ln.contains("§7指令: §f")) {
|
||||||
String cmd = ln.replace(
|
String cmd = ln.replace(
|
||||||
"§7指令: §f", "").trim();
|
"§7指令: §f", "").trim()
|
||||||
|
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
|
||||||
if (cmd.isEmpty()
|
if (cmd.isEmpty()
|
||||||
|| cmd.equals("/")
|
|| cmd.equals("/")
|
||||||
|| cmd.equals("null")) {
|
|| cmd.equals("null")) {
|
||||||
@@ -3976,6 +4074,11 @@ public class Main extends JavaPlugin
|
|||||||
if (toIconSlot) {
|
if (toIconSlot) {
|
||||||
ItemStack dragged =
|
ItemStack dragged =
|
||||||
e.getOldCursor().clone();
|
e.getOldCursor().clone();
|
||||||
|
// ★ 拦截宝箱自定义物品(事不过三原则)
|
||||||
|
if (gui.handleTreasureItemBlock(p, dragged)) {
|
||||||
|
e.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
String b64 = gui.serializeItem(dragged);
|
String b64 = gui.serializeItem(dragged);
|
||||||
if (b64 != null) {
|
if (b64 != null) {
|
||||||
db.saveMenuIcon(p.getName(), b64,
|
db.saveMenuIcon(p.getName(), b64,
|
||||||
@@ -4131,6 +4234,9 @@ public class Main extends JavaPlugin
|
|||||||
sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家");
|
sender.sendMessage("§e/protect removewhite <玩家> §7移除模式排除玩家");
|
||||||
sender.sendMessage("§e/protect listname <区域> §7查看和平白名单");
|
sender.sendMessage("§e/protect listname <区域> §7查看和平白名单");
|
||||||
sender.sendMessage("§e/protect listwhite §7查看模式排除名单");
|
sender.sendMessage("§e/protect listwhite §7查看模式排除名单");
|
||||||
|
sender.sendMessage("§a§l欢迎游玩草原探险服务器");
|
||||||
|
sender.sendMessage("§a§l服务器ip:mc2.ypshidifu.cn");
|
||||||
|
sender.sendMessage("§a§lJava免输端口,基岩版30679");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4151,6 +4257,9 @@ public class Main extends JavaPlugin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
sender.sendMessage("§e[更新] 正在检查所有插件更新...");
|
sender.sendMessage("§e[更新] 正在检查所有插件更新...");
|
||||||
|
sender.sendMessage("§a§l欢迎游玩草原探险服务器");
|
||||||
|
sender.sendMessage("§a§l服务器ip:mc2.ypshidifu.cn");
|
||||||
|
sender.sendMessage("§a§lJava免输端口,基岩版30679");
|
||||||
checkAllPluginsUpdate(sender);
|
checkAllPluginsUpdate(sender);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4181,6 +4290,51 @@ public class Main extends JavaPlugin
|
|||||||
}
|
}
|
||||||
return shopManager.handleCommand(sender, args);
|
return shopManager.handleCommand(sender, args);
|
||||||
}
|
}
|
||||||
|
// ★ menu 子命令 - 重载菜单配置
|
||||||
|
if (cmd.getName().equalsIgnoreCase("sdf1_login")
|
||||||
|
&& args.length >= 2
|
||||||
|
&& args[0].equalsIgnoreCase("menu")
|
||||||
|
&& args[1].equalsIgnoreCase("reload")) {
|
||||||
|
if (!isAdmin(sender)) {
|
||||||
|
sender.sendMessage("§c权限不足");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (menu == null) {
|
||||||
|
sender.sendMessage("§c菜单系统未加载");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
menu.loadMenu();
|
||||||
|
sender.sendMessage("§a菜单已重载,共 "
|
||||||
|
+ menu.getItems().size()
|
||||||
|
+ " 个菜单项");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ★ weblogin 子命令 - 生成Web登录Token
|
||||||
|
if (sub.equals("weblogin")) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("§c仅玩家可使用");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (webManager == null) {
|
||||||
|
sender.sendMessage("§cWeb通信未初始化");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
webManager.handleWebLogin((Player) sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ★ 独立 /web 和 /控制台 命令 - 跟 /sdf1_login weblogin 一样
|
||||||
|
if (cmdName.equals("web") || cmdName.equals("控制台")) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("§c仅玩家可使用");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (webManager == null) {
|
||||||
|
sender.sendMessage("§cWeb通信未初始化");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
webManager.handleWebLogin((Player) sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// set
|
// set
|
||||||
if (sub.equals("set")) {
|
if (sub.equals("set")) {
|
||||||
if (!isAdmin(sender)) {
|
if (!isAdmin(sender)) {
|
||||||
@@ -4198,13 +4352,31 @@ public class Main extends JavaPlugin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String salt = (String) db.getField(tgt, "password_salt");
|
String salt = (String) db.getField(tgt, "password_salt");
|
||||||
|
if (salt == null || salt.trim().isEmpty()) {
|
||||||
|
sender.sendMessage("§c玩家密码盐值缺失,无法设置临时密码");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
salt = salt.trim();
|
||||||
|
// 清理所有非 Base64 字符(反斜杠、引号等)
|
||||||
|
salt = salt.replaceAll("[^A-Za-z0-9+/=]", "");
|
||||||
|
if (salt.isEmpty()) {
|
||||||
|
sender.sendMessage("§c玩家密码盐值格式非法,无法设置临时密码");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
String hash = PasswordUtils.hash(tempPwd, salt);
|
String hash = PasswordUtils.hash(tempPwd, salt);
|
||||||
|
// ★ 只设置临时密码,不覆盖主密码
|
||||||
db.setField(tgt, "temp_password", hash);
|
db.setField(tgt, "temp_password", hash);
|
||||||
db.setField(tgt, "temp_pw_expire",
|
db.setField(tgt, "temp_pw_expire",
|
||||||
System.currentTimeMillis() + 300000L);
|
System.currentTimeMillis() + 300000L);
|
||||||
db.setField(tgt, "temp_pw_used", 0);
|
db.setField(tgt, "temp_pw_used", 0);
|
||||||
|
sender.sendMessage("§a已为 " + tgt + " 设置临时密码: " + tempPwd);
|
||||||
|
|
||||||
// ★ 立即发邮件(踢人之前)
|
// ★ 推送密码凭证到 Web 端(含临时密码)
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
webManager.pushWebLoginCredentials();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ★ 立即发邮件和推送密码到 Web
|
||||||
String emailAddr = (String) db.getField(tgt, "email");
|
String emailAddr = (String) db.getField(tgt, "email");
|
||||||
if (emailAddr != null && !emailAddr.isEmpty()) {
|
if (emailAddr != null && !emailAddr.isEmpty()) {
|
||||||
// 获取最近登录信息
|
// 获取最近登录信息
|
||||||
@@ -4230,7 +4402,7 @@ public class Main extends JavaPlugin
|
|||||||
|
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(this,
|
Bukkit.getScheduler().runTaskAsynchronously(this,
|
||||||
() -> {
|
() -> {
|
||||||
// ★ 发送临时密码邮件(带登录信息)
|
// ★ 发送临时密码邮件
|
||||||
boolean sent = email.sendTempPassword(
|
boolean sent = email.sendTempPassword(
|
||||||
to, fName, pwd);
|
to, fName, pwd);
|
||||||
// ★ 额外发送安全通知邮件
|
// ★ 额外发送安全通知邮件
|
||||||
@@ -4259,6 +4431,13 @@ public class Main extends JavaPlugin
|
|||||||
sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知");
|
sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 推送新密码到 Web 端
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
webManager.pushWebLoginCredentials();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ★ 删除重复的推送(已在上面推送过)
|
||||||
|
|
||||||
// 踢人
|
// 踢人
|
||||||
Player tp = Bukkit.getPlayer(tgt);
|
Player tp = Bukkit.getPlayer(tgt);
|
||||||
if (tp != null && tp.isOnline())
|
if (tp != null && tp.isOnline())
|
||||||
@@ -4693,6 +4872,20 @@ public class Main extends JavaPlugin
|
|||||||
if (!cmdName.equals("sdf1_login"))
|
if (!cmdName.equals("sdf1_login"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// ★ /sdf1_login reload: 重载全部设置
|
||||||
|
if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {
|
||||||
|
if (!isAdmin(sender)) {
|
||||||
|
sender.sendMessage("§c权限不足");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
reloadConfig();
|
||||||
|
if (areaProtection != null) areaProtection.reload();
|
||||||
|
if (webManager != null) webManager.reloadWebConfig();
|
||||||
|
sender.sendMessage("§a[§2Sdf1_login§a] 插件全部设置已重载");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
if (!(sender instanceof Player)) {
|
if (!(sender instanceof Player)) {
|
||||||
sender.sendMessage(
|
sender.sendMessage(
|
||||||
@@ -4737,6 +4930,9 @@ public class Main extends JavaPlugin
|
|||||||
pendingDeleteTask = null;
|
pendingDeleteTask = null;
|
||||||
if (name == null) return;
|
if (name == null) return;
|
||||||
db.deleteUser(name);
|
db.deleteUser(name);
|
||||||
|
if (webManager != null) {
|
||||||
|
webManager.deleteWebUser(name);
|
||||||
|
}
|
||||||
getLogger().info(
|
getLogger().info(
|
||||||
"[Sdf1_login] 已删除: " + name);
|
"[Sdf1_login] 已删除: " + name);
|
||||||
Player tp = Bukkit.getPlayer(name);
|
Player tp = Bukkit.getPlayer(name);
|
||||||
@@ -4772,6 +4968,9 @@ public class Main extends JavaPlugin
|
|||||||
pendingDeleteName = null;
|
pendingDeleteName = null;
|
||||||
pendingDeleteTask = null;
|
pendingDeleteTask = null;
|
||||||
db.deleteUser(name);
|
db.deleteUser(name);
|
||||||
|
if (webManager != null) {
|
||||||
|
webManager.deleteWebUser(name);
|
||||||
|
}
|
||||||
sender.sendMessage("§a已立即删除: " + name);
|
sender.sendMessage("§a已立即删除: " + name);
|
||||||
Player tp = Bukkit.getPlayer(name);
|
Player tp = Bukkit.getPlayer(name);
|
||||||
if (tp != null && tp.isOnline())
|
if (tp != null && tp.isOnline())
|
||||||
@@ -5008,6 +5207,11 @@ public class Main extends JavaPlugin
|
|||||||
recordPasswordChange(p2.getName(), oldHash, salt);
|
recordPasswordChange(p2.getName(), oldHash, salt);
|
||||||
db.setField(p2.getName(), "password_hash", newHash);
|
db.setField(p2.getName(), "password_hash", newHash);
|
||||||
db.setField(p2.getName(), "password_salt", newSalt);
|
db.setField(p2.getName(), "password_salt", newSalt);
|
||||||
|
// ★ 同步密码到Web端(异步推送,不阻塞主线程)
|
||||||
|
final Main self = this;
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
webManager.pushWebLoginCredentials();
|
||||||
|
});
|
||||||
if (!mainOk && tempOk)
|
if (!mainOk && tempOk)
|
||||||
db.clearTempPassword(p2.getName());
|
db.clearTempPassword(p2.getName());
|
||||||
// pw 命令改密码成功后
|
// pw 命令改密码成功后
|
||||||
@@ -5282,13 +5486,18 @@ public class Main extends JavaPlugin
|
|||||||
"password_same"));
|
"password_same"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
recordPasswordChange(
|
recordPasswordChange(
|
||||||
p2.getName(),
|
p2.getName(),
|
||||||
oldHash, salt);
|
oldHash, salt);
|
||||||
db.setField(p2.getName(),
|
db.setField(p2.getName(),
|
||||||
"password_hash", newHash);
|
"password_hash", newHash);
|
||||||
db.setField(p2.getName(),
|
db.setField(p2.getName(),
|
||||||
"password_salt", newSalt);
|
"password_salt", newSalt);
|
||||||
|
// ★ 同步密码到Web端(异步推送,不阻塞主线程)
|
||||||
|
final Main self2 = this;
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||||
|
self2.webManager.pushWebLoginCredentials();
|
||||||
|
});
|
||||||
if (!mainOk && tempOk) {
|
if (!mainOk && tempOk) {
|
||||||
db.clearTempPassword(
|
db.clearTempPassword(
|
||||||
p2.getName());
|
p2.getName());
|
||||||
@@ -5767,7 +5976,7 @@ public class Main extends JavaPlugin
|
|||||||
"take", "add", "kick",
|
"take", "add", "kick",
|
||||||
"ticket", "oa", "stop",
|
"ticket", "oa", "stop",
|
||||||
"shopadd", "shopdel", "back", "radio", "back", "shop",
|
"shopadd", "shopdel", "back", "radio", "back", "shop",
|
||||||
"update", "updateall"));
|
"update", "updateall", "menu"));
|
||||||
} else {
|
} else {
|
||||||
list.addAll(Arrays.asList(
|
list.addAll(Arrays.asList(
|
||||||
"pw", "email", "sign",
|
"pw", "email", "sign",
|
||||||
@@ -5775,6 +5984,14 @@ public class Main extends JavaPlugin
|
|||||||
"ticket", "oa", "stop"));
|
"ticket", "oa", "stop"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ★ menu reload Tab补全
|
||||||
|
if (cmd.getName().equalsIgnoreCase("sdf1_login")
|
||||||
|
&& args.length == 2
|
||||||
|
&& args[0].equalsIgnoreCase("menu")) {
|
||||||
|
if (isAdmin(sender)) {
|
||||||
|
list.add("reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
// 独立 /update 命令无子命令
|
// 独立 /update 命令无子命令
|
||||||
if (cmd.getName().equalsIgnoreCase("update")) {
|
if (cmd.getName().equalsIgnoreCase("update")) {
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 读取菜单.txt =====
|
// ===== 读取菜单.txt(主菜单唯一来源) =====
|
||||||
public void loadMenu() {
|
public void loadMenu() {
|
||||||
items.clear();
|
items.clear();
|
||||||
|
|
||||||
|
// 优先菜单.txt,备选menu.txt
|
||||||
File f = new File(
|
File f = new File(
|
||||||
plugin.getDataFolder(),
|
plugin.getDataFolder(),
|
||||||
"菜单.txt");
|
"菜单.txt");
|
||||||
if (!f.exists()) createDefault(f);
|
if (!f.exists()) {
|
||||||
|
File alt = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"menu.txt");
|
||||||
|
if (alt.exists()) {
|
||||||
|
f = alt;
|
||||||
|
} else {
|
||||||
|
createDefault(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
parseFile(f);
|
parseFile(f);
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Menu] 加载 "
|
"[Menu] 加载 " + items.size()
|
||||||
+ items.size()
|
+ " 个菜单项 (源: "
|
||||||
+ " 个菜单项");
|
+ f.getName() + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseFile(File f) {
|
private void parseFile(File f) {
|
||||||
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
|
|||||||
|
|
||||||
// ===== 保存 =====
|
// ===== 保存 =====
|
||||||
public void saveMenu() {
|
public void saveMenu() {
|
||||||
|
// 保存到加载源文件
|
||||||
File f = new File(
|
File f = new File(
|
||||||
plugin.getDataFolder(),
|
plugin.getDataFolder(),
|
||||||
"菜单.txt");
|
"菜单.txt");
|
||||||
|
if (!f.exists()) {
|
||||||
|
File alt = new File(
|
||||||
|
plugin.getDataFolder(),
|
||||||
|
"menu.txt");
|
||||||
|
if (alt.exists()) f = alt;
|
||||||
|
}
|
||||||
try (PrintWriter pw = new PrintWriter(
|
try (PrintWriter pw = new PrintWriter(
|
||||||
new OutputStreamWriter(
|
new OutputStreamWriter(
|
||||||
new FileOutputStream(f),
|
new FileOutputStream(f),
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ public class PasswordUtils {
|
|||||||
try {
|
try {
|
||||||
MessageDigest md =
|
MessageDigest md =
|
||||||
MessageDigest.getInstance("SHA-256");
|
MessageDigest.getInstance("SHA-256");
|
||||||
md.update(Base64.getDecoder().decode(salt));
|
// 清理非 Base64 字符,防止数据库中转义字符导致解析错误
|
||||||
|
String cleanSalt = salt.replaceAll("[^A-Za-z0-9+/=]", "");
|
||||||
|
if (cleanSalt.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Invalid salt: " + salt);
|
||||||
|
}
|
||||||
|
md.update(Base64.getDecoder().decode(cleanSalt));
|
||||||
byte[] hash =
|
byte[] hash =
|
||||||
md.digest(password.getBytes("UTF-8"));
|
md.digest(password.getBytes("UTF-8"));
|
||||||
return Base64.getEncoder().encodeToString(hash);
|
return Base64.getEncoder().encodeToString(hash);
|
||||||
|
|||||||
@@ -920,8 +920,8 @@ public class ShopManager implements Listener {
|
|||||||
&& !n.equals("coupons.md"));
|
&& !n.equals("coupons.md"));
|
||||||
}
|
}
|
||||||
if (allMd == null) {
|
if (allMd == null) {
|
||||||
// plugin.getLogger().warning(
|
plugin.getLogger().warning(
|
||||||
// "[Shop] shop目录为空");
|
"[Shop] shop目录为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,7 +1372,7 @@ public class ShopManager implements Listener {
|
|||||||
for (ShopCategory cat : categories)
|
for (ShopCategory cat : categories)
|
||||||
saveCategory(cat);
|
saveCategory(cat);
|
||||||
}
|
}
|
||||||
private ItemStack getShopStack(
|
public ItemStack getShopStack(
|
||||||
ShopItem item, int amount) {
|
ShopItem item, int amount) {
|
||||||
// 优先级1: 手动捕获的NBT数据
|
// 优先级1: 手动捕获的NBT数据
|
||||||
ItemStack cached =
|
ItemStack cached =
|
||||||
|
|||||||
@@ -48,18 +48,28 @@ public class UpdateChecker {
|
|||||||
private static final String GITHUB_REPO = "sdf1_login";
|
private static final String GITHUB_REPO = "sdf1_login";
|
||||||
private static final String GITEE_OWNER = "nihaoshidifu";
|
private static final String GITEE_OWNER = "nihaoshidifu";
|
||||||
private static final String GITEE_REPO = "sdf1_login";
|
private static final String GITEE_REPO = "sdf1_login";
|
||||||
|
private static final String GYP_OWNER = "youpaishidifu";
|
||||||
|
private static final String GYP_REPO = "Sdf1_login";
|
||||||
|
|
||||||
// API 地址(使用releases/latest获取最新版本)
|
// API 地址(使用releases/latest获取最新版本)
|
||||||
private static final String GITHUB_API =
|
private static final String GITHUB_API =
|
||||||
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
|
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
|
||||||
private static final String GITEE_API =
|
private static final String GITEE_API =
|
||||||
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
|
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
|
||||||
|
private static final String GYP_API =
|
||||||
|
"https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest";
|
||||||
|
|
||||||
|
// 第三路备选:宝塔静态HTML
|
||||||
|
private static final String FALLBACK_HTML =
|
||||||
|
"https://caoyuan.ypshidifu.cn/update_check.html";
|
||||||
|
|
||||||
// 仓库页面链接
|
// 仓库页面链接
|
||||||
private static final String GITHUB_LINK =
|
private static final String GITHUB_LINK =
|
||||||
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
|
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
|
||||||
private static final String GITEE_LINK =
|
private static final String GITEE_LINK =
|
||||||
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
|
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
|
||||||
|
private static final String GYP_LINK =
|
||||||
|
"https://git.ypshidifu.cn/" + GYP_OWNER + "/" + GYP_REPO + "/releases";
|
||||||
|
|
||||||
// 单次请求超时(毫秒)
|
// 单次请求超时(毫秒)
|
||||||
private static final int TIMEOUT_MS = 5000;
|
private static final int TIMEOUT_MS = 5000;
|
||||||
@@ -153,18 +163,31 @@ public class UpdateChecker {
|
|||||||
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
|
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 双通道并行执行,一路成功掐断另一路
|
// ★ 三通道并行执行,一路成功掐断其余
|
||||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||||
|
AtomicReference<CheckResult> gypResult = new AtomicReference<>();
|
||||||
AtomicReference<CheckResult> ghResult = new AtomicReference<>();
|
AtomicReference<CheckResult> ghResult = new AtomicReference<>();
|
||||||
AtomicReference<CheckResult> geResult = new AtomicReference<>();
|
AtomicReference<CheckResult> geResult = new AtomicReference<>();
|
||||||
CountDownLatch latch = new CountDownLatch(2);
|
CountDownLatch latch = new CountDownLatch(3);
|
||||||
|
|
||||||
|
// 通道0:git.ypshidifu.cn(最高优先级,Gitea格式)
|
||||||
|
Thread gypThread = new Thread(() -> {
|
||||||
|
if (!cancelled.get()) {
|
||||||
|
gypResult.set(tryChannel(GYP_API, "git.ypshidifu.cn", GYP_LINK));
|
||||||
|
if (gypResult.get().isSuccess()) {
|
||||||
|
cancelled.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
gypThread.setDaemon(true);
|
||||||
|
|
||||||
// 通道1:GitHub
|
// 通道1:GitHub
|
||||||
Thread ghThread = new Thread(() -> {
|
Thread ghThread = new Thread(() -> {
|
||||||
if (!cancelled.get()) {
|
if (!cancelled.get()) {
|
||||||
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
|
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
|
||||||
if (ghResult.get().isSuccess()) {
|
if (ghResult.get().isSuccess()) {
|
||||||
cancelled.set(true); // 成功则取消另一路
|
cancelled.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
@@ -176,63 +199,81 @@ public class UpdateChecker {
|
|||||||
if (!cancelled.get()) {
|
if (!cancelled.get()) {
|
||||||
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
|
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
|
||||||
if (geResult.get().isSuccess()) {
|
if (geResult.get().isSuccess()) {
|
||||||
cancelled.set(true); // 成功则取消另一路
|
cancelled.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
});
|
});
|
||||||
geThread.setDaemon(true);
|
geThread.setDaemon(true);
|
||||||
|
|
||||||
|
gypThread.start();
|
||||||
ghThread.start();
|
ghThread.start();
|
||||||
geThread.start();
|
geThread.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
latch.await(); // 等待两路都完成
|
latch.await();
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckResult gyp = gypResult.get();
|
||||||
CheckResult gh = ghResult.get();
|
CheckResult gh = ghResult.get();
|
||||||
CheckResult ge = geResult.get();
|
CheckResult ge = geResult.get();
|
||||||
|
|
||||||
// 选择成功的结果
|
// 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee)
|
||||||
CheckResult chosen = null;
|
CheckResult chosen = null;
|
||||||
|
|
||||||
if (gh != null && gh.isSuccess()) {
|
if (gyp != null && gyp.isSuccess()) {
|
||||||
|
chosen = gyp;
|
||||||
|
} else if (gh != null && gh.isSuccess()) {
|
||||||
chosen = gh;
|
chosen = gh;
|
||||||
} else if (ge != null && ge.isSuccess()) {
|
} else if (ge != null && ge.isSuccess()) {
|
||||||
chosen = ge;
|
chosen = ge;
|
||||||
} else {
|
} else {
|
||||||
// 双通道均失败
|
// ★ 双通道均失败,尝试第三路备选(宝塔静态HTML)
|
||||||
failCount++;
|
plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
|
||||||
StringBuilder sb = new StringBuilder();
|
CheckResult fbResult = tryFallbackChannel();
|
||||||
if (gh != null && gh.error != null) {
|
if (fbResult != null && fbResult.isSuccess()) {
|
||||||
sb.append("§7GitHub: §c").append(gh.error);
|
chosen = fbResult;
|
||||||
|
plugin.getLogger().info("[Sdf1_login] 第三路备选 BaoTa 检查成功: " + fbResult.version);
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (gyp != null && gyp.error != null) {
|
||||||
|
sb.append("§7git.ypshidifu.cn: §c").append(gyp.error);
|
||||||
|
}
|
||||||
|
if (gh != null && gh.error != null) {
|
||||||
|
if (sb.length() > 0) sb.append("\n");
|
||||||
|
sb.append("§7GitHub: §c").append(gh.error);
|
||||||
|
}
|
||||||
|
if (ge != null && ge.error != null) {
|
||||||
|
if (sb.length() > 0) sb.append("\n");
|
||||||
|
sb.append("§7Gitee: §c").append(ge.error);
|
||||||
|
}
|
||||||
|
if (fbResult != null && fbResult.error != null) {
|
||||||
|
if (sb.length() > 0) sb.append("\n");
|
||||||
|
sb.append("§7BaoTa: §c").append(fbResult.error);
|
||||||
|
}
|
||||||
|
if (failCount >= TRIP_THRESHOLD) {
|
||||||
|
circuitTripped = true;
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[Sdf1_login] 更新检测已熔断,连续失败" + failCount + "次");
|
||||||
|
scheduleRetry();
|
||||||
|
}
|
||||||
|
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已熔断,2分钟后自动重试,或重启服务器重置");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (ge != null && 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 + "次");
|
|
||||||
// 熔断后安排2分钟后重试
|
|
||||||
scheduleRetry();
|
|
||||||
}
|
|
||||||
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已熔断,2分钟后自动重试,或重启服务器重置");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 至少一个通道成功
|
// 至少一个通道成功
|
||||||
@@ -296,10 +337,15 @@ public class UpdateChecker {
|
|||||||
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
|
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示另一通道的失败原因
|
// 显示其他失败通道的错误信息
|
||||||
CheckResult other = (src.equals("GitHub")) ? ge : gh;
|
if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) {
|
||||||
if (other != null && !other.isSuccess() && other.error != null) {
|
sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.error);
|
||||||
sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error);
|
}
|
||||||
|
if (gh != null && !gh.isSuccess() && gh.error != null && !src.equals("GitHub")) {
|
||||||
|
sender.sendMessage("§7[Sdf1_login] [更新] GitHub 不可用: " + gh.error);
|
||||||
|
}
|
||||||
|
if (ge != null && !ge.isSuccess() && ge.error != null && !src.equals("Gitee")) {
|
||||||
|
sender.sendMessage("§7[Sdf1_login] [更新] Gitee 不可用: " + ge.error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -314,6 +360,81 @@ public class UpdateChecker {
|
|||||||
|
|
||||||
// ========== HTTP 请求 ==========
|
// ========== HTTP 请求 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三路备选:从宝塔静态HTML解析版本信息
|
||||||
|
* HTML中用 <!--UPDATE_DATA 包裹结构化数据:
|
||||||
|
* pluginName|version|downloadLink
|
||||||
|
*/
|
||||||
|
private CheckResult tryFallbackChannel() {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
URL url = new URL(FALLBACK_HTML);
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("User-Agent", "Sdf1-Plugin-UpdateChecker/1.0");
|
||||||
|
conn.setConnectTimeout(TIMEOUT_MS);
|
||||||
|
conn.setReadTimeout(TIMEOUT_MS);
|
||||||
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code != 200) {
|
||||||
|
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||||
|
"BaoTa: HTTP " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(conn.getInputStream(), "UTF-8"));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
// 解析 <!--UPDATE_DATA ... UPDATE_DATA--> 块
|
||||||
|
String html = sb.toString();
|
||||||
|
|
||||||
|
// ★ 去除 Cloudflare 注入的 CDN-CGI 链接(<a href=...>...</a>)
|
||||||
|
html = html.replaceAll("<a\\s[^>]*>[^<]*</a>", "");
|
||||||
|
|
||||||
|
int start = html.indexOf("<!--UPDATE_DATA");
|
||||||
|
int end = html.indexOf("UPDATE_DATA-->");
|
||||||
|
if (start < 0 || end < 0 || end <= start) {
|
||||||
|
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||||
|
"BaoTa: 无法解析更新数据块");
|
||||||
|
}
|
||||||
|
String block = html.substring(start, end);
|
||||||
|
|
||||||
|
// 逐行查找匹配当前插件名的记录
|
||||||
|
String[] lines = block.split("\n");
|
||||||
|
for (String l : lines) {
|
||||||
|
l = l.trim();
|
||||||
|
// 跳过空行、注释标记行、HTML注入标签
|
||||||
|
if (l.isEmpty() || l.startsWith("<!--") || l.startsWith("<")) continue;
|
||||||
|
if (l.startsWith("Sdf1_login|")) {
|
||||||
|
// 格式: Sdf1_login|1.0|https://...
|
||||||
|
String[] parts = l.split("\\|");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
String ver = parts[1].trim();
|
||||||
|
String link = parts.length >= 3 ? parts[2].trim() : GITHUB_LINK;
|
||||||
|
return new CheckResult(ver, "BaoTa", link, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||||
|
"BaoTa: 未找到 Sdf1_login 的版本信息");
|
||||||
|
|
||||||
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
|
return new CheckResult(null, "BaoTa", FALLBACK_HTML, "BaoTa: 连接超时");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||||
|
"BaoTa: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
if (conn != null) conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class CheckResult {
|
private static class CheckResult {
|
||||||
final String version;
|
final String version;
|
||||||
final String source;
|
final String source;
|
||||||
@@ -340,9 +461,10 @@ public class UpdateChecker {
|
|||||||
conn.setRequestMethod("GET");
|
conn.setRequestMethod("GET");
|
||||||
conn.setRequestProperty("User-Agent",
|
conn.setRequestProperty("User-Agent",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
|
||||||
conn.setRequestProperty("Accept", "*/*");
|
conn.setRequestProperty("Accept", "*/*");
|
||||||
conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9");
|
conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||||
|
conn.setRequestProperty("Connection", "keep-alive");
|
||||||
conn.setConnectTimeout(TIMEOUT_MS);
|
conn.setConnectTimeout(TIMEOUT_MS);
|
||||||
conn.setReadTimeout(TIMEOUT_MS);
|
conn.setReadTimeout(TIMEOUT_MS);
|
||||||
conn.setInstanceFollowRedirects(true);
|
conn.setInstanceFollowRedirects(true);
|
||||||
@@ -353,7 +475,10 @@ public class UpdateChecker {
|
|||||||
return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)");
|
return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)");
|
||||||
}
|
}
|
||||||
if (code == 403) {
|
if (code == 403) {
|
||||||
return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)");
|
String errBody = "";
|
||||||
|
try { BufferedReader er = new BufferedReader(new InputStreamReader(conn.getErrorStream())); StringBuilder eb = new StringBuilder(); String el; while ((el = er.readLine()) != null) eb.append(el); er.close(); errBody = eb.toString(); } catch (Exception ignored) {}
|
||||||
|
String detail = errBody.isEmpty() ? "" : " | " + errBody.substring(0, Math.min(errBody.length(), 200));
|
||||||
|
return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)" + detail);
|
||||||
}
|
}
|
||||||
if (code != 200) {
|
if (code != 200) {
|
||||||
return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code);
|
return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
name: Sdf1_login
|
name: Sdf1_login
|
||||||
version: 2.1
|
version: 2.6
|
||||||
main: Sdf1_login.Main
|
main: Sdf1_login.Main
|
||||||
api-version: '1.21'
|
api-version: '1.21'
|
||||||
commands:
|
commands:
|
||||||
@@ -96,7 +96,7 @@ commands:
|
|||||||
商店:
|
商店:
|
||||||
description: 打开商店
|
description: 打开商店
|
||||||
usage: /商店
|
usage: /商店
|
||||||
#testorder:
|
# testorder:
|
||||||
# description: 测试订单中心
|
# description: 测试订单中心
|
||||||
# usage: /testorder
|
# usage: /testorder
|
||||||
protect:
|
protect:
|
||||||
@@ -104,4 +104,10 @@ commands:
|
|||||||
usage: /protect
|
usage: /protect
|
||||||
update:
|
update:
|
||||||
description: 检查所有插件更新(集群控制)
|
description: 检查所有插件更新(集群控制)
|
||||||
usage: /update
|
usage: /update
|
||||||
|
web:
|
||||||
|
description: 生成Web端登录令牌
|
||||||
|
usage: /web
|
||||||
|
控制台:
|
||||||
|
description: 生成Web端登录令牌(中文版)
|
||||||
|
usage: /控制台
|
||||||
Reference in New Issue
Block a user