fix: 修复Web注册不写注册IP、玩家登录IP的bug; 新增库存高频轮询; 更新README下载地址
This commit is contained in:
@@ -565,7 +565,7 @@ http-port=443
|
||||
|
||||
|
||||
## 4. 下载地址
|
||||
- 官方仓库:[https://git.ypshidifu.cn/youpaishidifu/Sdf1_login](https://git.ypshidifu.cn/youpaishidifu/Sdf1_login) (推荐)
|
||||
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
|
||||
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
||||
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||
|
||||
+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)
|
||||
@@ -399,6 +399,8 @@ public class GUIManager implements Listener {
|
||||
|
||||
public void openSubMenu(
|
||||
Player p, String fileName) {
|
||||
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
|
||||
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
|
||||
String menuTitle =
|
||||
"§6§l" + fileName
|
||||
.replace(".txt", "");
|
||||
@@ -406,21 +408,153 @@ public class GUIManager implements Listener {
|
||||
null, 54, menuTitle);
|
||||
fillBg(g);
|
||||
|
||||
java.io.File f = new java.io.File(
|
||||
plugin.getDataFolder(), fileName);
|
||||
if (!f.exists()) {
|
||||
p.sendMessage("§c文件不存在: "
|
||||
java.io.File dir = plugin.getDataFolder();
|
||||
java.io.File f = null;
|
||||
String cleanName = fileName.trim();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] openSubMenu: searching '"
|
||||
+ cleanName + "' in '"
|
||||
+ dir.getAbsolutePath() + "'");
|
||||
|
||||
// ★ 第1步:listFiles遍历匹配
|
||||
java.io.File[] allTxt = dir.listFiles(
|
||||
(d, n) -> n.toLowerCase()
|
||||
.endsWith(".txt"));
|
||||
if (allTxt != null) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 目录.txt文件数: "
|
||||
+ allTxt.length);
|
||||
for (java.io.File ff : allTxt) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] > " + ff.getName()
|
||||
+ " size=" + ff.length());
|
||||
if (f == null && ff.getName()
|
||||
.equalsIgnoreCase(cleanName)) {
|
||||
f = ff;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] listFiles返回null!");
|
||||
}
|
||||
|
||||
// ★ 第2步:直接构造路径
|
||||
if (f == null) {
|
||||
f = new java.io.File(dir, cleanName);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 回退构造路径: "
|
||||
+ f.getAbsolutePath()
|
||||
+ " exists=" + f.exists()
|
||||
+ " size=" + f.length());
|
||||
}
|
||||
|
||||
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
|
||||
List<String> lines = null;
|
||||
if (f != null) {
|
||||
// 方法1:NIO UTF-8
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] UTF-8读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] UTF-8失败: "
|
||||
+ t.getClass().getSimpleName()
|
||||
+ ": " + t.getMessage());
|
||||
}
|
||||
|
||||
// 方法2:NIO GBK
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.Charset
|
||||
.forName("GBK"));
|
||||
plugin.getLogger().info(
|
||||
"[Menu] GBK读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] GBK失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 方法3:FileInputStream原始字节
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
java.io.FileInputStream fis =
|
||||
new java.io.FileInputStream(f);
|
||||
byte[] raw = fis.readAllBytes();
|
||||
fis.close();
|
||||
if (raw.length > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始字节: "
|
||||
+ raw.length + "B");
|
||||
// 尝试UTF-8解码
|
||||
String content = new String(raw,
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
java.util.List<String> rawLines =
|
||||
java.util.Arrays.asList(
|
||||
content.split("\\r?\\n"));
|
||||
lines = rawLines;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始读取: "
|
||||
+ lines.size() + "行");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 原始读取失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 打印前5行调试
|
||||
if (lines != null && !lines.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 最终行数: "
|
||||
+ lines.size());
|
||||
for (int i = 0;
|
||||
i < Math.min(5, lines.size());
|
||||
i++) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + (i + 1)
|
||||
+ ": [" + lines.get(i)
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
p.sendMessage("§c§l子菜单读取失败§r §7"
|
||||
+ fileName);
|
||||
p.sendMessage("§7文件: "
|
||||
+ (f != null
|
||||
? f.getAbsolutePath()
|
||||
+ " (" + f.length() + "B)"
|
||||
: "null"));
|
||||
p.sendMessage("§7存在: "
|
||||
+ (f != null ? f.exists() : "N/A"));
|
||||
if (allTxt != null) {
|
||||
p.sendMessage("§7目录.txt文件:");
|
||||
for (java.io.File ff : allTxt) {
|
||||
p.sendMessage("§7 "
|
||||
+ ff.getName()
|
||||
+ " (" + ff.length()
|
||||
+ "B)");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<String> lines =
|
||||
java.nio.file.Files
|
||||
.readAllLines(
|
||||
f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
List<String[]> items =
|
||||
new ArrayList<>();
|
||||
StringBuilder buf =
|
||||
@@ -484,11 +618,6 @@ public class GUIManager implements Listener {
|
||||
it.setItemMeta(im);
|
||||
g.setItem(i, it);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 子菜单读取失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
g.setItem(49, mkItem(
|
||||
Material.ARROW,
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.CraftItemEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
@@ -2328,6 +2329,63 @@ public class Main extends JavaPlugin
|
||||
// ★ 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
|
||||
public void onPickup(PlayerPickupItemEvent e) {
|
||||
@@ -3097,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] 标题追踪 =====
|
||||
{
|
||||
String dt = title;
|
||||
@@ -3203,7 +3205,8 @@ public class Main extends JavaPlugin
|
||||
"§7指令: §f")) {
|
||||
String cmd = subLn
|
||||
.replace("§7指令: §f", "")
|
||||
.trim();
|
||||
.trim()
|
||||
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
|
||||
if (cmd.isEmpty()
|
||||
|| cmd.equals("/")
|
||||
|| cmd.equals("null")) {
|
||||
@@ -3214,7 +3217,12 @@ public class Main extends JavaPlugin
|
||||
if (cmd.startsWith("/")) {
|
||||
cmd = cmd.substring(1);
|
||||
}
|
||||
Bukkit.dispatchCommand(p, cmd);
|
||||
// ★ 子菜单项如果是.txt文件,打开子菜单而非执行命令
|
||||
if (cmd.endsWith(".txt")) {
|
||||
gui.openSubMenu(p, cmd);
|
||||
} else {
|
||||
Bukkit.dispatchCommand(p, cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3464,7 +3472,8 @@ public class Main extends JavaPlugin
|
||||
for (String ln : meta.getLore()) {
|
||||
if (ln.contains("§7指令: §f")) {
|
||||
String cmd = ln.replace(
|
||||
"§7指令: §f", "").trim();
|
||||
"§7指令: §f", "").trim()
|
||||
.replaceAll("\u00A7[0-9a-fk-or]", ""); // 剥离残留MC颜色代码
|
||||
if (cmd.isEmpty()
|
||||
|| cmd.equals("/")
|
||||
|| cmd.equals("null")) {
|
||||
@@ -4281,6 +4290,25 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
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)) {
|
||||
@@ -5948,7 +5976,7 @@ public class Main extends JavaPlugin
|
||||
"take", "add", "kick",
|
||||
"ticket", "oa", "stop",
|
||||
"shopadd", "shopdel", "back", "radio", "back", "shop",
|
||||
"update", "updateall"));
|
||||
"update", "updateall", "menu"));
|
||||
} else {
|
||||
list.addAll(Arrays.asList(
|
||||
"pw", "email", "sign",
|
||||
@@ -5956,6 +5984,14 @@ public class Main extends JavaPlugin
|
||||
"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 命令无子命令
|
||||
if (cmd.getName().equalsIgnoreCase("update")) {
|
||||
return list;
|
||||
|
||||
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ===== 读取菜单.txt =====
|
||||
// ===== 读取菜单.txt(主菜单唯一来源) =====
|
||||
public void loadMenu() {
|
||||
items.clear();
|
||||
|
||||
// 优先菜单.txt,备选menu.txt
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) createDefault(f);
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) {
|
||||
f = alt;
|
||||
} else {
|
||||
createDefault(f);
|
||||
}
|
||||
}
|
||||
parseFile(f);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 加载 "
|
||||
+ items.size()
|
||||
+ " 个菜单项");
|
||||
"[Menu] 加载 " + items.size()
|
||||
+ " 个菜单项 (源: "
|
||||
+ f.getName() + ")");
|
||||
}
|
||||
|
||||
private void parseFile(File f) {
|
||||
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
|
||||
|
||||
// ===== 保存 =====
|
||||
public void saveMenu() {
|
||||
// 保存到加载源文件
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) f = alt;
|
||||
}
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
|
||||
@@ -237,6 +237,7 @@ public class WebManager {
|
||||
syncBondBalances();
|
||||
syncBondTransactions();
|
||||
syncAllPlayerIps();
|
||||
syncServiceProviders();
|
||||
// 首次同步只打一次,后续由调度器静默执行
|
||||
plugin.getLogger().info("[Web通信] 首次全量同步完成");
|
||||
|
||||
@@ -259,6 +260,9 @@ public class WebManager {
|
||||
// ★ 启动交易高频轮询(每5秒检查PHP端是否有待处理交易)
|
||||
startTransactionPolling();
|
||||
|
||||
// ★ 启动库存高频轮询(每5秒检测Web库存改动,无改动跳过,有改动立即更新)
|
||||
startShopStockFastPolling();
|
||||
|
||||
// ★ 启动CDK验证轮询(每2秒检查PHP端CDK验证请求)
|
||||
startCdkValidationPolling();
|
||||
|
||||
@@ -631,6 +635,7 @@ public class WebManager {
|
||||
pushWebLoginCredentials();
|
||||
syncUserRegistrations();
|
||||
syncAllPlayerIps();
|
||||
syncServiceProviders();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -708,6 +713,7 @@ public class WebManager {
|
||||
syncUserRegistrations();
|
||||
syncBondTransactions();
|
||||
syncAllPlayerIps();
|
||||
syncServiceProviders();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 全量同步异常: " + e.getMessage());
|
||||
} finally {
|
||||
@@ -779,6 +785,163 @@ public class WebManager {
|
||||
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5); // 每5秒检查一次
|
||||
}
|
||||
|
||||
// ==================== 库存高频轮询 ====================
|
||||
|
||||
/** 上次检测到的库存修改时间戳 */
|
||||
private volatile long lastKnownShopStockModified = 0;
|
||||
/** 连续失败计数 */
|
||||
private volatile int shopStockPollFailCount = 0;
|
||||
private static final int SHOP_STOCK_POLL_FAIL_THRESHOLD = 10;
|
||||
/** 防重入锁:pullShopStock执行期间不重复拉取 */
|
||||
private volatile boolean shopStockPulling = false;
|
||||
|
||||
/**
|
||||
* 库存高频轮询:每5秒请求Web端库存变更检测接口
|
||||
* 无改动跳过,有改动立即拉取完整库存并更新游戏
|
||||
* 60~90秒的全量同步定时器作为兜底
|
||||
*/
|
||||
private void startShopStockFastPolling() {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// ★ 防重入:正在拉取中则跳过
|
||||
if (shopStockPulling) return;
|
||||
|
||||
// ★ 连续失败过多时跳过(等兜底定时器)
|
||||
if (shopStockPollFailCount >= SHOP_STOCK_POLL_FAIL_THRESHOLD) {
|
||||
if (shopStockPollFailCount % 12 != 0) {
|
||||
shopStockPollFailCount++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=check_shop_stock_changed&secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&last_modified=" + lastKnownShopStockModified;
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
if (code != 200) {
|
||||
shopStockPollFailCount++;
|
||||
conn.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) sb.append(line);
|
||||
reader.close();
|
||||
conn.disconnect();
|
||||
|
||||
String resp = sb.toString();
|
||||
shopStockPollFailCount = 0;
|
||||
|
||||
// 解析响应
|
||||
boolean changed = resp.contains("\"changed\":true") || resp.contains("\"changed\": true");
|
||||
|
||||
if (changed) {
|
||||
// ★ 从响应中提取服务端last_modified(先提取,后面决定是否采纳)
|
||||
long serverLastModified = 0;
|
||||
int lmIdx = resp.indexOf("\"last_modified\":");
|
||||
if (lmIdx >= 0) {
|
||||
lmIdx += 16;
|
||||
int lmEnd = resp.indexOf(",", lmIdx);
|
||||
if (lmEnd < 0) lmEnd = resp.indexOf("}", lmIdx);
|
||||
if (lmEnd > lmIdx) {
|
||||
try {
|
||||
serverLastModified = Long.parseLong(
|
||||
resp.substring(lmIdx, lmEnd).trim());
|
||||
} catch (NumberFormatException e) { /* 忽略 */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 拉取并应用库存,只在成功更新后才推进时间戳
|
||||
shopStockPulling = true;
|
||||
try {
|
||||
boolean applied = pullShopStockSync();
|
||||
if (applied && serverLastModified > 0) {
|
||||
lastKnownShopStockModified = serverLastModified;
|
||||
}
|
||||
// applied=false → 不更新lastKnownShopStockModified → 下次重试
|
||||
} finally {
|
||||
shopStockPulling = false;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
shopStockPollFailCount++;
|
||||
}
|
||||
}
|
||||
}.runTaskTimerAsynchronously(plugin, 20L * 5, 20L * 5);
|
||||
plugin.getLogger().info("[Web通信] 库存高频轮询已启动(每5秒)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步拉取库存(阻塞调用,用于高频轮询器内避免嵌套异步)
|
||||
* 逻辑与pullShopStock相同,但不在BukkitRunnable内执行
|
||||
* @return 是否成功应用了库存更新
|
||||
*/
|
||||
private boolean pullShopStockSync() {
|
||||
try {
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=pull_shop_stock&secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
if (code != 200) {
|
||||
plugin.getLogger().warning("[库存高频] pull_shop_stock HTTP " + code);
|
||||
return false;
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) sb.append(line);
|
||||
reader.close();
|
||||
|
||||
String json = sb.toString();
|
||||
if (!json.contains("\"success\":true")) {
|
||||
plugin.getLogger().warning("[库存高频] pull_shop_stock 返回失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
int dataStart = json.indexOf("\"items\":");
|
||||
if (dataStart < 0) {
|
||||
plugin.getLogger().warning("[库存高频] pull_shop_stock 无items字段");
|
||||
return false;
|
||||
}
|
||||
String dataStr = json.substring(dataStart + 8);
|
||||
int arrEnd = findMatchingBracket(dataStr, 0);
|
||||
if (arrEnd < 0) {
|
||||
plugin.getLogger().warning("[库存高频] pull_shop_stock JSON解析失败");
|
||||
return false;
|
||||
}
|
||||
String arrStr = dataStr.substring(0, arrEnd + 1);
|
||||
|
||||
boolean applied = updateLocalShopStock(arrStr);
|
||||
// ★ PHP端pullShopStock已自动清除admin_stock,无需再调clearAdminStock()
|
||||
return applied;
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[库存高频] 拉取库存异常: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CDK验证轮询 ====================
|
||||
|
||||
/**
|
||||
@@ -1557,6 +1720,46 @@ public class WebManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 服务商列表同步到Web ====================
|
||||
public void syncServiceProviders() {
|
||||
try {
|
||||
String token = generateAndSyncToken("system", "sync");
|
||||
List<Map<String, Object>> providers = new ArrayList<>();
|
||||
for (Map<String, Object> sp : plugin.getDb().getAllServiceProviders()) {
|
||||
Map<String, Object> p = new LinkedHashMap<>();
|
||||
p.put("player_name", sp.get("player_name"));
|
||||
p.put("role", sp.get("role"));
|
||||
p.put("active", sp.get("active"));
|
||||
p.put("join_time", sp.get("join_time"));
|
||||
providers.add(p);
|
||||
}
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
plugin.getLogger().info("[Web通信] 无服务商,跳过同步");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("data", providers);
|
||||
|
||||
String response = httpPostWithToken("api/sync.php?action=sync_service_providers", token, body);
|
||||
|
||||
if (response != null) {
|
||||
Map<String, Object> result = parseJson(response);
|
||||
Boolean success = (Boolean) result.get("success");
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
plugin.getLogger().info("[Web通信] 服务商同步成功: " + providers.size() + "人");
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web通信] 服务商同步失败: " + result.get("message"));
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web通信] 服务商同步请求失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 服务商同步异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析md商品文件
|
||||
* 格式: | ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
|
||||
@@ -2466,13 +2669,20 @@ public class WebManager {
|
||||
boolean isRegistered = plugin.getDb().userExists(playerName);
|
||||
boolean syncSuccess = false;
|
||||
try {
|
||||
// ★ 获取玩家真实IP地址
|
||||
String playerIp = "";
|
||||
java.net.InetSocketAddress addr = sender.getAddress();
|
||||
if (addr != null && addr.getAddress() != null) {
|
||||
playerIp = addr.getAddress().getHostAddress();
|
||||
}
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&web_token=" + java.net.URLEncoder.encode(token, "UTF-8")
|
||||
+ "&expire_seconds=" + tokenExpireSeconds
|
||||
+ "&online=1"
|
||||
+ "®istered=" + (isRegistered ? "1" : "0");
|
||||
+ "®istered=" + (isRegistered ? "1" : "0")
|
||||
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8");
|
||||
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
@@ -2545,13 +2755,20 @@ public class WebManager {
|
||||
boolean syncSuccess = false;
|
||||
try {
|
||||
// 用GET请求,所有参数通过URL传递
|
||||
// ★ 获取玩家真实IP地址
|
||||
String playerIp = "";
|
||||
java.net.InetSocketAddress addr = player.getAddress();
|
||||
if (addr != null && addr.getAddress() != null) {
|
||||
playerIp = addr.getAddress().getHostAddress();
|
||||
}
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=push_player_login_status"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&web_token=" + java.net.URLEncoder.encode(token, "UTF-8")
|
||||
+ "&expire_seconds=" + tokenExpireSeconds
|
||||
+ "&online=1"
|
||||
+ "®istered=" + (isRegistered ? "1" : "0");
|
||||
+ "®istered=" + (isRegistered ? "1" : "0")
|
||||
+ "&ip=" + java.net.URLEncoder.encode(playerIp, "UTF-8");
|
||||
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
@@ -2639,6 +2856,7 @@ public class WebManager {
|
||||
syncBondBalances();
|
||||
syncBondTransactions();
|
||||
syncUserRegistrations();
|
||||
syncServiceProviders();
|
||||
}
|
||||
}.runTaskAsynchronously(plugin);
|
||||
sender.sendMessage("§a[Web] 同步任务已启动...");
|
||||
@@ -3592,8 +3810,13 @@ public class WebManager {
|
||||
if (arrEnd < 0) return;
|
||||
String arrStr = dataStr.substring(0, arrEnd + 1);
|
||||
|
||||
// 更新本地商品库存
|
||||
updateLocalShopStock(arrStr);
|
||||
// ★ 更新本地商品库存,返回是否有改动
|
||||
boolean changed = updateLocalShopStock(arrStr);
|
||||
|
||||
// ★ 如果有改动,清除PHP端的admin_stock标记(防止重复同步)
|
||||
if (changed) {
|
||||
clearAdminStock();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 静默处理
|
||||
@@ -3603,20 +3826,42 @@ public class WebManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新本地商品库存
|
||||
* 清除PHP端的管理员库存标记
|
||||
*/
|
||||
private void updateLocalShopStock(String itemsJson) {
|
||||
private void clearAdminStock() {
|
||||
try {
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=clear_admin_stock&secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "Sdf1-WebManager/1.0");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
conn.getResponseCode(); // 执行即可
|
||||
conn.disconnect();
|
||||
} catch (Exception e) {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新本地商品库存
|
||||
* @return 是否有任何库存被更新
|
||||
*/
|
||||
private boolean updateLocalShopStock(String itemsJson) {
|
||||
try {
|
||||
File shopDir = new File(plugin.getDataFolder(), "shop");
|
||||
if (!shopDir.exists()) return;
|
||||
if (!shopDir.exists()) return false;
|
||||
|
||||
File[] mdFiles = shopDir.listFiles((d, n) -> n.endsWith(".md"));
|
||||
if (mdFiles == null) return;
|
||||
if (mdFiles == null) return false;
|
||||
|
||||
// 解析Web端返回的库存数据
|
||||
// 格式: [{"id":"ITEM_ID","stock":100,"last_sync":1234567890}, ...]
|
||||
// 解析JSON提取id和stock
|
||||
// 格式: [{"id":"ITEM_ID","stock":100,"last_sync":...,"admin_stock_override":true}, ...]
|
||||
Map<String, Integer> webStock = new HashMap<>();
|
||||
// ★ 跟踪哪些商品是管理员手动修改的(admin_stock_override=true)
|
||||
Set<String> adminOverride = new HashSet<>();
|
||||
int idx = 0;
|
||||
while (true) {
|
||||
int idStart = itemsJson.indexOf("\"id\":\"", idx);
|
||||
@@ -3633,7 +3878,6 @@ public class WebManager {
|
||||
if (stockEnd < 0) stockEnd = itemsJson.indexOf("}", stockStart);
|
||||
if (stockEnd < 0) break;
|
||||
String stockStr = itemsJson.substring(stockStart, stockEnd).trim();
|
||||
// 去除可能的引号
|
||||
stockStr = stockStr.replace("\"", "");
|
||||
|
||||
try {
|
||||
@@ -3643,10 +3887,16 @@ public class WebManager {
|
||||
// 忽略无效值
|
||||
}
|
||||
|
||||
// ★ 检查admin_stock_override标记
|
||||
int overrideCheck = itemsJson.indexOf("\"admin_stock_override\":", idEnd);
|
||||
if (overrideCheck > idEnd && overrideCheck < stockEnd + 20) {
|
||||
adminOverride.add(itemId);
|
||||
}
|
||||
|
||||
idx = stockEnd + 1;
|
||||
}
|
||||
|
||||
if (webStock.isEmpty()) return;
|
||||
if (webStock.isEmpty()) return false;
|
||||
|
||||
// 更新每个md文件中的库存
|
||||
int updatedCount = 0;
|
||||
@@ -3671,17 +3921,21 @@ public class WebManager {
|
||||
|
||||
int newStock = webStock.get(itemId);
|
||||
String oldStock = cols[6].trim();
|
||||
// 始终用数字写入md文件,不写"无限"字符串(防止parseTableRow的Integer.parseInt失败)
|
||||
String newStockStr = String.valueOf(newStock);
|
||||
// 兼容旧文件中的"无限"文本:转为-1用于比较
|
||||
int oldStockNum;
|
||||
try {
|
||||
oldStockNum = Integer.parseInt(oldStock);
|
||||
} catch (NumberFormatException e) {
|
||||
oldStockNum = ("无限".equals(oldStock) || "∞".equals(oldStock)) ? -1 : 0;
|
||||
}
|
||||
// 不要把Web端的0库存覆盖到游戏的无限库存(-1)
|
||||
if (newStock == 0 && oldStockNum == -1) continue;
|
||||
|
||||
// ★ 管理员手动修改的库存:跳过保护逻辑,直接应用
|
||||
// 管理员设stock=0表示"售罄",需要同步到游戏
|
||||
if (!adminOverride.contains(itemId)) {
|
||||
// stock<-1 为未定义值(-2以下),不生效,跳过同步
|
||||
// -1=无限库存, 0=售罄, >=1=有库存,都正常同步
|
||||
if (newStock < -1) continue;
|
||||
}
|
||||
|
||||
if (oldStockNum != newStock) {
|
||||
cols[6] = " " + newStockStr + " ";
|
||||
@@ -3711,9 +3965,12 @@ public class WebManager {
|
||||
plugin.getLogger().info("[Web通信] 已同步Web端商品库存,更新了 " + updatedCount + " 个商品");
|
||||
lastPullShopStockLog = now;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 更新商品库存失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3724,20 +3981,38 @@ public class WebManager {
|
||||
try {
|
||||
File notifyFile = new File(plugin.getDataFolder(), "../sync_notify.txt");
|
||||
if (notifyFile.exists()) {
|
||||
long notifyTime = Long.parseLong(new String(java.nio.file.Files.readAllBytes(notifyFile.toPath())).trim());
|
||||
String content = new String(java.nio.file.Files.readAllBytes(notifyFile.toPath())).trim();
|
||||
// ★ 兼容两种格式:纯数字时间戳 或 JSON {"time":xxx,"tx_id":xxx}
|
||||
long notifyTime = 0;
|
||||
if (content.startsWith("{")) {
|
||||
// JSON格式
|
||||
int timeIdx = content.indexOf("\"time\":");
|
||||
if (timeIdx > 0) {
|
||||
int start = timeIdx + 8;
|
||||
int end = content.indexOf(",", start);
|
||||
if (end < 0) end = content.indexOf("}", start);
|
||||
if (end > start) {
|
||||
notifyTime = Long.parseLong(content.substring(start, end).trim());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notifyTime = Long.parseLong(content);
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 如果通知时间在5分钟内,执行同步
|
||||
if (now - notifyTime < 300000) {
|
||||
if (notifyTime > 0 && now - notifyTime < 300000) {
|
||||
// 有人在线才响应通知
|
||||
if (!Bukkit.getOnlinePlayers().isEmpty()) {
|
||||
syncShopData();
|
||||
syncBondBalances();
|
||||
syncBondTransactions();
|
||||
pushWebLoginCredentials();
|
||||
// ★ 先拉取(PHP→Java),再推送(Java→PHP),确保不覆盖管理员修改
|
||||
pullPendingTransactions();
|
||||
pullShopStock();
|
||||
pullBondChanges();
|
||||
syncOnlinePlayers();
|
||||
pushWebLoginCredentials();
|
||||
syncUserRegistrations();
|
||||
syncServiceProviders();
|
||||
syncShopData();
|
||||
// 删除通知文件
|
||||
notifyFile.delete();
|
||||
}
|
||||
@@ -4029,6 +4304,12 @@ public class WebManager {
|
||||
plugin.getLogger().info("[Web注册] 游戏账号创建成功: " + fName);
|
||||
}
|
||||
|
||||
// ★ 写入注册IP到login.db
|
||||
if (!fIp.isEmpty()) {
|
||||
dbMgr.setField(fName, "ip_address", fIp);
|
||||
dbMgr.setField(fName, "register_ip", fIp);
|
||||
}
|
||||
|
||||
// ★ 如果玩家当前在线,自动登录
|
||||
Player onlinePlayer = Bukkit.getPlayer(fName);
|
||||
if (onlinePlayer != null && onlinePlayer.isOnline()) {
|
||||
@@ -4155,6 +4436,12 @@ public class WebManager {
|
||||
dbMgr.createUser(fName, fHash, fSalt);
|
||||
plugin.getLogger().info("[Web注册] 补全成功: " + fName);
|
||||
|
||||
// ★ 写入注册IP到login.db
|
||||
if (!fIp.isEmpty()) {
|
||||
dbMgr.setField(fName, "ip_address", fIp);
|
||||
dbMgr.setField(fName, "register_ip", fIp);
|
||||
}
|
||||
|
||||
Player onlinePlayer = Bukkit.getPlayer(fName);
|
||||
if (onlinePlayer != null && onlinePlayer.isOnline()) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
@@ -4303,6 +4590,7 @@ public class WebManager {
|
||||
int points = Integer.parseInt(extractJsonNumber(dataJson, "points") != null ? extractJsonNumber(dataJson, "points") : "0");
|
||||
int giftStage = Integer.parseInt(extractJsonNumber(dataJson, "gift_stage") != null ? extractJsonNumber(dataJson, "gift_stage") : "0");
|
||||
int totalOnlineTime = Integer.parseInt(extractJsonNumber(dataJson, "total_online_time") != null ? extractJsonNumber(dataJson, "total_online_time") : "0");
|
||||
String ipAddress = extractJsonString(dataJson, "ip_address");
|
||||
|
||||
plugin.getLogger().info("[Web注册] 玩家在PHP后端已注册: " + playerName + ", 有密码凭证:" + !extractJsonString(dataJson, "password_hash").isEmpty());
|
||||
|
||||
@@ -4310,6 +4598,7 @@ public class WebManager {
|
||||
final String fEmail = email != null ? email : "";
|
||||
final String fHash = passwordHash != null ? passwordHash : "";
|
||||
final String fSalt = salt != null ? salt : "";
|
||||
final String fIp = ipAddress != null ? ipAddress : "";
|
||||
final long fRegisterTime = registerTime;
|
||||
final long fLastLoginTime = lastLoginTime;
|
||||
final int fPoints = points;
|
||||
@@ -4333,6 +4622,11 @@ public class WebManager {
|
||||
if (!fHash.isEmpty() && !fSalt.isEmpty()) {
|
||||
dbMgr.createUser(fName, fHash, fSalt);
|
||||
plugin.getLogger().info("[Web注册] 用户 " + fName + " 已从PHP后端同步注册数据到Java本地(含密码凭证)");
|
||||
// ★ 写入注册IP到login.db
|
||||
if (!fIp.isEmpty()) {
|
||||
dbMgr.setField(fName, "ip_address", fIp);
|
||||
dbMgr.setField(fName, "register_ip", fIp);
|
||||
}
|
||||
} else {
|
||||
// PHP后端没有密码凭证,可能是通过register.php的webRegister()直接注册的
|
||||
// 需要等待Java插件通过pollWebRegisterRequests()轮询后创建用户
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Sdf1_login
|
||||
version: 2.5
|
||||
version: 2.6
|
||||
main: Sdf1_login.Main
|
||||
api-version: '1.21'
|
||||
commands:
|
||||
|
||||
Reference in New Issue
Block a user