Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d40243fd2 | ||
|
|
8a54123d3a | ||
|
|
a306a00908 | ||
|
|
b901a72140 | ||
|
|
cc8727a2f9 | ||
|
|
57e9ba46a0 | ||
|
|
0c3478d1ff | ||
|
|
ba0b7fb879 | ||
|
|
3118f9bd1a | ||
|
|
f18e7485c7 | ||
|
|
8b147b2531 | ||
|
|
b1ea407f3f | ||
|
|
c9e740d4c9 | ||
|
|
a716424dd5 | ||
|
|
cb37eceb9b | ||
|
|
2618cc5d80 | ||
|
|
bca19bf3d0 | ||
|
|
7f01f75343 | ||
|
|
5edbcb57c3 | ||
|
|
d5cebbf6ad | ||
|
|
afa53876ad | ||
|
|
c2cf7ff220 | ||
|
|
91b03fd127 | ||
|
|
8da0ac68c8 | ||
|
|
99d83f1b07 | ||
|
|
5e10cbf420 | ||
|
|
729e1383c1 | ||
|
|
2cc8490016 | ||
|
|
41b50f7ee8 | ||
|
|
fc495802f2 | ||
|
|
abaf54470c | ||
|
|
53a7430184 | ||
|
|
56e53a1b07 | ||
|
|
7eb350a8da | ||
|
|
3d9475ccec | ||
|
|
4a28287dec | ||
|
|
7e1dbf26c8 | ||
|
|
e9724fd47e | ||
|
|
dcd1170851 | ||
|
|
40cafa4b74 | ||
|
|
3ece4c8369 | ||
|
|
106211564b | ||
|
|
c9a3d24e64 | ||
|
|
6f004f257f | ||
|
|
46897f06af | ||
|
|
fb871e886a | ||
|
|
925017e21a | ||
|
|
2f35b945fc | ||
|
|
addc5563c6 | ||
|
|
1e6d2419db | ||
|
|
17b7513276 | ||
|
|
698a6445ed | ||
|
|
36568cb31a | ||
|
|
5574749351 | ||
|
|
6a6685bcec | ||
|
|
18da80c799 | ||
|
|
6a7d3bd635 | ||
|
|
0bab938784 | ||
|
|
c437851470 | ||
|
|
265503be38 | ||
|
|
c8e8fe88e4 | ||
|
|
4f99b6af05 | ||
|
|
6b78165019 | ||
|
|
24aa623f7a | ||
|
|
7c42f60709 | ||
|
|
45dbc01eae | ||
|
|
3d3c830a6e | ||
|
|
77c49a1fa5 | ||
|
|
2cd56d637b | ||
|
|
90365bb3eb | ||
|
|
2706c0aa2a | ||
|
|
141a29a39c | ||
|
|
60ce93617b | ||
|
|
c303849335 | ||
|
|
c41ef2eacc | ||
|
|
ac8b23faff | ||
|
|
bbbbf54b3f | ||
|
|
fe4bee53de | ||
|
|
fa9be74e1f | ||
|
|
c94fb60a86 | ||
|
|
0223ece81d | ||
|
|
0949ae18b0 | ||
|
|
a33757a465 | ||
|
|
f3362380a3 | ||
|
|
47b21f31fc | ||
|
|
3555f26583 | ||
|
|
4fa9808ddc | ||
|
|
82242ed428 | ||
|
|
8aa20c4d39 | ||
|
|
ccf5a4e12d | ||
|
|
e02a05a1b6 | ||
|
|
5530d7e862 | ||
|
|
f7d13eb44c | ||
|
|
5eb0000029 | ||
|
|
9befb46afa | ||
|
|
b21835d00a | ||
|
|
e9eea049d9 | ||
|
|
2dfdad3a81 | ||
|
|
e23b2df365 | ||
|
|
4105501ec6 | ||
|
|
b26f979924 | ||
|
|
06f8bd68cd | ||
|
|
6d0c3a820a | ||
|
|
ae018f57e5 | ||
|
|
8ef4744cbd | ||
|
|
282930bfe6 | ||
|
|
09a4b1d869 | ||
|
|
a7ff29cdc3 | ||
|
|
6c738d484f | ||
|
|
a8005e6666 | ||
|
|
8c06b48989 | ||
|
|
d0c4f45928 | ||
|
|
7efa78dbf9 | ||
|
|
179f3a8355 | ||
|
|
f72875f0f7 | ||
|
|
f6d5540b16 | ||
|
|
3c3e902a6a | ||
|
|
b83202c08e | ||
|
|
ce0052cdfb | ||
|
|
ae2ae77a02 | ||
|
|
e8c83bba98 | ||
|
|
642e587cdd | ||
|
|
ffd11a0b9c | ||
|
|
33afe26b46 | ||
|
|
30e7f68c85 | ||
|
|
a06f7a669b | ||
|
|
f333e59621 | ||
|
|
80c8715264 | ||
|
|
f67efd0f8d | ||
|
|
3a3f4f1cd2 | ||
|
|
11e1d27013 | ||
|
|
3402a56d1c | ||
|
|
f2866ada9e | ||
|
|
48e1692822 | ||
|
|
44a9edca1b | ||
|
|
ca54dd61ba | ||
|
|
47e1ad3be5 | ||
|
|
46b483848c | ||
|
|
40edd85ea7 | ||
|
|
59313d4f32 | ||
|
|
c86e9f76b9 | ||
|
|
142b15973a | ||
|
|
3e21bbb112 | ||
|
|
df6cc2bea8 | ||
|
|
d23ce56606 | ||
|
|
eb3c336e38 | ||
|
|
f1cd0da7d4 | ||
|
|
329bca61a9 | ||
|
|
b6863c45c7 | ||
|
|
2867c7614a | ||
|
|
23337695b4 | ||
|
|
15f473abc8 | ||
|
|
d47997294d | ||
|
|
6d569fed23 | ||
|
|
6e24546a6c | ||
|
|
eb0b39d8b9 | ||
|
|
10e148be5f | ||
|
|
9599a66cc5 | ||
|
|
c05c6d9512 | ||
|
|
2a976710e9 | ||
|
|
c2a8385895 | ||
|
|
3c71664284 | ||
|
|
c9e2f71361 | ||
|
|
3c085e086d | ||
|
|
28a120fea0 | ||
|
|
dee6ab8eed | ||
|
|
5273220716 | ||
|
|
e416372a41 | ||
|
|
bbf23aad31 | ||
|
|
7ae4f46f75 | ||
|
|
4a74a0a77f | ||
|
|
9944fd5f16 | ||
|
|
3662466434 | ||
|
|
302268d0da | ||
|
|
6ec438abd9 | ||
|
|
9e3182e1a1 | ||
|
|
8cd380a93b | ||
|
|
a3030c3b80 | ||
|
|
2a5db378d3 | ||
|
|
05ebd07df6 | ||
|
|
542879177c | ||
|
|
69c9e0ceb9 | ||
|
|
58cc9677ae | ||
|
|
c691e9c44f | ||
|
|
70b8e991da | ||
|
|
ab2c4da6de | ||
|
|
9f185df6d9 | ||
|
|
ce13396c22 | ||
|
|
439da63793 | ||
|
|
92552f6007 | ||
|
|
152ebc2dc6 | ||
|
|
1550e3a461 | ||
|
|
575e54a27a | ||
|
|
d9e3b74fd9 | ||
|
|
fa9c5cf601 | ||
|
|
cd2d94c9d0 | ||
|
|
de3e1c8e83 | ||
|
|
34505ea2f5 | ||
|
|
b5d6519d11 | ||
|
|
73bee83051 | ||
|
|
7d8dc8f4c9 | ||
|
|
07791fa613 | ||
|
|
fed72b773f | ||
|
|
ebcb11338c | ||
|
|
8d4731a873 |
+13
@@ -45,6 +45,19 @@ txt/
|
|||||||
backup/
|
backup/
|
||||||
新建*.txt
|
新建*.txt
|
||||||
群戏归类.txt
|
群戏归类.txt
|
||||||
|
upload_sftp.py
|
||||||
|
|
||||||
|
# PHP Web 后端
|
||||||
|
web/Web.db
|
||||||
|
web/debug_log_tail.txt
|
||||||
|
web/upload_debug.ps1
|
||||||
|
web/api/*.bak*
|
||||||
|
# 敏感文件 - 禁止提交上传/调试脚本
|
||||||
|
web/upload_*.php
|
||||||
|
web/self_update.php
|
||||||
|
web/version_check.php
|
||||||
|
web/debug_*.php
|
||||||
|
web/db/debug_*.php
|
||||||
|
|
||||||
# Windows 保留设备名(无法用普通 rm 删除)
|
# Windows 保留设备名(无法用普通 rm 删除)
|
||||||
nul
|
nul
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
## 2026-07-04 13:33 - 新玩家验证码系统 + 全场景链接过滤 + 私信URL拦截
|
||||||
|
|
||||||
|
### 验证码系统实现
|
||||||
|
- ChatFilterManager 新增 VerificationResult 枚举(NEED_VERIFICATION/PENDING/VERIFIED/FAILED)
|
||||||
|
- 新玩家首次发消息时触发数学题验证码(10以内加减乘除,随机选择)
|
||||||
|
- 30秒有效期,答错/超时/非数字答案 → 吞噬消息
|
||||||
|
- 答对 → 插件代为广播缓存的原始消息(无前缀,像普通聊天一样)
|
||||||
|
- 老玩家(注册>1小时)快速验证后自动放行
|
||||||
|
- pendingMessages Map 缓存未验证消息,验证通过后广播
|
||||||
|
|
||||||
|
### 全场景链接过滤
|
||||||
|
- 告示牌(onSignChange): 已有的 filterLinkInContent 拦截
|
||||||
|
- 编辑书(onPlayerEditBook): 通过反射调用 getNewBook() 获取书页内容(兼容Paper 1.21.4)
|
||||||
|
- 私信(onPrivateMessage): 新增 PlayerCommandPreprocessEvent 拦截 /tell /msg /w /r 命令中的链接
|
||||||
|
|
||||||
|
### 编译部署
|
||||||
|
- Maven clean package 编译通过
|
||||||
|
- 推送到 git.ypshidifu.cn (main 分支)
|
||||||
|
- JAR 上传到 SFTP 服务器
|
||||||
|
- PHP 文件上传到 FTP /plugin/ 目录
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[01:19] Web端交易三个核心问题修复
|
|
||||||
==================================================
|
|
||||||
|
|
||||||
1. 商品ID无效(ACACIA_BOAT)- 已修复
|
|
||||||
- PHP shop.php 传 item_id="ACACIA_BOAT"(Material名称)
|
|
||||||
- Java dispatchItemByMaterialOrId() 已支持Material匹配+ID fallback
|
|
||||||
- displayName获取已修复:优先从ItemMeta.getDisplayName()读取,而不是直接用itemId
|
|
||||||
|
|
||||||
2. 扣款变给钱(bond_log符号反了)- 已修复
|
|
||||||
- BondManager.deductBondsInternal(): logLegacy(player, -amount) → logLegacy(player, amount)
|
|
||||||
- bond_log表的amount字段现在记录正数,UI根据type判断加减
|
|
||||||
|
|
||||||
3. 交易轮询慢(30-90秒)- 已修复
|
|
||||||
- PHP shop.php 和 cdk.php 在购买/兑换成功后立即POST到Java notify_sync回调
|
|
||||||
- Java收到notify_sync后调用requestImmediateTransactionPull()
|
|
||||||
- 最小间隔5秒,避免过度频繁
|
|
||||||
|
|
||||||
4. 回调服务器并发问题 - 已修复
|
|
||||||
- handleCallback()分离notify_sync和register_callback两种请求
|
|
||||||
- notify_sync不阻塞注册回调处理
|
|
||||||
|
|
||||||
5. detail JSON解析 - 已修复
|
|
||||||
- 支持转义引号\"的嵌套JSON解析
|
|
||||||
|
|
||||||
==================================================
|
|
||||||
下一步:
|
|
||||||
1. 用IDEA重新编译打包(Maven → Lifecycle → package)
|
|
||||||
2. 将Sdf1_login.jar复制到MC服务器plugins目录覆盖
|
|
||||||
3. 重启服务器测试
|
|
||||||
4. 验证:Web购物→立即到账、CDK兑换→余额同步、扣款方向正确
|
|
||||||
@@ -22,13 +22,14 @@
|
|||||||
|
|
||||||
| 前置插件 | 选装类型 | 功能作用 |
|
| 前置插件 | 选装类型 | 功能作用 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| [CY_beibao](https://github.com/ypsdf1/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
| [CY_beibao](https://git.ypshidifu.cn/youpaishidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 |
|
||||||
| EssentialsX | 可选 | 主流经济系统
|
| EssentialsX | 可选 | 主流经济系统 |
|
||||||
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
| [sdf1](https://git.ypshidifu.cn/youpaishidifu/Sdf1_plugin) | 可选,推荐 | CDK兑换系统 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 2.1 下载
|
### 2.1 下载
|
||||||
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
您可以从GitHub和Gitee、gitcode下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||||
|
|
||||||
### 2.2. 配置文件
|
### 2.2. 配置文件
|
||||||
|
|
||||||
@@ -505,6 +506,47 @@ http-port=443
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## php端说明:
|
||||||
|
1. 默认密码ypshidifu2026<br>
|
||||||
|
2. 工单系统全面支持markdown语法:
|
||||||
|
```md
|
||||||
|
# 一级标题
|
||||||
|
## 二级标题
|
||||||
|
### 三级标题
|
||||||
|
#### 四级标题
|
||||||
|
##### 五级标题
|
||||||
|
###### 六级标题
|
||||||
|
|
||||||
|
表格语法
|
||||||
|
|
||||||
|
| 表格列1 | 表格列2 |
|
||||||
|
| --- | --- |
|
||||||
|
| 表格1 | 表格2 |
|
||||||
|
|
||||||
|
>单嵌套语法
|
||||||
|
|
||||||
|
>嵌套块
|
||||||
|
>
|
||||||
|
>>语法
|
||||||
|
|
||||||
|
 图片语法
|
||||||
|
[]() 超链接语法
|
||||||
|
<br>换行语法
|
||||||
|
代码块语法
|
||||||
|
```
|
||||||
|
## 工单状态:
|
||||||
|
|
||||||
|
| 状态名 | 是否可回复 | 备注说明 |
|
||||||
|
| :---: | :---: | :---: |
|
||||||
|
| 已提交 | 可回复 | 初始化状态 |
|
||||||
|
| 已回复 | 可回复 | 玩家自己回复了/管理员/服务商回复了 |
|
||||||
|
| 已完结 | 不可回复 | 管理员驳回了工单请求 |
|
||||||
|
| 已撤销 | 不可回复 | 用户撤销了工单请求 |
|
||||||
|
|
||||||
|
## 服务商说明
|
||||||
|
- 若您需要授权某个玩家为服务商,请登录游戏,打开工单系统-管理面板-添加服务商,然后输入他的名字即可邀请服务商
|
||||||
|
|
||||||
|
|
||||||
## 运行截图
|
## 运行截图
|
||||||
|
|
||||||
主界面
|
主界面
|
||||||
@@ -563,8 +605,8 @@ http-port=443
|
|||||||
商城退单
|
商城退单
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 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)
|
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||||
|
|||||||
@@ -34,12 +34,6 @@
|
|||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.MilkBowl</groupId>
|
|
||||||
<artifactId>VaultAPI</artifactId>
|
|
||||||
<version>1.7.1</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xerial</groupId>
|
<groupId>org.xerial</groupId>
|
||||||
<artifactId>sqlite-jdbc</artifactId>
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ 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 {
|
||||||
|
|
||||||
@@ -14,20 +13,20 @@ public class BondManager {
|
|||||||
|
|
||||||
// ===== 交易监听器 =====
|
// ===== 交易监听器 =====
|
||||||
public interface TransactionListener {
|
public interface TransactionListener {
|
||||||
void onTransactionLogged(String playerName, String type, int amount);
|
void onTransaction(String playerName, String type, int amount);
|
||||||
}
|
}
|
||||||
private final List<TransactionListener> transactionListeners = new CopyOnWriteArrayList<>();
|
private final List<TransactionListener> transactionListeners = new ArrayList<>();
|
||||||
|
|
||||||
public void addTransactionListener(TransactionListener listener) {
|
public void addTransactionListener(TransactionListener listener) {
|
||||||
transactionListeners.add(listener);
|
transactionListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyTransactionListeners(String playerName, String type, int amount) {
|
private void fireTransactionEvent(String playerName, String type, int amount) {
|
||||||
for (TransactionListener listener : transactionListeners) {
|
for (TransactionListener listener : transactionListeners) {
|
||||||
try {
|
try {
|
||||||
listener.onTransactionLogged(playerName, type, amount);
|
listener.onTransaction(playerName, type, amount);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().warning("[BondManager] 交易监听器异常: " + e.getMessage());
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,11 +381,11 @@ public class BondManager {
|
|||||||
ps.setLong(9, System.currentTimeMillis());
|
ps.setLong(9, System.currentTimeMillis());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
// 触发交易监听器
|
||||||
|
fireTransactionEvent(player, type, amount);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
// ★ 通知监听器:交易已记录
|
|
||||||
notifyTransactionListeners(player, type, amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -721,40 +720,87 @@ public class BondManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 同步用:获取指定时间之后的交易 ========================
|
// ===== 获取玩家最近1小时出售总额 =====
|
||||||
|
public int getTodaySellTotal(String player) {
|
||||||
|
return getSellTotalInWindow(player, System.currentTimeMillis() - 3600000L);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private int getSellTotalInWindow(String player, long since) {
|
||||||
* 获取指定时间之后的所有交易记录(用于推送到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 {
|
try {
|
||||||
PreparedStatement ps = db.prepareStatement(
|
PreparedStatement ps = db.prepareStatement(
|
||||||
"SELECT * FROM bond_transaction "
|
"SELECT COALESCE(SUM(amount), 0) FROM bond_transaction "
|
||||||
+ "WHERE time > ? "
|
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||||
+ "ORDER BY time ASC "
|
ps.setString(1, player);
|
||||||
+ "LIMIT 200");
|
ps.setLong(2, since);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
int total = 0;
|
||||||
|
if (rs.next()) {
|
||||||
|
total = rs.getInt(1);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return total;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家最近1小时出售窗口中最早交易的时间戳。
|
||||||
|
* 返回0表示窗口内无交易。
|
||||||
|
*/
|
||||||
|
public long getEarliestSellTimeInWindow(String player) {
|
||||||
|
long since = System.currentTimeMillis() - 3600000L;
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT MIN(time) FROM bond_transaction "
|
||||||
|
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setLong(2, since);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
long earliest = 0;
|
||||||
|
if (rs.next()) {
|
||||||
|
earliest = rs.getLong(1);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return earliest;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定时间之后的所有交易记录(用于Web同步)
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
|
||||||
|
List<Map<String, Object>> transactions = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
|
||||||
|
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
|
||||||
ps.setLong(1, afterTime);
|
ps.setLong(1, afterTime);
|
||||||
ResultSet rs = ps.executeQuery();
|
ResultSet rs = ps.executeQuery();
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
Map<String, Object> row = new LinkedHashMap<>();
|
Map<String, Object> tx = new LinkedHashMap<>();
|
||||||
row.put("id", rs.getInt("id"));
|
tx.put("player_name", rs.getString("player_name"));
|
||||||
row.put("player_name", rs.getString("player_name"));
|
tx.put("type", rs.getString("type"));
|
||||||
row.put("type", rs.getString("type"));
|
tx.put("amount", rs.getInt("amount"));
|
||||||
row.put("amount", rs.getInt("amount"));
|
tx.put("target_player", rs.getString("target_player"));
|
||||||
row.put("target_player", rs.getString("target_player"));
|
tx.put("operator", rs.getString("operator"));
|
||||||
row.put("operator", rs.getString("operator"));
|
tx.put("reason", rs.getString("reason"));
|
||||||
row.put("reason", rs.getString("reason"));
|
tx.put("balance_before", rs.getInt("balance_before"));
|
||||||
row.put("balance_before", rs.getInt("balance_before"));
|
tx.put("balance_after", rs.getInt("balance_after"));
|
||||||
row.put("balance_after", rs.getInt("balance_after"));
|
tx.put("time", rs.getLong("time"));
|
||||||
row.put("time", rs.getLong("time"));
|
transactions.add(tx);
|
||||||
list.add(row);
|
|
||||||
}
|
}
|
||||||
rs.close(); ps.close();
|
rs.close();
|
||||||
} catch (SQLException e) { e.printStackTrace(); }
|
ps.close();
|
||||||
return list;
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return transactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,64 +170,29 @@ public class CDKManager {
|
|||||||
if ("points".equals(input.type)) {
|
if ("points".equals(input.type)) {
|
||||||
handlePointsInput(p, msg.trim(),
|
handlePointsInput(p, msg.trim(),
|
||||||
input.context);
|
input.context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("gift_target".equals(input.type)) {
|
||||||
|
// 赠予用户组:输入目标玩家名
|
||||||
|
String targetName = msg.trim();
|
||||||
|
if (!targetName.isEmpty()) {
|
||||||
|
GUIManager guiMgr = plugin.getGui();
|
||||||
|
if (guiMgr != null) {
|
||||||
|
guiMgr.handleGiftTargetInput(p, targetName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§c请输入有效的玩家名");
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleEconInput(Player admin,
|
private void handleEconInput(Player admin,
|
||||||
String msg,
|
String msg,
|
||||||
String target) {
|
String target) {
|
||||||
net.milkbowl.vault.economy.Economy econ
|
// Economy已移除,所有经济操作改用债券系统
|
||||||
= null;
|
admin.sendMessage("§c[经济操作] 功能已移除,请使用债券系统");
|
||||||
try {
|
|
||||||
var reg = plugin.getServer()
|
|
||||||
.getServicesManager()
|
|
||||||
.getRegistration(
|
|
||||||
net.milkbowl.vault
|
|
||||||
.economy
|
|
||||||
.Economy.class);
|
|
||||||
if (reg != null)
|
|
||||||
econ = reg.getProvider();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
if (econ == null) {
|
|
||||||
admin.sendMessage("§c经济系统不可用");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!msg.startsWith("+")
|
|
||||||
&& !msg.startsWith("-")) {
|
|
||||||
admin.sendMessage("§c格式: §a+100"
|
|
||||||
+ " §7或 §c-50");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String numStr =
|
|
||||||
msg.substring(1).trim();
|
|
||||||
double amount =
|
|
||||||
Double.parseDouble(numStr);
|
|
||||||
if (amount <= 0) {
|
|
||||||
admin.sendMessage("§c金额必须>0");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (msg.startsWith("-")) {
|
|
||||||
Player tp = plugin.getServer()
|
|
||||||
.getPlayerExact(target);
|
|
||||||
if (tp == null) {
|
|
||||||
admin.sendMessage("§c" + target
|
|
||||||
+ " 不在线");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!econ.has(tp, amount)) {
|
|
||||||
admin.sendMessage("§c余额不足");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
econ.withdrawPlayer(tp, amount);
|
|
||||||
admin.sendMessage("§a扣款成功");
|
|
||||||
} else {
|
|
||||||
econ.depositPlayer(target, amount);
|
|
||||||
admin.sendMessage("§a到账成功");
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
admin.sendMessage("§c格式错误");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBondInput(Player admin,
|
private void handleBondInput(Player admin,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package Sdf1_login;
|
package Sdf1_login;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@@ -34,11 +38,485 @@ public class ChatFilterManager {
|
|||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
private final Map<String, String> messages =
|
private final Map<String, String> messages =
|
||||||
new LinkedHashMap<>();
|
new LinkedHashMap<>();
|
||||||
|
// ★ 广告机检测:记录每个玩家发送链接的频率
|
||||||
|
private final Map<String, List<Long>> linkSendHistory =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
// ★ 挂机检测
|
||||||
|
private final Map<String, Long> playerLastActiveTime =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
// ★ 管理员邮件地址
|
||||||
|
private String adminEmail = "admin@example.com";
|
||||||
private boolean notifyAdmin = true;
|
private boolean notifyAdmin = true;
|
||||||
private boolean notifyAll = false;
|
private boolean notifyAll = false;
|
||||||
private int muteDuration = 300;
|
private int muteDuration = 300;
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
// ★ 非法域名后缀列表(热更新)
|
||||||
|
private final Set<String> illegalDomainSuffixes = new HashSet<>();
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ★ 新玩家验证码系统
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
|
||||||
|
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
/** 验证码数据存储(public供Main.java访问清理) */
|
||||||
|
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** 验证码类型对外公开 */
|
||||||
|
public enum VerificationType {
|
||||||
|
MATH, // 数学题
|
||||||
|
GUI // GUI物品选择
|
||||||
|
}
|
||||||
|
|
||||||
|
static class VerificationData {
|
||||||
|
long createTime;
|
||||||
|
int a, b, op; // 0=加法, 1=减法, 2=乘法
|
||||||
|
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
|
||||||
|
VerificationType type; // 验证码类型
|
||||||
|
boolean completed;
|
||||||
|
|
||||||
|
VerificationData(int a, int b, int op, VerificationType type) {
|
||||||
|
this.createTime = System.currentTimeMillis();
|
||||||
|
this.a = a; this.b = b; this.op = op;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() - createTime > 30000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码结果:通过/失败 */
|
||||||
|
public enum VerificationResult {
|
||||||
|
NEED_VERIFICATION, // 需要验证码
|
||||||
|
PENDING, // 验证码进行中
|
||||||
|
VERIFIED, // 已验证
|
||||||
|
FAILED // 验证失败(消息吞噬)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验证码答案缓存,供外部判断是否为答案 */
|
||||||
|
public boolean isPendingVerificationAnswer(String playerName, String msg) {
|
||||||
|
VerificationData vd = verificationData.get(playerName);
|
||||||
|
if (vd == null || vd.completed) return false;
|
||||||
|
// 尝试作为答案检查
|
||||||
|
VerificationResult result = checkMathAnswer(playerName, msg);
|
||||||
|
return result != VerificationResult.FAILED; // 通过或进行中都是有效的答案输入
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否需要验证码
|
||||||
|
*/
|
||||||
|
public VerificationResult checkNewPlayerVerification(Player p) {
|
||||||
|
String name = p.getName();
|
||||||
|
|
||||||
|
// ★ 永久已验证的玩家 → 直接放行
|
||||||
|
if (verifiedPlayers.contains(name)) {
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正在验证中(等待答案)
|
||||||
|
if (verificationData.containsKey(name)) {
|
||||||
|
VerificationData vd = verificationData.get(name);
|
||||||
|
if (vd.completed) {
|
||||||
|
verifiedPlayers.add(name);
|
||||||
|
verificationData.remove(name);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
// 验证码是否过期 — 过期后自动放行,不无限卡玩家
|
||||||
|
if (vd.isExpired()) {
|
||||||
|
verificationData.remove(name);
|
||||||
|
verifiedPlayers.add(name);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
return VerificationResult.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查玩家加入时间是否≤1小时
|
||||||
|
if (isNewPlayerUnder1Hour(p)) {
|
||||||
|
return VerificationResult.NEED_VERIFICATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 老玩家:直接永久验证通过
|
||||||
|
verifiedPlayers.add(name);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局共享的Random实例,避免频繁创建产生相同序列 */
|
||||||
|
private static final ThreadLocal<Random> sharedRandom = ThreadLocal.withInitial(Random::new);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机验证码:数学题或GUI随机50%概率
|
||||||
|
*/
|
||||||
|
public void generateMathVerification(String playerName) {
|
||||||
|
Random rand = sharedRandom.get();
|
||||||
|
// 50% 概率数学题,50% 概率GUI
|
||||||
|
if (rand.nextBoolean()) {
|
||||||
|
generateMathChallenge(playerName, rand);
|
||||||
|
} else {
|
||||||
|
generateGUIChallenge(playerName, rand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成数学题验证码 */
|
||||||
|
private void generateMathChallenge(String playerName, Random rand) {
|
||||||
|
int op = rand.nextInt(3); // 0=加, 1=减, 2=乘
|
||||||
|
int a, b;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 0: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
|
||||||
|
case 1: a = rand.nextInt(10) + 2; b = rand.nextInt(a) + 1; break;
|
||||||
|
default: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerificationData vd = new VerificationData(a, b, op, VerificationType.MATH);
|
||||||
|
verificationData.put(playerName, vd);
|
||||||
|
|
||||||
|
String opSymbol = switch (op) {
|
||||||
|
case 0 -> "+";
|
||||||
|
case 1 -> "-";
|
||||||
|
default -> "×";
|
||||||
|
};
|
||||||
|
|
||||||
|
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Player p = org.bukkit.Bukkit.getPlayer(playerName);
|
||||||
|
if (p != null && p.isOnline()) {
|
||||||
|
p.sendMessage("§e§l[验证码] §f请回答: §e" + a + " " + opSymbol + " " + b + " = ?");
|
||||||
|
p.sendMessage("§7(30秒内输入答案)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中文物品名映射表 */
|
||||||
|
private static final Map<String, String> MATERIAL_CN = new HashMap<>();
|
||||||
|
static {
|
||||||
|
MATERIAL_CN.put("EMERALD", "绿宝石");
|
||||||
|
MATERIAL_CN.put("DIAMOND", "钻石");
|
||||||
|
MATERIAL_CN.put("IRON_INGOT", "铁锭");
|
||||||
|
MATERIAL_CN.put("GOLD_INGOT", "金锭");
|
||||||
|
MATERIAL_CN.put("STONE", "石头");
|
||||||
|
MATERIAL_CN.put("COBBLESTONE", "圆石");
|
||||||
|
MATERIAL_CN.put("SAND", "沙子");
|
||||||
|
MATERIAL_CN.put("GRAVEL", "砂砾");
|
||||||
|
MATERIAL_CN.put("REDSTONE", "红石粉");
|
||||||
|
MATERIAL_CN.put("LAPIS_LAZULI", "青金石");
|
||||||
|
MATERIAL_CN.put("QUARTZ", "石英");
|
||||||
|
MATERIAL_CN.put("APPLE", "苹果");
|
||||||
|
MATERIAL_CN.put("BOWL", "碗");
|
||||||
|
MATERIAL_CN.put("CAKE", "蛋糕");
|
||||||
|
MATERIAL_CN.put("BOOK", "书");
|
||||||
|
MATERIAL_CN.put("FLINT", "燧石");
|
||||||
|
MATERIAL_CN.put("FLINT_AND_STEEL", "打火石");
|
||||||
|
MATERIAL_CN.put("LEATHER_HELMET", "皮革头盔");
|
||||||
|
MATERIAL_CN.put("LEATHER_CHESTPLATE", "皮革上衣");
|
||||||
|
MATERIAL_CN.put("LEATHER_LEGGINGS", "皮革护腿");
|
||||||
|
MATERIAL_CN.put("LEATHER_BOOTS", "皮革靴子");
|
||||||
|
MATERIAL_CN.put("GOLDEN_APPLE", "附魔金苹果");
|
||||||
|
MATERIAL_CN.put("OBSIDIAN", "黑曜石");
|
||||||
|
MATERIAL_CN.put("DIAMOND_HELMET", "钻石头盔");
|
||||||
|
MATERIAL_CN.put("DIAMOND_CHESTPLATE", "钻石护甲");
|
||||||
|
MATERIAL_CN.put("DIAMOND_LEGGINGS", "钻石护腿");
|
||||||
|
MATERIAL_CN.put("DIAMOND_BOOTS", "钻石靴子");
|
||||||
|
MATERIAL_CN.put("IRON_HELMET", "铁头盔");
|
||||||
|
MATERIAL_CN.put("IRON_CHESTPLATE", "铁护甲");
|
||||||
|
MATERIAL_CN.put("IRON_LEGGINGS", "铁护腿");
|
||||||
|
MATERIAL_CN.put("IRON_BOOTS", "铁靴子");
|
||||||
|
MATERIAL_CN.put("WOODEN_SWORD", "木剑");
|
||||||
|
MATERIAL_CN.put("STONE_SWORD", "石剑");
|
||||||
|
MATERIAL_CN.put("IRON_SWORD", "铁剑");
|
||||||
|
MATERIAL_CN.put("GOLDEN_SWORD", "金剑");
|
||||||
|
MATERIAL_CN.put("DIAMOND_SWORD", "钻石剑");
|
||||||
|
MATERIAL_CN.put("GREEN_CONCRETE", "绿色混凝土");
|
||||||
|
MATERIAL_CN.put("LIME_CONCRETE", "淡绿色混凝土");
|
||||||
|
MATERIAL_CN.put("JUNGLE_PLANKS", "丛林木板");
|
||||||
|
MATERIAL_CN.put("ACACIA_PLANKS", "金合欢木板");
|
||||||
|
MATERIAL_CN.put("BLAZE_ROD", "烈焰棒");
|
||||||
|
MATERIAL_CN.put("MAGMA_CREAM", "熔浆膏");
|
||||||
|
MATERIAL_CN.put("CLAY_BALL", "黏土球");
|
||||||
|
MATERIAL_CN.put("MUSIC_DISC_13", "音乐唱片C13");
|
||||||
|
MATERIAL_CN.put("MUSIC_DISC_PIGSTEP", "音乐唱片PigStep");
|
||||||
|
MATERIAL_CN.put("END_CRYSTAL", "末影水晶");
|
||||||
|
MATERIAL_CN.put("GLOWSTONE", "萤石粉");
|
||||||
|
MATERIAL_CN.put("DRAGON_EGG", "龙蛋");
|
||||||
|
MATERIAL_CN.put("NETHERITE_SCRAP", "下界合金碎片");
|
||||||
|
MATERIAL_CN.put("CREEPER_HEAD", "爬行者头颅");
|
||||||
|
MATERIAL_CN.put("GOLDEN_APPLE", "金苹果");
|
||||||
|
MATERIAL_CN.put("ENCHANTED_GOLDEN_APPLE", "附魔金苹果");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成中文物品名 */
|
||||||
|
private String toChineseName(String materialName) {
|
||||||
|
String upper = materialName.toUpperCase();
|
||||||
|
return MATERIAL_CN.getOrDefault(upper, materialName.replace("_", " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成GUI验证码:N个物品+1个正确答案,30秒超时 */
|
||||||
|
private void generateGUIChallenge(String playerName, Random rand) {
|
||||||
|
// 丰富物品池,保证足够多样性
|
||||||
|
Material[] materials = {Material.STONE, Material.COBBLESTONE, Material.GRAVEL,
|
||||||
|
Material.SAND, Material.CLAY_BALL, Material.REDSTONE,
|
||||||
|
Material.LAPIS_LAZULI, Material.QUARTZ, Material.IRON_INGOT,
|
||||||
|
Material.GOLD_INGOT, Material.DIAMOND, Material.EMERALD,
|
||||||
|
Material.GREEN_CONCRETE, Material.LIME_CONCRETE,
|
||||||
|
Material.JUNGLE_PLANKS, Material.ACACIA_PLANKS,
|
||||||
|
Material.BLAZE_ROD, Material.CREEPER_HEAD, Material.DRAGON_EGG,
|
||||||
|
Material.NETHERITE_SCRAP, Material.END_CRYSTAL, Material.GLOWSTONE,
|
||||||
|
Material.MAGMA_CREAM, Material.MUSIC_DISC_13, Material.MUSIC_DISC_PIGSTEP,
|
||||||
|
Material.BOOK, Material.BOWL, Material.CAKE, Material.FLINT,
|
||||||
|
Material.FLINT_AND_STEEL, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
|
||||||
|
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.APPLE,
|
||||||
|
Material.GOLDEN_APPLE, Material.ENCHANTED_GOLDEN_APPLE,
|
||||||
|
Material.OBSIDIAN, Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE,
|
||||||
|
Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.IRON_HELMET,
|
||||||
|
Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS,
|
||||||
|
Material.WOODEN_SWORD, Material.STONE_SWORD, Material.IRON_SWORD,
|
||||||
|
Material.GOLDEN_SWORD, Material.DIAMOND_SWORD};
|
||||||
|
|
||||||
|
// 随机选1个正确答案
|
||||||
|
Material correctItem = materials[rand.nextInt(materials.length)];
|
||||||
|
String chineseName = toChineseName(correctItem.name());
|
||||||
|
|
||||||
|
// 选N-1个干扰项(5~10个物品,其中1个正确,其余干扰)
|
||||||
|
int totalItems = 5 + rand.nextInt(6); // 5~10
|
||||||
|
List<Material> distractorPool = new ArrayList<>();
|
||||||
|
for (Material m : materials) {
|
||||||
|
if (m != correctItem) distractorPool.add(m);
|
||||||
|
}
|
||||||
|
Collections.shuffle(distractorPool, rand);
|
||||||
|
|
||||||
|
List<Material> itemsToPlace = new ArrayList<>();
|
||||||
|
itemsToPlace.add(correctItem);
|
||||||
|
for (int i = 0; i < Math.min(totalItems - 1, distractorPool.size()); i++) {
|
||||||
|
itemsToPlace.add(distractorPool.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机打乱物品位置
|
||||||
|
Collections.shuffle(itemsToPlace, rand);
|
||||||
|
|
||||||
|
// 存储:正确答案的Material名(用英文做校验),显示用中文名
|
||||||
|
VerificationData vd = new VerificationData(0, 0, 0, VerificationType.GUI);
|
||||||
|
vd.guiTargets = correctItem.name(); // 校验用英文
|
||||||
|
vd.type = VerificationType.GUI;
|
||||||
|
verificationData.put(playerName, vd);
|
||||||
|
|
||||||
|
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Player p = org.bukkit.Bukkit.getPlayer(playerName);
|
||||||
|
if (p != null && p.isOnline()) {
|
||||||
|
try {
|
||||||
|
// 在GUI标题中明确显示中文目标物品名
|
||||||
|
Inventory inv = Bukkit.createInventory(null, 27, "§e§l点击" + chineseName + "完成验证");
|
||||||
|
|
||||||
|
// 填充灰色玻璃半透明方块作为背景
|
||||||
|
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
|
||||||
|
ItemMeta gm = glass.getItemMeta();
|
||||||
|
if (gm != null) { gm.setDisplayName(" "); glass.setItemMeta(gm); }
|
||||||
|
for (int i = 0; i < 27; i++) inv.setItem(i, glass);
|
||||||
|
|
||||||
|
// 随机打乱物品放置位置(避开边框slot 0-8, 18-26, 9,14,19)
|
||||||
|
List<Integer> availableSlots = Arrays.asList(9, 10, 11, 13, 14, 15, 17, 19, 20, 21);
|
||||||
|
Collections.shuffle(availableSlots, rand);
|
||||||
|
|
||||||
|
int itemIdx = 0;
|
||||||
|
for (int slot : availableSlots) {
|
||||||
|
if (itemIdx >= itemsToPlace.size()) break;
|
||||||
|
Material mat = itemsToPlace.get(itemIdx);
|
||||||
|
String cnName = toChineseName(mat.name());
|
||||||
|
inv.setItem(slot, mkItem(mat, "§f" + cnName, ""));
|
||||||
|
itemIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.openInventory(inv);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.getLogger().warning("[验证码] GUI挑战生成失败: " + ex.getMessage());
|
||||||
|
// 降级为数学题
|
||||||
|
generateMathChallenge(playerName, rand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:创建带显示名的物品
|
||||||
|
private ItemStack mkItem(Material mat, String displayName, String lore) {
|
||||||
|
ItemStack stack = new ItemStack(mat);
|
||||||
|
ItemMeta meta = stack.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName(displayName);
|
||||||
|
List<String> lores = new ArrayList<>();
|
||||||
|
lores.add(lore);
|
||||||
|
meta.setLore(lores);
|
||||||
|
stack.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerificationResult checkAnswer(String playerName, String answerStr) {
|
||||||
|
VerificationData vd = verificationData.get(playerName);
|
||||||
|
if (vd == null) return VerificationResult.VERIFIED;
|
||||||
|
|
||||||
|
if (vd.isExpired()) {
|
||||||
|
verificationData.remove(playerName);
|
||||||
|
verifiedPlayers.add(playerName);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vd.type == VerificationType.MATH) {
|
||||||
|
return checkMathAnswer(playerName, answerStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI验证码 — 聊天输入不作为答案,返回PENDING等待GUI点击
|
||||||
|
return VerificationResult.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家回答是否正确
|
||||||
|
* @deprecated 使用 checkAnswer 代替
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public VerificationResult checkMathAnswer(String playerName, String answerStr) {
|
||||||
|
VerificationData vd = verificationData.get(playerName);
|
||||||
|
if (vd == null) return VerificationResult.VERIFIED; // 无验证码 → 放行
|
||||||
|
|
||||||
|
if (vd.isExpired()) {
|
||||||
|
verificationData.remove(playerName);
|
||||||
|
verifiedPlayers.add(playerName); // 过期后不再要求
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vd.type == VerificationType.MATH) {
|
||||||
|
try {
|
||||||
|
int ans = Integer.parseInt(answerStr.trim());
|
||||||
|
int expected;
|
||||||
|
switch (vd.op) {
|
||||||
|
case 0 -> expected = vd.a + vd.b;
|
||||||
|
case 1 -> expected = vd.a - vd.b;
|
||||||
|
default -> expected = vd.a * vd.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ans == expected) {
|
||||||
|
vd.completed = true;
|
||||||
|
verificationData.remove(playerName);
|
||||||
|
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
} else {
|
||||||
|
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
|
||||||
|
return VerificationResult.FAILED;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 非数字答案 → 同样不放行,重新答题
|
||||||
|
return VerificationResult.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI验证码 — answerStr 为空时表示未回答
|
||||||
|
if (answerStr == null || answerStr.isEmpty()) {
|
||||||
|
return VerificationResult.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerificationResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理GUI物品点击验证码
|
||||||
|
* @param playerName 玩家名
|
||||||
|
* @param clickedItemName 点击的物品Material名(如"STONE")
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
public VerificationResult checkGUIClick(String playerName, String clickedItemName) {
|
||||||
|
String name = playerName;
|
||||||
|
VerificationData vd = verificationData.get(name);
|
||||||
|
if (vd == null) return VerificationResult.VERIFIED;
|
||||||
|
if (!VerificationType.GUI.equals(vd.type)) return VerificationResult.PENDING;
|
||||||
|
|
||||||
|
if (vd.isExpired()) {
|
||||||
|
verificationData.remove(name);
|
||||||
|
verifiedPlayers.add(name);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 关键修复:点击的物品名必须等于目标物品名才通过
|
||||||
|
if (clickedItemName != null && clickedItemName.equals(vd.guiTargets)) {
|
||||||
|
vd.completed = true;
|
||||||
|
verificationData.remove(name);
|
||||||
|
verifiedPlayers.add(name);
|
||||||
|
return VerificationResult.VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击错误物品 → 消息拦截
|
||||||
|
return VerificationResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 临时缓存玩家未验证的消息(N+1 机制:缓存列表,答对第N题后释放N条)
|
||||||
|
public final Map<String, List<String>> pendingMessages = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** 缓存玩家消息,等验证通过时代为广播 */
|
||||||
|
public void cachePendingMessage(String playerName, String message) {
|
||||||
|
pendingMessages.computeIfAbsent(playerName, k -> new ArrayList<>()).add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 广播所有缓存的消息(N+1机制:广播缓存的N-1条 + 当前第N条) */
|
||||||
|
public void broadcastCachedMessages(String playerName) {
|
||||||
|
List<String> messages = pendingMessages.remove(playerName);
|
||||||
|
if (messages != null) {
|
||||||
|
for (String msg : messages) {
|
||||||
|
Bukkit.broadcastMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除玩家缓存的消息(验证失败时调用) */
|
||||||
|
public void clearPendingMessages(String playerName) {
|
||||||
|
pendingMessages.remove(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的验证码
|
||||||
|
*/
|
||||||
|
public void cleanupExpiredVerifications() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
verificationData.entrySet().removeIf(e -> {
|
||||||
|
if (e.getValue().isExpired() && !e.getValue().completed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否为新玩家(register_time 在1小时内)
|
||||||
|
*/
|
||||||
|
public boolean isNewPlayerUnder1Hour(Player p) {
|
||||||
|
if (p == null || !p.isOnline()) return false;
|
||||||
|
try {
|
||||||
|
Main mainPlugin = (Main) plugin;
|
||||||
|
DatabaseManager db = mainPlugin.getDb();
|
||||||
|
if (db == null) return false; // 查不到就不拦截,直接放行
|
||||||
|
|
||||||
|
// register_time 在数据库存的是秒级时间戳
|
||||||
|
Object regTimeObj = db.getField(p.getName(), "register_time");
|
||||||
|
if (regTimeObj == null) return false; // 没注册记录 → 放行
|
||||||
|
|
||||||
|
long regTimeSeconds = ((Number) regTimeObj).longValue();
|
||||||
|
if (regTimeSeconds == 0) return false; // 注册时间为0 → 放行
|
||||||
|
|
||||||
|
long regTimeMs = regTimeSeconds * 1000;
|
||||||
|
long elapsed = System.currentTimeMillis() - regTimeMs;
|
||||||
|
boolean isNew = elapsed < 3600000;
|
||||||
|
|
||||||
|
// 日志调试:打印注册时间差值
|
||||||
|
if (isNew) {
|
||||||
|
plugin.getLogger().info("[验证码] 新玩家: " + p.getName() + " 注册" + (elapsed/1000) + "秒前");
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNew;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false; // 出错时默认放行,不卡玩家
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 广告机检测 ====================
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
@@ -50,12 +528,15 @@ public class ChatFilterManager {
|
|||||||
|
|
||||||
private static final Pattern URL_PATTERN =
|
private static final Pattern URL_PATTERN =
|
||||||
Pattern.compile(
|
Pattern.compile(
|
||||||
"(?i)(?:https?://\\S+)"
|
"(?i)"
|
||||||
+ "|(?:(?:[a-zA-Z0-9]"
|
// 1. 标准URL: http://xxx.com/...
|
||||||
+ "(?:[a-zA-Z0-9\\-]*"
|
+ "(?:https?://\\S+)"
|
||||||
+ "[a-zA-Z0-9])?\\.)+"
|
// 2. 点分隔域名: xxx.com, my.site.cn
|
||||||
+ "[a-zA-Z]{2,})"
|
+ "|(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,})(?:/\\S*)?"
|
||||||
+ "(?:/\\S*)?");
|
// 3. 下划线分隔域名: xxx_com, my_site.cn, 123_ypshidifu.cn
|
||||||
|
+ "|(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)"
|
||||||
|
+ "(?:[_.][a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*"
|
||||||
|
+ "[_.][a-zA-Z]{2,}");
|
||||||
private static final Pattern DOT_VARIANTS =
|
private static final Pattern DOT_VARIANTS =
|
||||||
Pattern.compile(
|
Pattern.compile(
|
||||||
"[\uff0e\u3002\u2025\u2026\u00b7]");
|
"[\uff0e\u3002\u2025\u2026\u00b7]");
|
||||||
@@ -609,4 +1090,178 @@ public class ChatFilterManager {
|
|||||||
|| s.contains("启用")
|
|| s.contains("启用")
|
||||||
|| s.contains("是");
|
|| s.contains("是");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 广告机检测 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查消息是否包含第三方推广内容(邮件、游戏内推广等)
|
||||||
|
*/
|
||||||
|
public boolean isAdvertisingContent(String msg) {
|
||||||
|
String lower = msg.toLowerCase();
|
||||||
|
// 检测关键词
|
||||||
|
String[] keywords = {"群", "群号", "QQ", "qq群", "加群", "交流群", "服务器", "开服", "联机", "白嫖", "免费送", "折扣", "打折", "代充", "充值优惠", "外挂", "脚本", "辅助", "破解"};
|
||||||
|
for (String kw : keywords) {
|
||||||
|
if (lower.contains(kw)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录玩家发送链接的行为,检测广告机
|
||||||
|
* @return 如果检测到广告机返回true
|
||||||
|
*/
|
||||||
|
public boolean checkAdBotBehavior(String playerName, String msg) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// 提取URL
|
||||||
|
List<String> urls = extractUrls(msg);
|
||||||
|
if (urls.isEmpty()) return false;
|
||||||
|
|
||||||
|
// 检查是否全部非白名单
|
||||||
|
boolean hasNonWhitelisted = false;
|
||||||
|
for (String url : urls) {
|
||||||
|
if (!isWhitelisted(url)) {
|
||||||
|
hasNonWhitelisted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasNonWhitelisted) return false;
|
||||||
|
|
||||||
|
// 检查是否包含推广关键词
|
||||||
|
if (!isAdvertisingContent(msg)) return false;
|
||||||
|
|
||||||
|
// 记录链接发送时间
|
||||||
|
linkSendHistory.computeIfAbsent(playerName, k -> new ArrayList<>()).add(now);
|
||||||
|
|
||||||
|
// 清理超过10分钟前的记录
|
||||||
|
List<Long> history = linkSendHistory.get(playerName);
|
||||||
|
history.removeIf(t -> (now - t) > 600000);
|
||||||
|
|
||||||
|
// 10分钟内发送超过3次非白名单链接 → 广告机
|
||||||
|
if (history.size() >= 3) {
|
||||||
|
linkSendHistory.remove(playerName);
|
||||||
|
return true; // 检测到广告机
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记玩家活跃(用于挂机检测)
|
||||||
|
*/
|
||||||
|
public void markPlayerActive(String playerName) {
|
||||||
|
playerLastActiveTime.put(playerName, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否长时间挂机(超过5分钟)
|
||||||
|
*/
|
||||||
|
public boolean isPlayerIdle(String playerName, long idleThresholdMs) {
|
||||||
|
Long lastActive = playerLastActiveTime.get(playerName);
|
||||||
|
if (lastActive == null) return true;
|
||||||
|
return (System.currentTimeMillis() - lastActive) > idleThresholdMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送管理员通知邮件
|
||||||
|
*/
|
||||||
|
public void sendAdminNotification(String subject, String body) {
|
||||||
|
// 异步发送,不阻塞主线程
|
||||||
|
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
EmailManager emailMgr = ((Main) plugin).getEmail();
|
||||||
|
if (emailMgr != null) {
|
||||||
|
emailMgr.sendBody(adminEmail, subject, body);
|
||||||
|
plugin.getLogger().info("[广告机检测] 已发送管理员通知: " + subject);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[广告机检测] 发送管理员通知失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 非法域名后缀管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载非法域名后缀配置文件(支持热更新)
|
||||||
|
* 配置文件: plugins/Sdf1_login/illegal_domains.txt
|
||||||
|
* 每行一个后缀,如: com, cn, test 等
|
||||||
|
*/
|
||||||
|
public void loadIllegalDomains() {
|
||||||
|
illegalDomainSuffixes.clear();
|
||||||
|
File f = new File(plugin.getDataFolder(), "illegal_domains.txt");
|
||||||
|
if (!f.exists()) {
|
||||||
|
plugin.getLogger().info("[非法域名] 配置文件不存在,已加载默认内置后缀");
|
||||||
|
// 内置默认后缀作为兜底
|
||||||
|
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
|
||||||
|
illegalDomainSuffixes.add(s.toLowerCase());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
// 去掉可能的 * 前缀
|
||||||
|
String suffix = line.replaceFirst("^\\*", "").toLowerCase();
|
||||||
|
if (!suffix.isEmpty()) {
|
||||||
|
illegalDomainSuffixes.add(suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("[非法域名] 已加载 " + illegalDomainSuffixes.size() + " 个非法后缀(来自配置文件)");
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[非法域名] 加载失败,使用内置默认后缀: " + e.getMessage());
|
||||||
|
// 加载失败时用内置兜底
|
||||||
|
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
|
||||||
|
illegalDomainSuffixes.add(s.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查名称是否包含非法域名格式(如 baidu_com, mysite.cn 等)
|
||||||
|
*/
|
||||||
|
public boolean containsIllegalDomain(String name) {
|
||||||
|
if (name == null || name.isEmpty()) return false;
|
||||||
|
String lowerName = name.toLowerCase();
|
||||||
|
|
||||||
|
// 支持两种格式:1) dot分隔 xxx.com 2) underscore分隔 xxx_com
|
||||||
|
for (String separator : new String[]{".", "_"}) {
|
||||||
|
int idx = lowerName.lastIndexOf(separator);
|
||||||
|
if (idx < 0) continue;
|
||||||
|
|
||||||
|
String suffix = lowerName.substring(idx + 1);
|
||||||
|
|
||||||
|
// 去掉可能的前缀嵌套
|
||||||
|
if (suffix.contains(separator)) {
|
||||||
|
suffix = suffix.substring(suffix.lastIndexOf(separator) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String domainSuffix : illegalDomainSuffixes) {
|
||||||
|
if (suffix.equals(domainSuffix) && !suffix.isEmpty()) {
|
||||||
|
// 前面必须有主体部分(排除纯后缀如 .com 本身)
|
||||||
|
String before = lowerName.substring(0, idx);
|
||||||
|
if (!before.isEmpty() && before.length() >= 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时检查空闲玩家
|
||||||
|
*/
|
||||||
|
public void checkIdlePlayers() {
|
||||||
|
long idleThreshold = 5 * 60 * 1000L; // 5分钟
|
||||||
|
org.bukkit.Bukkit.getOnlinePlayers().forEach(p -> {
|
||||||
|
if (isPlayerIdle(p.getName(), idleThreshold)) {
|
||||||
|
// 不再发送空闲消息,仅刷新计时
|
||||||
|
playerLastActiveTime.put(p.getName(), System.currentTimeMillis()); // 刷新计时
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,8 @@ public class ChatInputManager {
|
|||||||
name, "password_salt");
|
name, "password_salt");
|
||||||
String hash =
|
String hash =
|
||||||
PasswordUtils.hash(msg, salt);
|
PasswordUtils.hash(msg, salt);
|
||||||
boolean mainOk =
|
String result = db.checkPasswordWithFallback(name, hash);
|
||||||
db.checkPassword(name, hash);
|
if ("main".equals(result)) {
|
||||||
if (mainOk) {
|
|
||||||
state.type =
|
state.type =
|
||||||
InputType.CHANGE_PWD_STEP2;
|
InputType.CHANGE_PWD_STEP2;
|
||||||
state.ticketTitle = "main";
|
state.ticketTitle = "main";
|
||||||
@@ -100,10 +99,7 @@ public class ChatInputManager {
|
|||||||
"§e请输入新密码:");
|
"§e请输入新密码:");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean tempOk =
|
if ("temp".equals(result)) {
|
||||||
db.checkPasswordOrTemp(
|
|
||||||
name, hash);
|
|
||||||
if (tempOk) {
|
|
||||||
state.type =
|
state.type =
|
||||||
InputType.CHANGE_PWD_STEP2;
|
InputType.CHANGE_PWD_STEP2;
|
||||||
state.ticketTitle = "temp";
|
state.ticketTitle = "temp";
|
||||||
|
|||||||
@@ -149,18 +149,8 @@ public class CommissionManager {
|
|||||||
originalAmount * rate;
|
originalAmount * rate;
|
||||||
if (commission <= 0) return;
|
if (commission <= 0) return;
|
||||||
|
|
||||||
// 通过 Vault 发放
|
// Economy已移除,佣金提成功能不可用
|
||||||
var rsp = main.getServer()
|
// 邀请人可通过债券系统获取奖励
|
||||||
.getServicesManager()
|
|
||||||
.getRegistration(
|
|
||||||
net.milkbowl.vault
|
|
||||||
.economy.Economy.class);
|
|
||||||
if (rsp == null) return;
|
|
||||||
net.milkbowl.vault.economy.Economy econ =
|
|
||||||
rsp.getProvider();
|
|
||||||
if (econ == null) return;
|
|
||||||
|
|
||||||
econ.depositPlayer(inviter, commission);
|
|
||||||
|
|
||||||
// 通知邀请人
|
// 通知邀请人
|
||||||
Player inviterP =
|
Player inviterP =
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ public class ConfigManager {
|
|||||||
public int garbageInterval = 300;
|
public int garbageInterval = 300;
|
||||||
public int garbageMaxRounds = 1;
|
public int garbageMaxRounds = 1;
|
||||||
|
|
||||||
|
// ===== 区域防护配置 =====
|
||||||
|
public String areaProtectAdminMode = "tag"; // "tag" 或 "op",不共存
|
||||||
|
public String areaProtectAdminTag = "admin"; // 管理员Tag名
|
||||||
|
public double areaProtectPricePerSqM = 10.0; // 每平米价格(债券)
|
||||||
|
public int areaProtectMaxLands = 3; // 每个玩家最大领地数
|
||||||
|
|
||||||
|
// ===== 传送配置 =====
|
||||||
|
public int tpRequestValidSeconds = 90; // 请求有效时间(秒)
|
||||||
|
public int tpSendIntervalSeconds = 10; // 发送间隔(秒)
|
||||||
|
|
||||||
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
|
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
|
||||||
public ConfigManager(File dataFolder) {
|
public ConfigManager(File dataFolder) {
|
||||||
this.dataFolder = dataFolder;
|
this.dataFolder = dataFolder;
|
||||||
@@ -146,6 +156,41 @@ public class ConfigManager {
|
|||||||
Map<String, String> m = new LinkedHashMap<>();
|
Map<String, String> m = new LinkedHashMap<>();
|
||||||
loadMap(file, m);
|
loadMap(file, m);
|
||||||
|
|
||||||
|
// 检查是否需要补充新配置项
|
||||||
|
boolean hasNewKey = false;
|
||||||
|
String[][] newKeys = {
|
||||||
|
{"签到债券最小", String.valueOf(checkinBondMin)},
|
||||||
|
{"签到债券最大", String.valueOf(checkinBondMax)},
|
||||||
|
{"奖励发放方式", "债券"},
|
||||||
|
{"区域防护_管理员模式", areaProtectAdminMode},
|
||||||
|
{"区域防护_管理员Tag", areaProtectAdminTag},
|
||||||
|
{"区域防护_每平米价格", String.valueOf(areaProtectPricePerSqM)},
|
||||||
|
{"区域防护_最大领地数", String.valueOf(areaProtectMaxLands)},
|
||||||
|
{"传送_请求有效秒", String.valueOf(tpRequestValidSeconds)},
|
||||||
|
{"传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds)},
|
||||||
|
};
|
||||||
|
for (String[] nk : newKeys) {
|
||||||
|
if (!m.containsKey(nk[0])) { hasNewKey = true; }
|
||||||
|
}
|
||||||
|
if (hasNewKey) {
|
||||||
|
try {
|
||||||
|
FileWriter fw = new FileWriter(file, true);
|
||||||
|
BufferedWriter bw = new BufferedWriter(fw);
|
||||||
|
bw.newLine();
|
||||||
|
bw.write("# ===== 新增配置项 =====");
|
||||||
|
bw.newLine();
|
||||||
|
for (String[] nk : newKeys) {
|
||||||
|
if (!m.containsKey(nk[0])) {
|
||||||
|
bw.write(nk[0] + "=" + nk[1]);
|
||||||
|
bw.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bw.flush(); bw.close();
|
||||||
|
System.out.println("[Config] 已补充缺失的配置项");
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取ConfigManager管理的配置项
|
||||||
adminTag = m.getOrDefault("管理标签", adminTag);
|
adminTag = m.getOrDefault("管理标签", adminTag);
|
||||||
adminPassword = m.getOrDefault("管理密码", adminPassword);
|
adminPassword = m.getOrDefault("管理密码", adminPassword);
|
||||||
maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3);
|
maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3);
|
||||||
@@ -183,45 +228,16 @@ public class ConfigManager {
|
|||||||
rewardChannel = m.getOrDefault("奖励发放方式", "债券");
|
rewardChannel = m.getOrDefault("奖励发放方式", "债券");
|
||||||
rewardChannel = normalizeRewardChannel(rewardChannel);
|
rewardChannel = normalizeRewardChannel(rewardChannel);
|
||||||
|
|
||||||
// 验证合法值
|
// ===== 区域防护配置 =====
|
||||||
if (!"economy".equalsIgnoreCase(rewardChannel)
|
areaProtectAdminMode = m.getOrDefault("区域防护_管理员模式", "tag").toLowerCase();
|
||||||
&& !"bonds".equalsIgnoreCase(rewardChannel)) {
|
areaProtectAdminTag = m.getOrDefault("区域防护_管理员Tag", adminTag);
|
||||||
rewardChannel = "bonds";
|
areaProtectPricePerSqM = parseDouble(m.getOrDefault("区域防护_每平米价格", "10"), 10.0);
|
||||||
}
|
areaProtectMaxLands = parseInt(m.getOrDefault("区域防护_最大领地数", "3"), 3);
|
||||||
|
|
||||||
|
tpRequestValidSeconds = parseIntFromString(m.getOrDefault("传送_请求有效秒", "90"));
|
||||||
|
tpSendIntervalSeconds = parseIntFromString(m.getOrDefault("传送_发送间隔秒", "10"));
|
||||||
|
|
||||||
|
// rewardChannel 已标准化为 bonds/economy,默认bonds
|
||||||
// ★ 自动补充新配置项 ★
|
|
||||||
boolean needAppend = false;
|
|
||||||
List<String> newKeys = new ArrayList<>();
|
|
||||||
if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); }
|
|
||||||
if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); }
|
|
||||||
if (!m.containsKey("奖励发放方式")) {
|
|
||||||
needAppend = true;
|
|
||||||
newKeys.add("奖励发放方式=债券");
|
|
||||||
}
|
|
||||||
if (needAppend) {
|
|
||||||
try {
|
|
||||||
FileWriter fw = new FileWriter(file, true);
|
|
||||||
BufferedWriter bw = new BufferedWriter(fw);
|
|
||||||
bw.newLine();
|
|
||||||
for (String key : newKeys) { bw.write(key); bw.newLine(); }
|
|
||||||
bw.flush(); bw.close();
|
|
||||||
System.out.println("[Config] 已补充签到债券配置");
|
|
||||||
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
if (!m.containsKey("奖励发放方式")) {
|
|
||||||
try {
|
|
||||||
FileWriter fw = new FileWriter(file, true);
|
|
||||||
BufferedWriter bw = new BufferedWriter(fw);
|
|
||||||
bw.newLine();
|
|
||||||
bw.write("奖励发放方式=bonds");
|
|
||||||
bw.newLine();
|
|
||||||
bw.flush(); bw.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 奖励方式标准化:中英文全兼容
|
* 奖励方式标准化:中英文全兼容
|
||||||
@@ -245,30 +261,106 @@ public class ConfigManager {
|
|||||||
|
|
||||||
public void saveSettings() {
|
public void saveSettings() {
|
||||||
File file = new File(dataFolder, "插件设置.txt");
|
File file = new File(dataFolder, "插件设置.txt");
|
||||||
|
|
||||||
|
// ★ 读取已有配置,保留Web通信等其他模块管理的配置行
|
||||||
|
java.util.LinkedHashMap<String, String> existingPairs = new java.util.LinkedHashMap<>();
|
||||||
|
java.util.List<String> existingLines = new ArrayList<>();
|
||||||
|
if (file.exists()) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
existingLines.add(line);
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (!trimmed.isEmpty() && !trimmed.startsWith("#") && trimmed.contains("=")) {
|
||||||
|
int eq = trimmed.indexOf('=');
|
||||||
|
String k = trimmed.substring(0, eq).trim();
|
||||||
|
existingPairs.put(k, trimmed.substring(eq + 1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ ConfigManager管理的配置项
|
||||||
|
java.util.LinkedHashMap<String, String> managed = new java.util.LinkedHashMap<>();
|
||||||
|
managed.put("管理标签", adminTag);
|
||||||
|
managed.put("管理密码", adminPassword);
|
||||||
|
managed.put("每IP最大账号数", String.valueOf(maxAccountsPerIP));
|
||||||
|
managed.put("审批模式", approvalMode);
|
||||||
|
managed.put("自动审批延迟分钟", String.valueOf(autoApproveDelayMinutes));
|
||||||
|
managed.put("登录超时秒数", String.valueOf(loginTimeout));
|
||||||
|
managed.put("挂机踢出", String.valueOf(afkEnabled));
|
||||||
|
managed.put("挂机超时秒数", String.valueOf(afkTimeout));
|
||||||
|
managed.put("resource-pack-url", broadcastServerUrl);
|
||||||
|
managed.put("http-port", String.valueOf(httpPort));
|
||||||
|
managed.put("垃圾站_启用", String.valueOf(garbageEnabled));
|
||||||
|
managed.put("垃圾站_清理间隔秒", String.valueOf(garbageInterval));
|
||||||
|
managed.put("垃圾站_保留轮数", String.valueOf(garbageMaxRounds));
|
||||||
|
managed.put("签到固定金额", String.valueOf(checkinRewardFixed));
|
||||||
|
managed.put("签到最小金额", String.valueOf(checkinRewardMin));
|
||||||
|
managed.put("签到最大金额", String.valueOf(checkinRewardMax));
|
||||||
|
managed.put("签到给积分", String.valueOf(checkinGivePoints));
|
||||||
|
managed.put("签到积分数量", String.valueOf(checkinPoints));
|
||||||
|
managed.put("签到债券最小", String.valueOf(checkinBondMin));
|
||||||
|
managed.put("签到债券最大", String.valueOf(checkinBondMax));
|
||||||
|
managed.put("补签积分倍率", String.valueOf(backCheckPointMultiplier));
|
||||||
|
managed.put("奖励发放方式", "债券");
|
||||||
|
managed.put("传送_请求有效秒", String.valueOf(tpRequestValidSeconds));
|
||||||
|
managed.put("传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds));
|
||||||
|
|
||||||
|
// ★ 保留非ConfigManager管理的配置行(如web通信-*等)
|
||||||
|
// ★ 同时保留连续的注释块(如 "# ===== Web通信配置 =====")
|
||||||
|
java.util.List<String> preservedLines = new ArrayList<>();
|
||||||
|
for (String line : existingLines) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (trimmed.isEmpty()) continue;
|
||||||
|
// 如果是注释行,检查后面是否有非管理配置,保留这种注释块
|
||||||
|
if (trimmed.startsWith("#")) {
|
||||||
|
// 检查后续是否有非管理配置
|
||||||
|
int idx = existingLines.indexOf(line);
|
||||||
|
boolean hasNonManaged = false;
|
||||||
|
for (int j = idx + 1; j < existingLines.size(); j++) {
|
||||||
|
String next = existingLines.get(j).trim();
|
||||||
|
if (next.isEmpty() || next.startsWith("#")) continue;
|
||||||
|
if (next.contains("=")) {
|
||||||
|
int eq = next.indexOf('=');
|
||||||
|
String k = next.substring(0, eq).trim();
|
||||||
|
if (!managed.containsKey(k)) { hasNonManaged = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasNonManaged) preservedLines.add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!trimmed.contains("=")) continue;
|
||||||
|
int eq = trimmed.indexOf('=');
|
||||||
|
String key = trimmed.substring(0, eq).trim();
|
||||||
|
if (!managed.containsKey(key)) {
|
||||||
|
preservedLines.add(key + "=" + trimmed.substring(eq + 1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 写入完整配置:管理的配置在前,其他模块配置在后(带注释分隔)
|
||||||
List<String> L = new ArrayList<>();
|
List<String> L = new ArrayList<>();
|
||||||
L.add("# ===== Sdf1_login 插件设置 =====");
|
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||||
L.add("管理标签=" + adminTag);
|
for (java.util.Map.Entry<String, String> e : managed.entrySet()) {
|
||||||
L.add("管理密码=" + adminPassword);
|
L.add(e.getKey() + "=" + e.getValue());
|
||||||
L.add("每IP最大账号数=" + maxAccountsPerIP);
|
}
|
||||||
L.add("审批模式=" + approvalMode);
|
// 追加保留的其他模块配置
|
||||||
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
|
if (!preservedLines.isEmpty()) {
|
||||||
L.add("登录超时秒数=" + loginTimeout);
|
L.add("");
|
||||||
L.add("挂机踢出=" + afkEnabled);
|
// 从 preservedLines 中找出开头的注释块并添加
|
||||||
L.add("挂机超时秒数=" + afkTimeout);
|
int firstDataIdx = 0;
|
||||||
L.add("resource-pack-url=" + broadcastServerUrl);
|
for (int i = 0; i < preservedLines.size(); i++) {
|
||||||
L.add("http-port=" + httpPort);
|
if (preservedLines.get(i).startsWith("#")) {
|
||||||
L.add("垃圾站_启用=" + garbageEnabled);
|
L.add(preservedLines.get(i));
|
||||||
L.add("垃圾站_清理间隔秒=" + garbageInterval);
|
firstDataIdx = i + 1;
|
||||||
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
|
} else {
|
||||||
L.add("签到固定金额=" + checkinRewardFixed);
|
break;
|
||||||
L.add("签到最小金额=" + checkinRewardMin);
|
}
|
||||||
L.add("签到最大金额=" + checkinRewardMax);
|
}
|
||||||
L.add("签到给积分=" + checkinGivePoints);
|
for (int i = firstDataIdx; i < preservedLines.size(); i++) {
|
||||||
L.add("签到积分数量=" + checkinPoints);
|
L.add(preservedLines.get(i));
|
||||||
L.add("签到债券最小=" + checkinBondMin);
|
}
|
||||||
L.add("签到债券最大=" + checkinBondMax);
|
}
|
||||||
L.add("补签积分倍率=" + backCheckPointMultiplier);
|
|
||||||
L.add("奖励发放方式=债券");
|
|
||||||
writeLines(file, L);
|
writeLines(file, L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +389,25 @@ public class ConfigManager {
|
|||||||
L.add("签到债券最大=3");
|
L.add("签到债券最大=3");
|
||||||
L.add("补签积分倍率=5");
|
L.add("补签积分倍率=5");
|
||||||
L.add("奖励发放方式=债券");
|
L.add("奖励发放方式=债券");
|
||||||
|
// 区域防护配置
|
||||||
|
L.add("区域防护_管理员模式=tag");
|
||||||
|
L.add("区域防护_管理员Tag=admin");
|
||||||
|
L.add("区域防护_每平米价格=10");
|
||||||
|
L.add("区域防护_最大领地数=3");
|
||||||
|
L.add("传送_请求有效秒=90");
|
||||||
|
L.add("传送_发送间隔秒=10");
|
||||||
|
// Web通信配置(默认禁用,需手动开启)
|
||||||
|
L.add("");
|
||||||
|
L.add("# ===== Web通信配置 =====");
|
||||||
|
L.add("web通信-启用=false");
|
||||||
|
L.add("web通信-地址=https://caoyuan.ypshidifu.cn/plugin");
|
||||||
|
L.add("web通信-Token有效期秒=600");
|
||||||
|
L.add("web通信-同步间隔分钟=5");
|
||||||
|
L.add("web通信-回调端口=9090");
|
||||||
|
L.add("web通信-密钥=sdf1_web_comm_2026_ypshidifu");
|
||||||
|
L.add("");
|
||||||
|
L.add("# ===== 其他模块配置 =====");
|
||||||
|
// 始终附加空行和注释分隔符,让后续追加逻辑更容易识别边界
|
||||||
writeLines(f, L);
|
writeLines(f, L);
|
||||||
}
|
}
|
||||||
public List<String> getAfkWhitelist() {
|
public List<String> getAfkWhitelist() {
|
||||||
@@ -362,11 +473,438 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void writeLines(File file, List<String> lines) {
|
private void writeLines(File file, List<String> lines) {
|
||||||
try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
// ★ 原子写入:先写临时文件再rename,防止写入中断导致文件损坏
|
||||||
|
File tmp = new File(file.getParentFile(), file.getName() + ".tmp");
|
||||||
|
try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmp), StandardCharsets.UTF_8))) {
|
||||||
for (String l : lines) { w.write(l); w.newLine(); }
|
for (String l : lines) { w.write(l); w.newLine(); }
|
||||||
} catch (IOException ignored) {}
|
w.flush(); // 强制刷盘
|
||||||
|
w.close(); // 确保关闭
|
||||||
|
// ★ 原子替换:将.tmp文件重命名为目标文件
|
||||||
|
// 不能反过来(file→tmp),否则Linux上file.renameTo(tmp)会成功覆盖.tmp→丢失新内容+原文件被删
|
||||||
|
if (!tmp.renameTo(file)) {
|
||||||
|
// 替换失败,删除临时文件
|
||||||
|
tmp.delete();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// 写入失败时删除临时文件,保留原文件不变
|
||||||
|
tmp.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
|
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
|
||||||
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
|
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将自然语言时间字符串解析为秒数
|
||||||
|
* 支持:90 | 1:30 | 1.30 | 一分钟三十秒 | 2分钟 | 90秒 | minute | min | II | IX | 贰分 | Roman numerals
|
||||||
|
*/
|
||||||
|
public int parseIntFromString(String s) {
|
||||||
|
if (s == null || s.trim().isEmpty()) return 90;
|
||||||
|
String trimmed = s.trim();
|
||||||
|
|
||||||
|
// ====== 0. 中文混合解析(优先:支持"2分钟"、"一分钟三十秒"、"贰分钟"等) ======
|
||||||
|
String testTrimmed = trimmed;
|
||||||
|
boolean hasChinese = false;
|
||||||
|
for (int ci = 0; ci < testTrimmed.length(); ci++) {
|
||||||
|
char cc = testTrimmed.charAt(ci);
|
||||||
|
if (cc >= '\u4e00' && cc <= '\u9fff') { hasChinese = true; break; }
|
||||||
|
}
|
||||||
|
if (hasChinese) {
|
||||||
|
int cnResult = parseChineseTime(testTrimmed);
|
||||||
|
if (cnResult >= 0) return cnResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 1. 英文混合解析(支持"2 minutes"、"one hour thirty seconds"等) ======
|
||||||
|
int enResult = parseEnglishTime(testTrimmed.toLowerCase());
|
||||||
|
if (enResult >= 0) return enResult;
|
||||||
|
|
||||||
|
// ====== 2. 纯数字(秒) ======
|
||||||
|
try { return Integer.parseInt(testTrimmed); } catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// ====== 3. 冒号分隔 mm:ss ======
|
||||||
|
if (testTrimmed.contains(":")) {
|
||||||
|
String[] parts = testTrimmed.split(":");
|
||||||
|
try {
|
||||||
|
int min = Integer.parseInt(parts[0].trim());
|
||||||
|
int sec = parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 0;
|
||||||
|
return min * 60 + sec;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 4. 点号分隔 mm.ss ======
|
||||||
|
if (testTrimmed.matches(".*\\d+\\.\\d+.*")) {
|
||||||
|
try {
|
||||||
|
String[] parts = testTrimmed.split("\\.");
|
||||||
|
if (parts.length == 2 && parts[0].matches("\\d+") && parts[1].matches("\\d+")) {
|
||||||
|
int min = Integer.parseInt(parts[0]);
|
||||||
|
int sec = Integer.parseInt(parts[1]);
|
||||||
|
return min * 60 + sec;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 5. 罗马数字(支持含空格:如"ii ii"→10) ======
|
||||||
|
String testLower = testTrimmed.toLowerCase();
|
||||||
|
if (testLower.chars().allMatch(c -> "ivxlcdm ".indexOf(c) >= 0) && !testTrimmed.replace(" ", "").isEmpty()) {
|
||||||
|
String[] parts = testTrimmed.split("\\s+");
|
||||||
|
int total = 0;
|
||||||
|
for (String part : parts) {
|
||||||
|
if (part.isEmpty()) continue;
|
||||||
|
int val = parseRoman(part);
|
||||||
|
if (val > 0) total += val;
|
||||||
|
}
|
||||||
|
if (total > 0) return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== 6. 兜底:提取所有数字 ======
|
||||||
|
java.util.regex.Matcher nm = java.util.regex.Pattern.compile("\\d+").matcher(testTrimmed);
|
||||||
|
java.util.List<Integer> nums = new java.util.ArrayList<>();
|
||||||
|
while (nm.find()) nums.add(Integer.parseInt(nm.group()));
|
||||||
|
if (nums.size() == 1) return nums.get(0);
|
||||||
|
if (nums.size() >= 2) return nums.get(0) * 60 + nums.get(1);
|
||||||
|
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析单个罗马数字串 */
|
||||||
|
private int parseRoman(String s) {
|
||||||
|
s = s.toLowerCase();
|
||||||
|
java.util.Map<Character, Integer> m = new java.util.HashMap<>();
|
||||||
|
m.put('i', 1); m.put('v', 5); m.put('x', 10);
|
||||||
|
m.put('l', 50); m.put('c', 100); m.put('d', 500); m.put('m', 1000);
|
||||||
|
int total = 0, prev = 0;
|
||||||
|
for (int i = s.length() - 1; i >= 0; i--) {
|
||||||
|
int curr = m.getOrDefault(s.charAt(i), 0);
|
||||||
|
if (curr < prev) total -= curr; else total += curr;
|
||||||
|
prev = curr;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中文时间解析(含阿拉伯数字+中文单位混合,如"2分钟"、"90秒"、"一分钟三十秒"、"贰分"等)
|
||||||
|
* 返回秒数,无法解析返回-1
|
||||||
|
*/
|
||||||
|
private int parseChineseTime(String raw) {
|
||||||
|
// 中文数字映射(含繁体/大写/各种变体)
|
||||||
|
java.util.Map<String, Integer> cnDigit = new java.util.LinkedHashMap<>();
|
||||||
|
cnDigit.put("零", 0); cnDigit.put("\u3007", 0);
|
||||||
|
cnDigit.put("一", 1); cnDigit.put("壹", 1); cnDigit.put("幺", 1);
|
||||||
|
cnDigit.put("二", 2); cnDigit.put("贰", 2); cnDigit.put("貳", 2); cnDigit.put("两", 2); cnDigit.put("弐", 2);
|
||||||
|
cnDigit.put("三", 3); cnDigit.put("叁", 3); cnDigit.put("參", 3);
|
||||||
|
cnDigit.put("四", 4); cnDigit.put("肆", 4);
|
||||||
|
cnDigit.put("五", 5); cnDigit.put("伍", 5);
|
||||||
|
cnDigit.put("六", 6); cnDigit.put("陆", 6); cnDigit.put("陸", 6);
|
||||||
|
cnDigit.put("七", 7); cnDigit.put("柒", 7); cnDigit.put("漆", 7);
|
||||||
|
cnDigit.put("八", 8); cnDigit.put("捌", 8);
|
||||||
|
cnDigit.put("九", 9); cnDigit.put("玖", 9);
|
||||||
|
|
||||||
|
// 时间单位映射(优先匹配长单位)
|
||||||
|
java.util.List<java.util.Map.Entry<String, Integer>> unitsList = new java.util.ArrayList<>();
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("秒钟", 1));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("小时", 3600));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("小時", 3600));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("分钟", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("分鍾", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("分鐘", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("分種", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("秒", 1));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("分", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("钟", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("鐘", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("時", 3600));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("s", 1));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("m", 60));
|
||||||
|
unitsList.add(new java.util.AbstractMap.SimpleEntry<>("h", 3600));
|
||||||
|
|
||||||
|
java.util.Set<String> multipliers = new java.util.HashSet<>(
|
||||||
|
java.util.Arrays.asList("十", "拾", "百", "佰", "千", "仟", "万"));
|
||||||
|
|
||||||
|
// 英文数字映射(混合解析用)
|
||||||
|
java.util.Map<String, Integer> enDigit = new java.util.LinkedHashMap<>();
|
||||||
|
enDigit.put("zero", 0); enDigit.put("one", 1); enDigit.put("two", 2); enDigit.put("three", 3); enDigit.put("four", 4);
|
||||||
|
enDigit.put("five", 5); enDigit.put("six", 6); enDigit.put("seven", 7); enDigit.put("eight", 8); enDigit.put("nine", 9);
|
||||||
|
enDigit.put("ten", 10); enDigit.put("eleven", 11); enDigit.put("twelve", 12); enDigit.put("thirteen", 13);
|
||||||
|
enDigit.put("fourteen", 14); enDigit.put("fifteen", 15); enDigit.put("sixteen", 16); enDigit.put("seventeen", 17);
|
||||||
|
enDigit.put("eighteen", 18); enDigit.put("nineteen", 19);
|
||||||
|
enDigit.put("twenty", 20); enDigit.put("thirty", 30); enDigit.put("forty", 40); enDigit.put("fifty", 50);
|
||||||
|
enDigit.put("sixty", 60); enDigit.put("seventy", 70); enDigit.put("eighty", 80); enDigit.put("ninety", 90);
|
||||||
|
enDigit.put("hundred", 100); enDigit.put("thousand", 1000);
|
||||||
|
|
||||||
|
String s = raw;
|
||||||
|
int totalSeconds = 0;
|
||||||
|
int pendingValue = 0;
|
||||||
|
|
||||||
|
// ★ 第一步:按最大匹配原则分词
|
||||||
|
java.util.List<String> tokens = new java.util.ArrayList<>();
|
||||||
|
int i = 0;
|
||||||
|
while (i < s.length()) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c >= '\u4e00' && c <= '\u9fff') {
|
||||||
|
// 尝试2字符匹配(先匹配单位复合词)
|
||||||
|
if (i + 1 < s.length() && s.charAt(i+1) >= '\u4e00' && s.charAt(i+1) <= '\u9fff') {
|
||||||
|
String two = s.substring(i, i+2);
|
||||||
|
boolean matchedUnit = false;
|
||||||
|
for (var entry : unitsList) {
|
||||||
|
if (entry.getKey().equals(two)) {
|
||||||
|
tokens.add(two);
|
||||||
|
i += 2;
|
||||||
|
matchedUnit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchedUnit) continue;
|
||||||
|
|
||||||
|
// 检查两个中文字符串是否可以组合成中文数字(如"十五"→15、「三十」→30、「一百」→100)
|
||||||
|
String firstChar = String.valueOf(s.charAt(i));
|
||||||
|
String secondChar = String.valueOf(s.charAt(i+1));
|
||||||
|
Integer dFirst = cnDigit.get(firstChar);
|
||||||
|
Integer dSecond = cnDigit.get(secondChar);
|
||||||
|
|
||||||
|
if (dFirst != null && dSecond != null) {
|
||||||
|
// 两个都是数字 → 拼成一个完整中文数字(如"一二"→12, "二五"→25)
|
||||||
|
tokens.add(firstChar + secondChar);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dFirst != null && multipliers.contains(secondChar)) {
|
||||||
|
// 数字+乘法器(如"一二"中"二"不是乘法器,跳过这分支)
|
||||||
|
// 实际上这种情况不会发生因为multipliers是{十,百,千,万}
|
||||||
|
}
|
||||||
|
// 不满足组合条件 → 拆开
|
||||||
|
tokens.add(firstChar);
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
tokens.add(String.valueOf(c));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else if ((c >= '0' && c <= '9') || c == '.') {
|
||||||
|
int start = i;
|
||||||
|
while (i < s.length() && ((s.charAt(i) >= '0' && s.charAt(i) <= '9') || s.charAt(i) == '.')) i++;
|
||||||
|
tokens.add(s.substring(start, i));
|
||||||
|
} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
|
||||||
|
int start = i;
|
||||||
|
while (i < s.length() && ((s.charAt(i) >= 'a' && s.charAt(i) <= 'z') || (s.charAt(i) >= 'A' && s.charAt(i) <= 'Z'))) i++;
|
||||||
|
tokens.add(s.substring(start, i).toLowerCase());
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 第二步:将token中的中文数字组合串解析为实际数值
|
||||||
|
// 如"十五"→15、"三十"→30、「一百二十」→120
|
||||||
|
java.util.List<Integer> parsedTokens = new java.util.ArrayList<>();
|
||||||
|
for (String tok : tokens) {
|
||||||
|
// 检查是否是多字符中文数字组合(如"十五")
|
||||||
|
boolean isMultiCharNum = false;
|
||||||
|
for (String t : tokens) {
|
||||||
|
if (t.equals(tok) && t.length() > 1 && t.chars().allMatch(c -> (c >= '\u4e00' && c <= '\u9fff') || (c >= '0' && c <= '9'))) {
|
||||||
|
// 所有字符都是中文数字或乘法器 → 解析中文数字组合
|
||||||
|
int val = evalChineseCompoundNum(tok, cnDigit, multipliers);
|
||||||
|
if (val >= 0) {
|
||||||
|
parsedTokens.add(val);
|
||||||
|
isMultiCharNum = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMultiCharNum) continue;
|
||||||
|
parsedTokens.add(null); // placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 第三步:遍历解析后的token序列,计算总值
|
||||||
|
for (int idx = 0; idx < tokens.size(); idx++) {
|
||||||
|
String tok = tokens.get(idx);
|
||||||
|
|
||||||
|
// 检查是否是中文复合数字(已通过parsedTokens解析为整数)
|
||||||
|
if (parsedTokens.get(idx) != null) {
|
||||||
|
int val = parsedTokens.get(idx);
|
||||||
|
if (val >= 0) {
|
||||||
|
pendingValue = val;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文数字映射
|
||||||
|
Integer digit = cnDigit.get(tok);
|
||||||
|
if (digit != null) {
|
||||||
|
pendingValue = digit;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 乘法器(十百千万)
|
||||||
|
if (multipliers.contains(tok)) {
|
||||||
|
if (pendingValue == 0) pendingValue = 1;
|
||||||
|
if (tok.equals("十") || tok.equals("拾")) pendingValue *= 10;
|
||||||
|
else if (tok.equals("百") || tok.equals("佰")) pendingValue *= 100;
|
||||||
|
else if (tok.equals("千") || tok.equals("仟")) pendingValue *= 1000;
|
||||||
|
else if (tok.equals("万")) pendingValue *= 10000;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间单位
|
||||||
|
Integer unitMul = null;
|
||||||
|
for (var entry : unitsList) {
|
||||||
|
if (entry.getKey().equals(tok)) {
|
||||||
|
unitMul = entry.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unitMul != null) {
|
||||||
|
if (pendingValue == 0) pendingValue = 1;
|
||||||
|
totalSeconds += pendingValue * unitMul;
|
||||||
|
pendingValue = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阿拉伯数字
|
||||||
|
try {
|
||||||
|
if (tok.contains(".")) pendingValue = (int) Double.parseDouble(tok);
|
||||||
|
else pendingValue = Integer.parseInt(tok);
|
||||||
|
continue;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// 英文数字单词(混合解析:如"壹零zero秒" → 100秒)
|
||||||
|
Integer enVal = enDigit.get(tok);
|
||||||
|
if (enVal != null) {
|
||||||
|
pendingValue = enVal;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 罗马数字(如"II分X秒"→2分10秒=130秒)
|
||||||
|
if (tok.length() > 0 && tok.chars().allMatch(c -> "ivxlcdm".indexOf(c) >= 0)) {
|
||||||
|
int romanVal = parseRoman(tok);
|
||||||
|
if (romanVal > 0) {
|
||||||
|
pendingValue = romanVal;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无法识别的token
|
||||||
|
pendingValue = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剩余的pendingValue当作秒
|
||||||
|
totalSeconds += pendingValue;
|
||||||
|
|
||||||
|
return totalSeconds > 0 ? totalSeconds : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 评估中文数字组合串(如"十五"→15、「一百二十三」→123) */
|
||||||
|
private int evalChineseCompoundNum(String s, java.util.Map<String, Integer> cnDigit, java.util.Set<String> multipliers) {
|
||||||
|
int total = 0, current = 0;
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
String ch = String.valueOf(c);
|
||||||
|
Integer d = cnDigit.get(ch);
|
||||||
|
if (d != null && d > 0) {
|
||||||
|
if (current > 0) current = current * 10 + d;
|
||||||
|
else current = d;
|
||||||
|
} else if (d != null && d == 0) {
|
||||||
|
// zero - skip
|
||||||
|
} else if (multipliers.contains(ch)) {
|
||||||
|
if (ch.equals("十") || ch.equals("拾")) current = (current == 0 ? 1 : current) * 10;
|
||||||
|
else if (ch.equals("百") || ch.equals("佰")) current = (current == 0 ? 1 : current) * 100;
|
||||||
|
else if (ch.equals("千") || ch.equals("仟")) current = (current == 0 ? 1 : current) * 1000;
|
||||||
|
else if (ch.equals("万")) current = (current == 0 ? 1 : current) * 10000;
|
||||||
|
total += current;
|
||||||
|
current = 0;
|
||||||
|
} else {
|
||||||
|
// non-digit, non-multiplier character → not a pure Chinese number
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total += current;
|
||||||
|
return total > 0 ? total : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析英文时间字符串,返回秒数或-1(无法解析) */
|
||||||
|
private int parseEnglishTime(String s) {
|
||||||
|
// 英文数字映射(1-90, 100, 1000)
|
||||||
|
java.util.Map<String, Integer> en = new java.util.LinkedHashMap<>();
|
||||||
|
en.put("zero", 0); en.put("one", 1); en.put("two", 2); en.put("three", 3); en.put("four", 4);
|
||||||
|
en.put("five", 5); en.put("six", 6); en.put("seven", 7); en.put("eight", 8); en.put("nine", 9);
|
||||||
|
en.put("ten", 10); en.put("eleven", 11); en.put("twelve", 12); en.put("thirteen", 13);
|
||||||
|
en.put("fourteen", 14); en.put("fifteen", 15); en.put("sixteen", 16); en.put("seventeen", 17);
|
||||||
|
en.put("eighteen", 18); en.put("nineteen", 19); en.put("twenty", 20);
|
||||||
|
en.put("thirty", 30); en.put("forty", 40); en.put("fifty", 50); en.put("sixty", 60);
|
||||||
|
en.put("seventy", 70); en.put("eighty", 80); en.put("ninety", 90);
|
||||||
|
en.put("hundred", 100); en.put("thousand", 1000);
|
||||||
|
|
||||||
|
// 单位倍率
|
||||||
|
java.util.Map<String, Integer> units = new java.util.LinkedHashMap<>();
|
||||||
|
units.put("second", 1); units.put("seconds", 1); units.put("sec", 1); units.put("secs", 1);
|
||||||
|
units.put("second", 1); units.put("minute", 60); units.put("minutes", 60); units.put("min", 60); units.put("mins", 60);
|
||||||
|
units.put("hour", 3600); units.put("hours", 3600); units.put("hr", 3600); units.put("hrs", 3600);
|
||||||
|
// 单字母缩写
|
||||||
|
units.put("s", 1); units.put("m", 60); units.put("h", 3600);
|
||||||
|
|
||||||
|
// 按空格和逗号分割
|
||||||
|
String[] words = s.split("[\\s,]+");
|
||||||
|
int totalSeconds = 0;
|
||||||
|
int currentNum = 0;
|
||||||
|
boolean hasUnit = false;
|
||||||
|
|
||||||
|
for (String word : words) {
|
||||||
|
if (word.isEmpty()) continue;
|
||||||
|
|
||||||
|
// 1. 检查是否是单位(可能紧跟数字,如"2min"→拆分为"2"和"min",这里处理不含数字的情况)
|
||||||
|
// 由于已经按空格分割了,单位应该是独立的word
|
||||||
|
Integer unitMul = units.get(word);
|
||||||
|
if (unitMul != null) {
|
||||||
|
if (currentNum == 0) currentNum = 1;
|
||||||
|
totalSeconds += currentNum * unitMul;
|
||||||
|
currentNum = 0;
|
||||||
|
hasUnit = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否是阿拉伯数字
|
||||||
|
try {
|
||||||
|
// 可能是带单位的合并词如"2min"、"5h"
|
||||||
|
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(\\d+)([a-z]+)").matcher(word);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
currentNum = Integer.parseInt(matcher.group(1));
|
||||||
|
String unitStr = matcher.group(2);
|
||||||
|
Integer uMul = units.get(unitStr);
|
||||||
|
if (uMul != null) {
|
||||||
|
totalSeconds += currentNum * uMul;
|
||||||
|
currentNum = 0;
|
||||||
|
hasUnit = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentNum = Integer.parseInt(word);
|
||||||
|
continue;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// 3. 检查是否是英文数字单词
|
||||||
|
Integer numVal = en.get(word);
|
||||||
|
if (numVal != null) {
|
||||||
|
if (numVal >= 100) {
|
||||||
|
if (currentNum == 0) currentNum = 1;
|
||||||
|
currentNum *= numVal;
|
||||||
|
} else {
|
||||||
|
currentNum += numVal;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查连字符数字(twenty-one, thirty-five)
|
||||||
|
if (word.contains("-")) {
|
||||||
|
String[] parts = word.split("-");
|
||||||
|
int compound = 0;
|
||||||
|
for (String p : parts) {
|
||||||
|
Integer pv = en.get(p);
|
||||||
|
if (pv != null) compound += pv;
|
||||||
|
}
|
||||||
|
if (compound > 0) {
|
||||||
|
currentNum += compound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnit ? totalSeconds : -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ public class DatabaseManager {
|
|||||||
"TEXT DEFAULT ''");
|
"TEXT DEFAULT ''");
|
||||||
safeAdd(st, "last_login_ip",
|
safeAdd(st, "last_login_ip",
|
||||||
"TEXT DEFAULT ''");
|
"TEXT DEFAULT ''");
|
||||||
|
// UI偏好:1=CLI(默认), 0=GUI
|
||||||
|
safeAdd(st, "ui_mode",
|
||||||
|
"INTEGER DEFAULT 1");
|
||||||
|
|
||||||
// ===== 背包备份表:安全迁移 =====
|
// ===== 背包备份表:安全迁移 =====
|
||||||
// 检查旧表是否存在(用 inventory_data 列判断)
|
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||||
@@ -255,6 +258,30 @@ public class DatabaseManager {
|
|||||||
+ ")");
|
+ ")");
|
||||||
safeAdd(st, "menu_snowball",
|
safeAdd(st, "menu_snowball",
|
||||||
"INTEGER DEFAULT 1");
|
"INTEGER DEFAULT 1");
|
||||||
|
|
||||||
|
// Home传送点表 — 只在不存在时创建,不删除已有数据
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS homes ("
|
||||||
|
+ "player_name TEXT NOT NULL,"
|
||||||
|
+ "home_name TEXT NOT NULL,"
|
||||||
|
+ "world TEXT NOT NULL,"
|
||||||
|
+ "x REAL NOT NULL,"
|
||||||
|
+ "y REAL NOT NULL,"
|
||||||
|
+ "z REAL NOT NULL,"
|
||||||
|
+ "yaw REAL DEFAULT 0,"
|
||||||
|
+ "pitch REAL DEFAULT 0,"
|
||||||
|
+ "created_at INTEGER DEFAULT 0,"
|
||||||
|
+ "PRIMARY KEY(player_name, home_name)"
|
||||||
|
+ ")");
|
||||||
|
safeAdd(st, "menu_snowball",
|
||||||
|
"INTEGER DEFAULT 1");
|
||||||
|
|
||||||
|
// 传送请求表
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS teleport_requests ("
|
||||||
|
+ "sender TEXT NOT NULL,"
|
||||||
|
+ "receiver TEXT NOT NULL,"
|
||||||
|
+ "type TEXT NOT NULL DEFAULT 'tpa',"
|
||||||
|
+ "timestamp INTEGER NOT NULL DEFAULT 0"
|
||||||
|
+ ")");
|
||||||
st.close();
|
st.close();
|
||||||
logger.info("[Sdf1_login] 数据库初始化完成");
|
logger.info("[Sdf1_login] 数据库初始化完成");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -274,6 +301,56 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 自动续费偏好 ====================
|
||||||
|
|
||||||
|
/** 获取玩家的自动续费偏好 */
|
||||||
|
public boolean getPlayerAutoRenew(String player, String group) {
|
||||||
|
try {
|
||||||
|
Statement st = db.createStatement();
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||||
|
String sql = "SELECT auto_renew FROM player_auto_renew WHERE player_name = ? AND group_name = ?";
|
||||||
|
PreparedStatement ps = db.prepareStatement(sql);
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, group);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) return rs.getInt("auto_renew") == 1;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置玩家的自动续费偏好 */
|
||||||
|
public void setPlayerAutoRenew(String player, String group, boolean value) {
|
||||||
|
try {
|
||||||
|
Statement st = db.createStatement();
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||||
|
String sql = "INSERT OR REPLACE INTO player_auto_renew (player_name, group_name, auto_renew) VALUES (?, ?, ?)";
|
||||||
|
PreparedStatement ps = db.prepareStatement(sql);
|
||||||
|
ps.setString(1, player);
|
||||||
|
ps.setString(2, group);
|
||||||
|
ps.setInt(3, value ? 1 : 0);
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取玩家所有自动续费开启的组 */
|
||||||
|
public List<Map<String, Object>> getAutoRenewGroups(String player) {
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Statement st = db.createStatement();
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||||
|
String sql = "SELECT group_name FROM player_auto_renew WHERE player_name = ? AND auto_renew = 1";
|
||||||
|
PreparedStatement ps = db.prepareStatement(sql);
|
||||||
|
ps.setString(1, player);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, Object> row = new HashMap<>();
|
||||||
|
row.put("group_name", rs.getString("group_name"));
|
||||||
|
result.add(row);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
try {
|
try {
|
||||||
if (db != null && !db.isClosed())
|
if (db != null && !db.isClosed())
|
||||||
@@ -281,6 +358,51 @@ public class DatabaseManager {
|
|||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() {
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UI偏好 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家UI偏好
|
||||||
|
* @return 1=CLI(默认), 0=GUI
|
||||||
|
*/
|
||||||
|
public int getUiMode(String playerName) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT ui_mode FROM users WHERE player_name=?");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
int mode = 1; // 默认CLI
|
||||||
|
if (rs.next()) {
|
||||||
|
mode = rs.getInt("ui_mode");
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return mode;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return 1; // 默认CLI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置玩家UI偏好
|
||||||
|
* @param mode 1=CLI, 0=GUI
|
||||||
|
*/
|
||||||
|
public void setUiMode(String playerName, int mode) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"UPDATE users SET ui_mode=? WHERE player_name=?");
|
||||||
|
ps.setInt(1, mode);
|
||||||
|
ps.setString(2, playerName);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// 静默
|
||||||
|
}
|
||||||
|
}
|
||||||
// ==================== 菜单图标 ====================
|
// ==================== 菜单图标 ====================
|
||||||
|
|
||||||
public void saveMenuIcon(String name,
|
public void saveMenuIcon(String name,
|
||||||
@@ -438,9 +560,11 @@ public class DatabaseManager {
|
|||||||
? String.valueOf(ip) : null;
|
? String.valueOf(ip) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkPasswordOrTemp(
|
/**
|
||||||
String name, String hash) {
|
* 验证密码(仅主密码)
|
||||||
// 先检查主密码
|
* @return true if matched
|
||||||
|
*/
|
||||||
|
public boolean checkPassword(String name, String hash) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = db.prepareStatement(
|
PreparedStatement ps = db.prepareStatement(
|
||||||
"SELECT password_hash "
|
"SELECT password_hash "
|
||||||
@@ -453,8 +577,7 @@ public class DatabaseManager {
|
|||||||
rs.getString("password_hash");
|
rs.getString("password_hash");
|
||||||
rs.close();
|
rs.close();
|
||||||
ps.close();
|
ps.close();
|
||||||
if (hash.equals(mainHash))
|
return hash.equals(mainHash);
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
rs.close();
|
rs.close();
|
||||||
ps.close();
|
ps.close();
|
||||||
@@ -462,47 +585,51 @@ public class DatabaseManager {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
// 再检查临时密码
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码(主密码优先,不通过再查临时密码)
|
||||||
|
* 注意: 传入的 hash 必须是主密码的 hash
|
||||||
|
* @return "main" | "temp" | null
|
||||||
|
*/
|
||||||
|
public String checkPasswordWithFallback(String name, String mainHash) {
|
||||||
|
// 先检查主密码
|
||||||
|
if (checkPassword(name, mainHash)) {
|
||||||
|
return "main";
|
||||||
|
}
|
||||||
|
// 主密码不匹配,检查临时密码
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps2 = db.prepareStatement(
|
PreparedStatement ps = db.prepareStatement(
|
||||||
"SELECT temp_password, "
|
"SELECT temp_password, "
|
||||||
+ "temp_pw_expire, "
|
+ "temp_pw_expire, "
|
||||||
+ "temp_pw_used "
|
+ "temp_pw_used "
|
||||||
+ "FROM users "
|
+ "FROM users "
|
||||||
+ "WHERE player_name=?");
|
+ "WHERE player_name=?");
|
||||||
ps2.setString(1, name);
|
ps.setString(1, name);
|
||||||
ResultSet rs2 = ps2.executeQuery();
|
ResultSet rs = ps.executeQuery();
|
||||||
if (rs2.next()) {
|
if (rs.next()) {
|
||||||
String tempHash =
|
String tempHash = rs.getString("temp_password");
|
||||||
rs2.getString(
|
long expire = rs.getLong("temp_pw_expire");
|
||||||
"temp_password");
|
int used = rs.getInt("temp_pw_used");
|
||||||
long expire =
|
rs.close();
|
||||||
rs2.getLong(
|
ps.close();
|
||||||
"temp_pw_expire");
|
|
||||||
int used =
|
|
||||||
rs2.getInt(
|
|
||||||
"temp_pw_used");
|
|
||||||
rs2.close();
|
|
||||||
ps2.close();
|
|
||||||
if (tempHash != null
|
if (tempHash != null
|
||||||
&& !tempHash.isEmpty()
|
&& !tempHash.isEmpty()
|
||||||
&& hash.equals(tempHash)
|
&& mainHash.equals(tempHash)
|
||||||
&& System.currentTimeMillis()
|
&& System.currentTimeMillis() < expire
|
||||||
< expire
|
|
||||||
&& used != 1) {
|
&& used != 1) {
|
||||||
return true;
|
return "temp";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rs2.close();
|
rs.close();
|
||||||
ps2.close();
|
ps.close();
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Map<String, Object> getUser(String name) {
|
public Map<String, Object> getUser(String name) {
|
||||||
Map<String, Object> r =
|
Map<String, Object> r =
|
||||||
new LinkedHashMap<>();
|
new LinkedHashMap<>();
|
||||||
@@ -569,32 +696,6 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkPassword(String name,
|
|
||||||
String hash) {
|
|
||||||
try {
|
|
||||||
PreparedStatement ps = db.prepareStatement(
|
|
||||||
"SELECT password_hash "
|
|
||||||
+ "FROM users "
|
|
||||||
+ "WHERE player_name=?");
|
|
||||||
ps.setString(1, name);
|
|
||||||
ResultSet rs = ps.executeQuery();
|
|
||||||
if (rs.next()) {
|
|
||||||
String stored =
|
|
||||||
rs.getString("password_hash");
|
|
||||||
rs.close();
|
|
||||||
ps.close();
|
|
||||||
return hash.equals(stored);
|
|
||||||
}
|
|
||||||
rs.close();
|
|
||||||
ps.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isUsingTempPassword(String name) {
|
public boolean isUsingTempPassword(String name) {
|
||||||
String h = (String) getField(name,
|
String h = (String) getField(name,
|
||||||
"temp_password");
|
"temp_password");
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 死亡报告系统
|
||||||
|
* - 记录玩家死亡坐标(一次性,新死亡覆盖)
|
||||||
|
* - 死亡后发送包含坐标的消息
|
||||||
|
*/
|
||||||
|
public class DeathReport implements Listener {
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
// 内存缓存: player_name → DeathData
|
||||||
|
private final ConcurrentHashMap<String, DeathData> deathReports = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static class DeathData {
|
||||||
|
public final String playerName;
|
||||||
|
public String worldName;
|
||||||
|
public final double x, y, z;
|
||||||
|
public long deathTime;
|
||||||
|
public final UUID playerId;
|
||||||
|
|
||||||
|
DeathData(String playerName, UUID playerId, Location loc) {
|
||||||
|
this.playerName = playerName;
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.worldName = loc.getWorld() != null ? loc.getWorld().getName() : "world";
|
||||||
|
this.x = loc.getX();
|
||||||
|
this.y = loc.getY();
|
||||||
|
this.z = loc.getZ();
|
||||||
|
this.deathTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeathReport(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
initTable();
|
||||||
|
loadAllFromDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 事件监听 ====================
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Location deathLoc = player.getLocation();
|
||||||
|
if (deathLoc == null) return;
|
||||||
|
|
||||||
|
DeathData data = new DeathData(player.getName(), player.getUniqueId(), deathLoc);
|
||||||
|
deathReports.put(player.getName(), data);
|
||||||
|
saveToDB(data);
|
||||||
|
|
||||||
|
// 发送死亡报告消息,带可点击的返回按钮
|
||||||
|
String timeStr = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(data.deathTime));
|
||||||
|
|
||||||
|
Component header = Component.text("§c✦ 你已死亡 ✦ ").append(
|
||||||
|
Component.text("世界: §f" + data.worldName + " §| §7坐标: §e" +
|
||||||
|
String.format("%.1f, %.1f, %.1f", data.x, data.y, data.z) + " §7时间: §f" + timeStr)
|
||||||
|
);
|
||||||
|
|
||||||
|
// "返回死亡地点" 按钮 - 通过 /back 命令
|
||||||
|
Component backButton = Component.text("§e[点击返回死亡地点] ")
|
||||||
|
.clickEvent(ClickEvent.runCommand("/back"))
|
||||||
|
.hoverEvent(HoverEvent.showText(Component.text("§7传送到你死亡的位置")));
|
||||||
|
|
||||||
|
player.sendMessage(header);
|
||||||
|
player.sendMessage(backButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 查询/获取 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家的最新死亡坐标(如果有的话)
|
||||||
|
*/
|
||||||
|
public DeathData getDeathReport(String playerName) {
|
||||||
|
return deathReports.get(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除玩家的死亡报告
|
||||||
|
*/
|
||||||
|
public void clearDeathReport(String playerName) {
|
||||||
|
deathReports.remove(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 数据库操作 ====================
|
||||||
|
|
||||||
|
private void loadAllFromDB() {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"SELECT player_name, player_uuid, world, x, y, z, death_time FROM death_reports");
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
DeathData data = new DeathData(
|
||||||
|
rs.getString("player_name"),
|
||||||
|
UUID.fromString(rs.getString("player_uuid")),
|
||||||
|
new Location(null, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"))
|
||||||
|
);
|
||||||
|
data.worldName = rs.getString("world");
|
||||||
|
data.deathTime = rs.getLong("death_time");
|
||||||
|
deathReports.put(data.playerName, data);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// 表可能不存在,忽略(会在第一次死亡时自动创建)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveToDB(DeathData data) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"INSERT OR REPLACE INTO death_reports (player_name, player_uuid, world, x, y, z, death_time) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
ps.setString(1, data.playerName);
|
||||||
|
ps.setString(2, data.playerId.toString());
|
||||||
|
ps.setString(3, data.worldName);
|
||||||
|
ps.setDouble(4, data.x);
|
||||||
|
ps.setDouble(5, data.y);
|
||||||
|
ps.setDouble(6, data.z);
|
||||||
|
ps.setLong(7, data.deathTime);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[死亡报告] 保存失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistAll() {
|
||||||
|
for (DeathData data : deathReports.values()) {
|
||||||
|
saveToDB(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
public void initTable() {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
java.sql.Statement st = conn.createStatement();
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS death_reports ("
|
||||||
|
+ "player_name TEXT PRIMARY KEY, "
|
||||||
|
+ "player_uuid TEXT NOT NULL, "
|
||||||
|
+ "world TEXT, "
|
||||||
|
+ "x REAL, y REAL, z REAL, "
|
||||||
|
+ "death_time LONG)");
|
||||||
|
st.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[死亡报告] 建表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,8 @@ public class GUIManager implements Listener {
|
|||||||
"§6§l邀请数据";
|
"§6§l邀请数据";
|
||||||
public static final String T_USER_MGMT =
|
public static final String T_USER_MGMT =
|
||||||
"§6§l用户管理";
|
"§6§l用户管理";
|
||||||
|
public static final String T_USER_GROUP =
|
||||||
|
"§6§l用户组管理";
|
||||||
public static final String T_TASK_CENTER =
|
public static final String T_TASK_CENTER =
|
||||||
"§6§l任务中心";
|
"§6§l任务中心";
|
||||||
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
||||||
@@ -193,18 +195,7 @@ public class GUIManager implements Listener {
|
|||||||
}
|
}
|
||||||
// 在 openMain 方法中,Row3 区域加:
|
// 在 openMain 方法中,Row3 区域加:
|
||||||
|
|
||||||
// ★ 余额查询 - 所有人都能看 ★
|
// 余额查询已移除Vault支持,仅显示债券余额
|
||||||
double myBal = 0;
|
|
||||||
try {
|
|
||||||
var reg = plugin.getServer()
|
|
||||||
.getServicesManager()
|
|
||||||
.getRegistration(
|
|
||||||
net.milkbowl.vault.economy
|
|
||||||
.Economy.class);
|
|
||||||
if (reg != null)
|
|
||||||
myBal = reg.getProvider()
|
|
||||||
.getBalance(p);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
int myBonds = plugin.getBonds().getBonds(p.getName());
|
int myBonds = plugin.getBonds().getBonds(p.getName());
|
||||||
g.setItem(32, mkItem(Material.EMERALD,
|
g.setItem(32, mkItem(Material.EMERALD,
|
||||||
"§b§l我的钱包",
|
"§b§l我的钱包",
|
||||||
@@ -217,6 +208,20 @@ public class GUIManager implements Listener {
|
|||||||
"§7给钱、扣钱"));
|
"§7给钱、扣钱"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 领地系统 - 39号位(读取用户偏好)
|
||||||
|
int landUiMode = 1;
|
||||||
|
try {
|
||||||
|
landUiMode = plugin.getDb()
|
||||||
|
.getUiMode(p.getName());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
g.setItem(39, mkItem(Material.GRASS_BLOCK,
|
||||||
|
"§a§l领地系统",
|
||||||
|
"§7管理你的领地和权限",
|
||||||
|
"",
|
||||||
|
landUiMode == 0
|
||||||
|
? "§e点击打开GUI"
|
||||||
|
: "§e点击打开CLI"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===== Row0(0-8): 自定义菜单项 =====
|
// ===== Row0(0-8): 自定义菜单项 =====
|
||||||
@@ -327,28 +332,19 @@ public class GUIManager implements Listener {
|
|||||||
public void openBalanceOps(Player p) {
|
public void openBalanceOps(Player p) {
|
||||||
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
|
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
|
||||||
fillBg(g);
|
fillBg(g);
|
||||||
|
// Vault经济支持已移除,仅显示债券余额
|
||||||
net.milkbowl.vault.economy.Economy eco = null;
|
|
||||||
try {
|
|
||||||
var reg = plugin.getServer().getServicesManager()
|
|
||||||
.getRegistration(net.milkbowl.vault.economy.Economy.class);
|
|
||||||
if (reg != null) eco = reg.getProvider();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
int slot = 0;
|
int slot = 0;
|
||||||
for (Player op : Bukkit.getOnlinePlayers()) {
|
for (Player op : Bukkit.getOnlinePlayers()) {
|
||||||
if (slot >= 45) break;
|
if (slot >= 45) break;
|
||||||
double bal = 0;
|
|
||||||
if (eco != null) bal = eco.getBalance(op);
|
|
||||||
boolean isSelf = op.getName().equals(p.getName());
|
boolean isSelf = op.getName().equals(p.getName());
|
||||||
int bondBal = plugin.getBonds().getBonds(op.getName());
|
int bondBal = plugin.getBonds().getBonds(op.getName());
|
||||||
g.setItem(slot, mkItem(
|
g.setItem(slot, mkItem(
|
||||||
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
|
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
|
||||||
"§e" + op.getName(),
|
"§e" + op.getName(),
|
||||||
"§7金币: §a$" + String.format("%.2f", bal),
|
|
||||||
"§7债券: §e" + bondBal + " §6枚",
|
"§7债券: §e" + bondBal + " §6枚",
|
||||||
"",
|
"",
|
||||||
"§a左键经济(±) §6右键债券(±)"));
|
"§a仅显示债券余额(Vault经济支持已移除)"));
|
||||||
slot++;
|
slot++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +395,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", "");
|
||||||
@@ -406,21 +404,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文件不存在: "
|
|
||||||
+ fileName);
|
plugin.getLogger().info(
|
||||||
return;
|
"[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 {
|
try {
|
||||||
List<String> lines =
|
lines = java.nio.file.Files
|
||||||
java.nio.file.Files
|
.readAllLines(f.toPath(),
|
||||||
.readAllLines(
|
|
||||||
f.toPath(),
|
|
||||||
java.nio.charset
|
java.nio.charset
|
||||||
.StandardCharsets
|
.StandardCharsets
|
||||||
.UTF_8);
|
.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;
|
||||||
|
}
|
||||||
List<String[]> items =
|
List<String[]> items =
|
||||||
new ArrayList<>();
|
new ArrayList<>();
|
||||||
StringBuilder buf =
|
StringBuilder buf =
|
||||||
@@ -484,11 +614,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,
|
||||||
@@ -609,78 +734,12 @@ public class GUIManager implements Listener {
|
|||||||
Collections.shuffle(candidates);
|
Collections.shuffle(candidates);
|
||||||
|
|
||||||
// 3. 主线程分批检测,每tick处理30个坐标
|
// 3. 主线程分批检测,每tick处理30个坐标
|
||||||
final int[] idx = {0};
|
new BiomeScanTask(plugin, p, category, candidates).runTaskTimer(plugin, 0L, 1L);
|
||||||
|
|
||||||
new BukkitRunnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
World w = p.getWorld();
|
|
||||||
int end = Math.min(
|
|
||||||
idx[0] + BIOME_BATCH,
|
|
||||||
candidates.size());
|
|
||||||
|
|
||||||
for (int i = idx[0]; i < end; i++) {
|
|
||||||
int x = candidates.get(i)[0];
|
|
||||||
int z = candidates.get(i)[1];
|
|
||||||
|
|
||||||
// 跳过未加载区块
|
|
||||||
if (!w.isChunkLoaded(x >> 4, z >> 4))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// 获取地表高度
|
|
||||||
int surfaceY =
|
|
||||||
w.getHighestBlockYAt(x, z);
|
|
||||||
|
|
||||||
// 在地表Y取群系(避免地下误判)
|
|
||||||
String key = w.getBlockAt(
|
|
||||||
x, surfaceY, z)
|
|
||||||
.getBiome()
|
|
||||||
.getKey().toString();
|
|
||||||
String cat =
|
|
||||||
mapBiomeCategory(
|
|
||||||
key, w.getName());
|
|
||||||
|
|
||||||
if (category.equals(cat)) {
|
|
||||||
this.cancel();
|
|
||||||
Bukkit.getScheduler()
|
|
||||||
.runTask(plugin, () -> {
|
|
||||||
Location tp =
|
|
||||||
new Location(
|
|
||||||
w,
|
|
||||||
x + 0.5,
|
|
||||||
surfaceY + 1,
|
|
||||||
z + 0.5);
|
|
||||||
p.teleport(tp);
|
|
||||||
p.sendMessage(
|
|
||||||
"§a已传送到 "
|
|
||||||
+ category
|
|
||||||
+ " ("
|
|
||||||
+ x + ","
|
|
||||||
+ (surfaceY + 1)
|
|
||||||
+ ","
|
|
||||||
+ z + ")");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idx[0] = end;
|
|
||||||
if (idx[0] >= candidates.size()) {
|
|
||||||
this.cancel();
|
|
||||||
Bukkit.getScheduler()
|
|
||||||
.runTask(plugin, () ->
|
|
||||||
p.sendMessage(
|
|
||||||
"§c附近未找到 "
|
|
||||||
+ category
|
|
||||||
+ ",请换个位置再试"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.runTaskTimer(plugin, 0L, 1L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private String mapBiomeCategory(
|
private static String mapBiomeCategory(
|
||||||
String biome, String worldName) {
|
String biome, String worldName) {
|
||||||
if (biome == null) return "平原";
|
if (biome == null) return "平原";
|
||||||
String b = biome.toLowerCase();
|
String b = biome.toLowerCase();
|
||||||
@@ -863,7 +922,7 @@ public class GUIManager implements Listener {
|
|||||||
// ===== 余额操作 =====
|
// ===== 余额操作 =====
|
||||||
if ("§d§l余额操作".equals(title)) {
|
if ("§d§l余额操作".equals(title)) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
if (raw == 49) { openAdmin(p); return; }
|
if (raw == 49) { plugin.openMyWallet(p); return; }
|
||||||
if (raw < 0 || raw >= 45) return;
|
if (raw < 0 || raw >= 45) return;
|
||||||
ItemStack item = event.getView()
|
ItemStack item = event.getView()
|
||||||
.getTopInventory().getItem(raw);
|
.getTopInventory().getItem(raw);
|
||||||
@@ -927,6 +986,13 @@ public class GUIManager implements Listener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 用户组管理 =====
|
||||||
|
if (T_USER_GROUP.equals(title)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
handleUserGroupClick(p, raw, event.isLeftClick(), event.isRightClick(), event.isShiftClick());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 打包确认 =====
|
// ===== 打包确认 =====
|
||||||
if ("§6§l选择包装方式".equals(title)) {
|
if ("§6§l选择包装方式".equals(title)) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@@ -2711,6 +2777,279 @@ public class GUIManager implements Listener {
|
|||||||
plugin.getConfig2().adminTag);
|
plugin.getConfig2().adminTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 用户组管理GUI ====================
|
||||||
|
|
||||||
|
/** 打开用户组管理主界面 */
|
||||||
|
public void openUserGroup(Player p) {
|
||||||
|
UserGroupManager ugm = plugin.getUserGroup();
|
||||||
|
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||||
|
|
||||||
|
Inventory g = Bukkit.createInventory(null, 54, T_USER_GROUP);
|
||||||
|
|
||||||
|
int bonds = plugin.getBonds().getBonds(p.getName());
|
||||||
|
g.setItem(4, mkItem(Material.EMERALD, "§b§l我的债券: " + bonds + " 张", "§7用于购买/续费用户组"));
|
||||||
|
|
||||||
|
// Row1 (9-17): 可购买的用户组
|
||||||
|
List<UserGroupManager.UserGroupConfig> allGroups = ugm.getAllGroups();
|
||||||
|
int slot = 9;
|
||||||
|
for (UserGroupManager.UserGroupConfig cfg : allGroups) {
|
||||||
|
if (slot > 17) break;
|
||||||
|
if (cfg.joinPrice <= 0) continue; // 不开放付费的组不显示
|
||||||
|
|
||||||
|
boolean isMember = ugm.isPlayerInGroup(p.getName(), cfg.name);
|
||||||
|
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||||
|
|
||||||
|
if (isMember) {
|
||||||
|
// 已加入 → 显示在 Row2
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Material mat = isMember ? Material.LIME_DYE : Material.WHITE_DYE;
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
lore.add("§7价格: §e" + cfg.joinPrice + " 张债券");
|
||||||
|
if (cfg.durationMinutes > 0) {
|
||||||
|
long days = cfg.durationMinutes / 1440;
|
||||||
|
long hours = (cfg.durationMinutes % 1440) / 60;
|
||||||
|
String dur = days > 0 ? days + "天" : hours + "小时";
|
||||||
|
lore.add("§7有效期: §e" + dur);
|
||||||
|
} else {
|
||||||
|
lore.add("§7有效期: §a永久");
|
||||||
|
}
|
||||||
|
if (cfg.renewPrice > 0) {
|
||||||
|
lore.add("§7续费价格: §e" + cfg.renewPrice + " 张债券");
|
||||||
|
}
|
||||||
|
lore.add("");
|
||||||
|
lore.add("§a左键: 购买给自己");
|
||||||
|
lore.add("§b右键: 赠送给他人");
|
||||||
|
|
||||||
|
g.setItem(slot, mkItem(mat, cfg.displayColor + "§l" + displayName + " §7(购买)", lore.toArray(new String[0])));
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row2 (18-26): 我的用户组
|
||||||
|
slot = 18;
|
||||||
|
List<Map<String, Object>> myGroups = ugm.getPlayerGroups(p.getName());
|
||||||
|
for (Map<String, Object> grp : myGroups) {
|
||||||
|
if (slot > 26) break;
|
||||||
|
String grpName = String.valueOf(grp.get("group_name"));
|
||||||
|
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||||
|
if (cfg == null) continue;
|
||||||
|
|
||||||
|
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||||
|
long expiry = ((Number) grp.getOrDefault("expiry_time", 0)).longValue();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
List<String> lore = new ArrayList<>();
|
||||||
|
if (expiry > 0) {
|
||||||
|
if (expiry <= now) {
|
||||||
|
lore.add("§c已过期");
|
||||||
|
} else {
|
||||||
|
long remainMs = expiry - now;
|
||||||
|
long remainMin = remainMs / 60000;
|
||||||
|
long rDays = remainMin / 1440;
|
||||||
|
long rHours = (remainMin % 1440) / 60;
|
||||||
|
lore.add("§7剩余: §e" + (rDays > 0 ? rDays + "天" : rHours + "小时"));
|
||||||
|
lore.add("§7到期: §7" + new java.text.SimpleDateFormat("MM-dd HH:mm").format(new java.util.Date(expiry)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lore.add("§7有效期: §a永久");
|
||||||
|
}
|
||||||
|
// 自动续费状态
|
||||||
|
boolean autoRenew = getAutoRenew(p.getName(), grpName);
|
||||||
|
lore.add("§7自动续费: " + (autoRenew ? "§a开启" : "§c关闭"));
|
||||||
|
lore.add("");
|
||||||
|
lore.add("§a左键: 续费");
|
||||||
|
lore.add("§b右键: 赠送给他人");
|
||||||
|
lore.add("§eShift+右键: " + (autoRenew ? "§c关闭" : "§a开启") + "自动续费");
|
||||||
|
|
||||||
|
g.setItem(slot, mkItem(Material.LIME_DYE, cfg.displayColor + "§l" + displayName, lore.toArray(new String[0])));
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row4 (40): 赠送入口
|
||||||
|
g.setItem(40, mkItem(Material.SPECTRAL_ARROW, "§e§l赠予用户组", "§7为其他玩家购买/续费用户组"));
|
||||||
|
|
||||||
|
// Row5 (49): 返回
|
||||||
|
g.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||||
|
|
||||||
|
fillBg(g);
|
||||||
|
p.openInventory(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理用户组GUI点击 */
|
||||||
|
public boolean handleUserGroupClick(Player p, int raw, boolean left, boolean right, boolean shift) {
|
||||||
|
UserGroupManager ugm = plugin.getUserGroup();
|
||||||
|
if (ugm == null) return false;
|
||||||
|
|
||||||
|
if (raw == 49) { openMain(p); return true; } // 返回
|
||||||
|
|
||||||
|
// 赠送按钮
|
||||||
|
if (raw == 40) {
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||||
|
plugin.getCDK().requestInput(p, "gift_target", "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row1: 可购买的组 (9-17)
|
||||||
|
if (raw >= 9 && raw <= 17) {
|
||||||
|
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||||
|
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||||
|
String grpName = extractGroupNameFromItem(item);
|
||||||
|
if (grpName == null) return false;
|
||||||
|
|
||||||
|
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||||
|
if (cfg == null) return false;
|
||||||
|
|
||||||
|
if (left && !right) {
|
||||||
|
// 购买给自己
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§7正在购买 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||||
|
String err = ugm.joinGroupByPrice(p.getName(), cfg.name);
|
||||||
|
if (err != null) {
|
||||||
|
p.sendMessage("§c购买失败: " + err);
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§a§l购买成功! §7你已加入用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (right && !left) {
|
||||||
|
// 赠送给他人
|
||||||
|
p.closeInventory();
|
||||||
|
// 记住赠送的组名
|
||||||
|
pendingGiftGroup.put(p.getName(), grpName);
|
||||||
|
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||||
|
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row2: 我的组 (18-26)
|
||||||
|
if (raw >= 18 && raw <= 26) {
|
||||||
|
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||||
|
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||||
|
String grpName = extractGroupNameFromItem(item);
|
||||||
|
if (grpName == null) return false;
|
||||||
|
|
||||||
|
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||||
|
if (cfg == null) return false;
|
||||||
|
|
||||||
|
if (shift && right) {
|
||||||
|
// Shift+右键: 切换自动续费
|
||||||
|
boolean current = getAutoRenew(p.getName(), grpName);
|
||||||
|
setAutoRenew(p.getName(), grpName, !current);
|
||||||
|
p.sendMessage("§a自动续费已" + (!current ? "开启" : "关闭") + ": §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||||
|
openUserGroup(p); // 刷新界面
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left && !right) {
|
||||||
|
// 续费
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§7正在续费 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||||
|
String err = ugm.renewGroup(p.getName(), cfg.name);
|
||||||
|
if (err != null) {
|
||||||
|
p.sendMessage("§c续费失败: " + err);
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§a§l续费成功! §7用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7已续期");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (right && !left && !shift) {
|
||||||
|
// 赠送给他人
|
||||||
|
p.closeInventory();
|
||||||
|
pendingGiftGroup.put(p.getName(), grpName);
|
||||||
|
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||||
|
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理赠予玩家名输入 */
|
||||||
|
public void handleGiftTargetInput(Player p, String targetName) {
|
||||||
|
String grpName = pendingGiftGroup.remove(p.getName());
|
||||||
|
if (grpName == null) return;
|
||||||
|
|
||||||
|
UserGroupManager ugm = plugin.getUserGroup();
|
||||||
|
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||||
|
|
||||||
|
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||||
|
if (cfg == null) { p.sendMessage("§c用户组不存在: " + grpName); return; }
|
||||||
|
|
||||||
|
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||||
|
|
||||||
|
// 检查目标玩家是否存在
|
||||||
|
DatabaseManager dbMgr = plugin.getDb();
|
||||||
|
if (dbMgr != null && !dbMgr.userExists(targetName)) {
|
||||||
|
p.sendMessage("§c玩家 §e" + targetName + " §c不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 使用新的赠送方法:送礼人(p)付费,收礼人(targetName)免费获得
|
||||||
|
p.sendMessage("§7正在为 §e" + targetName + " §7购买用户组 §e" + displayName + " §7...");
|
||||||
|
String err = ugm.giftGroupToPlayer(p.getName(), targetName, cfg.name);
|
||||||
|
if (err != null) {
|
||||||
|
p.sendMessage("§c赠予失败: " + err);
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§a§l赠予成功!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 赠予流程:记录赠送的组名
|
||||||
|
private final Map<String, String> pendingGiftGroup = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// ★ 自动续费偏好(per-player)
|
||||||
|
private final Map<String, Boolean> autoRenewCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private boolean getAutoRenew(String player, String group) {
|
||||||
|
String key = player + "|" + group;
|
||||||
|
if (autoRenewCache.containsKey(key)) return autoRenewCache.get(key);
|
||||||
|
// 从数据库读取
|
||||||
|
boolean val = false;
|
||||||
|
try {
|
||||||
|
DatabaseManager dbMgr = plugin.getDb();
|
||||||
|
if (dbMgr != null) {
|
||||||
|
val = dbMgr.getPlayerAutoRenew(player, group);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
autoRenewCache.put(key, val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAutoRenew(String player, String group, boolean value) {
|
||||||
|
String key = player + "|" + group;
|
||||||
|
autoRenewCache.put(key, value);
|
||||||
|
try {
|
||||||
|
DatabaseManager dbMgr = plugin.getDb();
|
||||||
|
if (dbMgr != null) {
|
||||||
|
dbMgr.setPlayerAutoRenew(player, group, value);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从GUI物品名称中提取用户组名(去掉格式化前缀和后缀) */
|
||||||
|
private String extractGroupNameFromItem(ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) return null;
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta == null || meta.getDisplayName() == null) return null;
|
||||||
|
String name = meta.getDisplayName();
|
||||||
|
// 去掉所有§格式化代码
|
||||||
|
name = name.replaceAll("§[0-9a-fk-orA-FK-OR]", "");
|
||||||
|
// 去掉后缀 " (购买)"
|
||||||
|
name = name.replaceAll("\\s*\\(购买\\)$", "").trim();
|
||||||
|
// 匹配已知组名
|
||||||
|
UserGroupManager ugm = plugin.getUserGroup();
|
||||||
|
if (ugm == null) return null;
|
||||||
|
for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) {
|
||||||
|
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||||
|
if (name.equals(displayName) || name.equals(cfg.name)) {
|
||||||
|
return cfg.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void fillBg(Inventory inv) {
|
private void fillBg(Inventory inv) {
|
||||||
ItemStack glass = new ItemStack(
|
ItemStack glass = new ItemStack(
|
||||||
Material.GRAY_STAINED_GLASS_PANE);
|
Material.GRAY_STAINED_GLASS_PANE);
|
||||||
@@ -2774,4 +3113,55 @@ public class GUIManager implements Listener {
|
|||||||
}
|
}
|
||||||
return it;
|
return it;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 命名静态内部类替代匿名BukkitRunnable,修复Paper 26 ClassLoader兼容性 */
|
||||||
|
private static class BiomeScanTask extends BukkitRunnable {
|
||||||
|
private final Main plugin;
|
||||||
|
private final Player p;
|
||||||
|
private final String category;
|
||||||
|
private final java.util.List<int[]> candidates;
|
||||||
|
private int idx = 0;
|
||||||
|
|
||||||
|
BiomeScanTask(Main plugin, Player p, String category, java.util.List<int[]> candidates) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.p = p;
|
||||||
|
this.category = category;
|
||||||
|
this.candidates = candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
World w = p.getWorld();
|
||||||
|
int end = Math.min(idx + BIOME_BATCH, candidates.size());
|
||||||
|
|
||||||
|
for (int i = idx; i < end; i++) {
|
||||||
|
int x = candidates.get(i)[0];
|
||||||
|
int z = candidates.get(i)[1];
|
||||||
|
|
||||||
|
if (!w.isChunkLoaded(x >> 4, z >> 4)) continue;
|
||||||
|
|
||||||
|
int surfaceY = w.getHighestBlockYAt(x, z);
|
||||||
|
String key = w.getBlockAt(x, surfaceY, z)
|
||||||
|
.getBiome().getKey().toString();
|
||||||
|
String cat = mapBiomeCategory(key, w.getName());
|
||||||
|
|
||||||
|
if (category.equals(cat)) {
|
||||||
|
this.cancel();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Location tp = new Location(w, x + 0.5, surfaceY + 1, z + 0.5);
|
||||||
|
p.teleport(tp);
|
||||||
|
p.sendMessage("§a已传送到 " + category + " (" + x + "," + (surfaceY + 1) + "," + z + ")");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = end;
|
||||||
|
if (idx >= candidates.size()) {
|
||||||
|
this.cancel();
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
p.sendMessage("§c附近未找到 " + category + ",请换个位置再试"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package Sdf1_login;
|
package Sdf1_login;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Item;
|
import org.bukkit.entity.Item;
|
||||||
@@ -12,7 +11,6 @@ import org.bukkit.inventory.meta.ItemMeta;
|
|||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.sql.Date;
|
import java.sql.Date;
|
||||||
@@ -287,11 +285,8 @@ public class GarbageManager {
|
|||||||
.decode(base64);
|
.decode(base64);
|
||||||
String json = new String(bytes,
|
String json = new String(bytes,
|
||||||
StandardCharsets.UTF_8);
|
StandardCharsets.UTF_8);
|
||||||
Type type = new TypeToken<
|
|
||||||
Map<String, Object>>() {}
|
|
||||||
.getType();
|
|
||||||
Map<String, Object> map =
|
Map<String, Object> map =
|
||||||
new Gson().fromJson(json, type);
|
new Gson().fromJson(json, Map.class);
|
||||||
map = fixNumberTypes(map);
|
map = fixNumberTypes(map);
|
||||||
return ItemStack.deserialize(map);
|
return ItemStack.deserialize(map);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -425,7 +420,7 @@ public class GarbageManager {
|
|||||||
"§7已是最后一页"));
|
"§7已是最后一页"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 52 格:返回
|
// 52 格:返回主菜单
|
||||||
g.setItem(52, mk(Material.ARROW,
|
g.setItem(52, mk(Material.ARROW,
|
||||||
"§7返回",
|
"§7返回",
|
||||||
"§7返回主菜单"));
|
"§7返回主菜单"));
|
||||||
|
|||||||
@@ -194,28 +194,21 @@ public class GiftManager {
|
|||||||
giveItem(p, Material.WOODEN_AXE);
|
giveItem(p, Material.WOODEN_AXE);
|
||||||
giveItem(p, Material.WOODEN_SHOVEL);
|
giveItem(p, Material.WOODEN_SHOVEL);
|
||||||
} else if (stage == 2) {
|
} else if (stage == 2) {
|
||||||
if (plugin.getEconomy() != null) {
|
// 游戏币改为债券
|
||||||
double before = plugin.getEconomy()
|
BondManager bm = plugin.getBonds();
|
||||||
.getBalance(p);
|
if (bm != null) {
|
||||||
plugin.getEconomy()
|
int before = bm.getBonds(p.getName());
|
||||||
.depositPlayer(p, 500);
|
bm.addBonds(p.getName(), 500, "gift_stage2", "", "新手礼包", "阶段2奖励");
|
||||||
double after = plugin.getEconomy()
|
int after = bm.getBonds(p.getName());
|
||||||
.getBalance(p);
|
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
|
||||||
+ (long) before
|
|
||||||
+ "§f → §a" + (long) after);
|
|
||||||
}
|
}
|
||||||
} else if (stage == 3) {
|
} else if (stage == 3) {
|
||||||
if (plugin.getEconomy() != null) {
|
BondManager bm = plugin.getBonds();
|
||||||
double before = plugin.getEconomy()
|
if (bm != null) {
|
||||||
.getBalance(p);
|
int before = bm.getBonds(p.getName());
|
||||||
plugin.getEconomy()
|
bm.addBonds(p.getName(), 1500, "gift_stage3", "", "新手礼包", "阶段3奖励");
|
||||||
.depositPlayer(p, 1500);
|
int after = bm.getBonds(p.getName());
|
||||||
double after = plugin.getEconomy()
|
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||||
.getBalance(p);
|
|
||||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
|
||||||
+ (long) before
|
|
||||||
+ "§f → §a" + (long) after);
|
|
||||||
}
|
}
|
||||||
giveItem(p, Material.IRON_PICKAXE);
|
giveItem(p, Material.IRON_PICKAXE);
|
||||||
giveItem(p, Material.IRON_AXE);
|
giveItem(p, Material.IRON_AXE);
|
||||||
@@ -225,16 +218,12 @@ public class GiftManager {
|
|||||||
giveItem(p, Material.IRON_LEGGINGS);
|
giveItem(p, Material.IRON_LEGGINGS);
|
||||||
giveItem(p, Material.IRON_BOOTS);
|
giveItem(p, Material.IRON_BOOTS);
|
||||||
} else if (stage == 4) {
|
} else if (stage == 4) {
|
||||||
if (plugin.getEconomy() != null) {
|
BondManager bm = plugin.getBonds();
|
||||||
double before = plugin.getEconomy()
|
if (bm != null) {
|
||||||
.getBalance(p);
|
int before = bm.getBonds(p.getName());
|
||||||
plugin.getEconomy()
|
bm.addBonds(p.getName(), 2000, "gift_stage4", "", "新手礼包", "阶段4奖励");
|
||||||
.depositPlayer(p, 2000);
|
int after = bm.getBonds(p.getName());
|
||||||
double after = plugin.getEconomy()
|
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||||
.getBalance(p);
|
|
||||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
|
||||||
+ (long) before
|
|
||||||
+ "§f → §a" + (long) after);
|
|
||||||
}
|
}
|
||||||
giveItem(p, Material.DIAMOND_HELMET);
|
giveItem(p, Material.DIAMOND_HELMET);
|
||||||
giveItem(p, Material.DIAMOND_CHESTPLATE);
|
giveItem(p, Material.DIAMOND_CHESTPLATE);
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.*;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerBedEnterEvent;
|
||||||
|
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home传送点管理器
|
||||||
|
* 支持: sethome, delhome, home, homes
|
||||||
|
* 集成用户组系统: home_limit (0=全局默认, >0=组限制, -1=无限)
|
||||||
|
*/
|
||||||
|
public class HomeManager implements Listener {
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
// 床位置缓存: player_name → Location (用于自动记录床位置)
|
||||||
|
private final Map<String, Location> bedLocations = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public HomeManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
initDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 数据结构 ====================
|
||||||
|
|
||||||
|
public static class HomeData {
|
||||||
|
public String name;
|
||||||
|
public String world;
|
||||||
|
public double x, y, z;
|
||||||
|
public float yaw, pitch;
|
||||||
|
public long createdAt;
|
||||||
|
|
||||||
|
public Location toLocation(World world) {
|
||||||
|
return new Location(world, x, y, z, yaw, pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 数据库初始化 ====================
|
||||||
|
|
||||||
|
private void initDatabase() {
|
||||||
|
try {
|
||||||
|
java.sql.Connection db = plugin.getDb().getConnection();
|
||||||
|
if (db == null) {
|
||||||
|
plugin.getLogger().warning("[Home] 无法获取数据库连接");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.sql.Statement st = db.createStatement();
|
||||||
|
// homes表已在DatabaseManager中创建,此处确保索引
|
||||||
|
st.execute("CREATE INDEX IF NOT EXISTS idx_homes_player ON homes(player_name)");
|
||||||
|
st.close();
|
||||||
|
|
||||||
|
plugin.getLogger().info("[Home] 数据库初始化完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Home] 数据库初始化失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 命令处理 ====================
|
||||||
|
|
||||||
|
public boolean handleCommand(Player player, String label, String[] args) {
|
||||||
|
String lowerLabel = label.toLowerCase();
|
||||||
|
|
||||||
|
switch (lowerLabel) {
|
||||||
|
case "sethome":
|
||||||
|
return handleSetHome(player, args);
|
||||||
|
case "delhome":
|
||||||
|
return handleDelHome(player, args);
|
||||||
|
case "home":
|
||||||
|
return handleHome(player, args);
|
||||||
|
case "homes":
|
||||||
|
return handleHomes(player, args);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /sethome <名称> - 设置家
|
||||||
|
*/
|
||||||
|
private boolean handleSetHome(Player player, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
player.sendMessage("§c用法: /sethome <名称>");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String homeName = args[0].toLowerCase();
|
||||||
|
|
||||||
|
// 校验名称格式
|
||||||
|
if (!homeName.matches("^[a-zA-Z0-9_]{1,16}$")) {
|
||||||
|
player.sendMessage("§c家名只能包含字母、数字、下划线,长度1-16");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查家数量限制
|
||||||
|
int maxHomes = getMaxHomes(player);
|
||||||
|
if (maxHomes != -1) {
|
||||||
|
List<HomeData> homes = getHomes(player.getName());
|
||||||
|
if (homes.size() >= maxHomes) {
|
||||||
|
player.sendMessage("§c你已达到家数量上限 (" + maxHomes + "个)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在同名家
|
||||||
|
if (homeExists(player.getName(), homeName)) {
|
||||||
|
player.sendMessage("§c你已有一个名为 §e" + homeName + " §c的家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存家
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
saveHome(player.getName(), homeName, loc);
|
||||||
|
|
||||||
|
player.sendMessage("§a家 §e" + homeName + " §a已设置!");
|
||||||
|
player.sendMessage("§7使用 §e/home " + homeName + " §7传送回家");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /delhome <名称> - 删除家
|
||||||
|
*/
|
||||||
|
private boolean handleDelHome(Player player, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
player.sendMessage("§c用法: /delhome <名称>");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String homeName = args[0].toLowerCase();
|
||||||
|
|
||||||
|
if (!homeExists(player.getName(), homeName)) {
|
||||||
|
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteHome(player.getName(), homeName);
|
||||||
|
player.sendMessage("§a家 §e" + homeName + " §a已删除!");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /home [名称] - 传送到家
|
||||||
|
*/
|
||||||
|
private boolean handleHome(Player player, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
// 无参数: 传送到第一个家
|
||||||
|
List<HomeData> homes = getHomes(player.getName());
|
||||||
|
if (homes.isEmpty()) {
|
||||||
|
player.sendMessage("§c你还没有设置任何家");
|
||||||
|
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
teleportToHome(player, homes.get(0));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String homeName = args[0].toLowerCase();
|
||||||
|
HomeData home = getHome(player.getName(), homeName);
|
||||||
|
|
||||||
|
if (home == null) {
|
||||||
|
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportToHome(player, home);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /homes - 查看所有家
|
||||||
|
*/
|
||||||
|
private boolean handleHomes(Player player, String[] args) {
|
||||||
|
List<HomeData> homes = getHomes(player.getName());
|
||||||
|
|
||||||
|
if (homes.isEmpty()) {
|
||||||
|
player.sendMessage("§e你还没有设置任何家");
|
||||||
|
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxHomes = getMaxHomes(player);
|
||||||
|
player.sendMessage("§6§l你的家 §7(" + homes.size() + (maxHomes == -1 ? "/" + "∞" : "/" + maxHomes) + ")");
|
||||||
|
player.sendMessage("§7────────────────────");
|
||||||
|
|
||||||
|
for (HomeData home : homes) {
|
||||||
|
String worldName = home.world != null ? home.world : "未知";
|
||||||
|
player.sendMessage("§e • " + home.name + " §7[" + worldName + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage("§7────────────────────");
|
||||||
|
player.sendMessage("§7使用 §e/home <名称> §7传送");
|
||||||
|
player.sendMessage("§7使用 §e/sethome <名称> §7设置新家");
|
||||||
|
player.sendMessage("§7使用 §e/delhome <名称> §7删除家");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 床事件监听 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家上床时自动记录床位置
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerBedEnter(PlayerBedEnterEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Location bedLoc = event.getBed().getLocation();
|
||||||
|
|
||||||
|
bedLocations.put(player.getName(), bedLoc);
|
||||||
|
|
||||||
|
// 自动保存为"bed"家(如果不存在)
|
||||||
|
if (!homeExists(player.getName(), "bed")) {
|
||||||
|
saveHome(player.getName(), "bed", bedLoc);
|
||||||
|
player.sendMessage("§a床位置已自动保存为 §e/home bed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家重生时更新床位置
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Location respawnLoc = event.getRespawnLocation();
|
||||||
|
|
||||||
|
// 如果重生点是床位置,更新bed家
|
||||||
|
Location bedLoc = bedLocations.get(player.getName());
|
||||||
|
if (bedLoc != null && bedLoc.getWorld() != null) {
|
||||||
|
if (respawnLoc.getWorld() != null &&
|
||||||
|
respawnLoc.getWorld().getName().equals(bedLoc.getWorld().getName()) &&
|
||||||
|
respawnLoc.distance(bedLoc) < 5) {
|
||||||
|
// 重生点就是床,更新bed家
|
||||||
|
updateHome(player.getName(), "bed", respawnLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 家数量限制 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家最大家数量
|
||||||
|
* @return -1=无限, 0=使用全局默认, >0=用户组独立限制
|
||||||
|
*/
|
||||||
|
private int getMaxHomes(Player player) {
|
||||||
|
UserGroupManager userGroupMgr = plugin.getUserGroup();
|
||||||
|
if (userGroupMgr == null) {
|
||||||
|
return getGlobalHomeLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取玩家最高优先级用户组
|
||||||
|
UserGroupManager.UserGroupConfig highestGroup = userGroupMgr.getHighestGroup(player.getName());
|
||||||
|
if (highestGroup == null) {
|
||||||
|
return getGlobalHomeLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户组的home_limit配置
|
||||||
|
int groupLimit = userGroupMgr.getHomeLimit(highestGroup.name);
|
||||||
|
|
||||||
|
if (groupLimit == -1) {
|
||||||
|
// 无限
|
||||||
|
return -1;
|
||||||
|
} else if (groupLimit == 0) {
|
||||||
|
// 跟随全局
|
||||||
|
return getGlobalHomeLimit();
|
||||||
|
} else {
|
||||||
|
// 独立限制
|
||||||
|
return groupLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全局家数量限制
|
||||||
|
*/
|
||||||
|
private int getGlobalHomeLimit() {
|
||||||
|
// 默认5个家,可从配置文件读取
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 数据库操作 ====================
|
||||||
|
|
||||||
|
private boolean homeExists(String playerName, String homeName) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection db = plugin.getDb().getConnection();
|
||||||
|
if (db == null) return false;
|
||||||
|
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT COUNT(*) FROM homes WHERE player_name=? AND home_name=?");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ps.setString(2, homeName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
boolean exists = rs.next() && rs.getInt(1) > 0;
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return exists;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HomeData> getHomes(String playerName) {
|
||||||
|
List<HomeData> homes = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
java.sql.Connection db = plugin.getDb().getConnection();
|
||||||
|
if (db == null) return homes;
|
||||||
|
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT * FROM homes WHERE player_name=? ORDER BY created_at");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
HomeData home = new HomeData();
|
||||||
|
home.name = rs.getString("home_name");
|
||||||
|
home.world = rs.getString("world");
|
||||||
|
home.x = rs.getDouble("x");
|
||||||
|
home.y = rs.getDouble("y");
|
||||||
|
home.z = rs.getDouble("z");
|
||||||
|
home.yaw = rs.getFloat("yaw");
|
||||||
|
home.pitch = rs.getFloat("pitch");
|
||||||
|
home.createdAt = rs.getLong("created_at");
|
||||||
|
homes.add(home);
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[Home] 获取家列表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return homes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HomeData getHome(String playerName, String homeName) {
|
||||||
|
List<HomeData> homes = getHomes(playerName);
|
||||||
|
for (HomeData home : homes) {
|
||||||
|
if (home.name.equalsIgnoreCase(homeName)) {
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveHome(String playerName, String homeName, Location loc) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection db = plugin.getDb().getConnection();
|
||||||
|
if (db == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"INSERT OR REPLACE INTO homes (player_name, home_name, world, x, y, z, yaw, pitch, created_at) "
|
||||||
|
+ "VALUES (?,?,?,?,?,?,?,?,?)");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ps.setString(2, homeName);
|
||||||
|
ps.setString(3, loc.getWorld() != null ? loc.getWorld().getName() : "world");
|
||||||
|
ps.setDouble(4, loc.getX());
|
||||||
|
ps.setDouble(5, loc.getY());
|
||||||
|
ps.setDouble(6, loc.getZ());
|
||||||
|
ps.setFloat(7, loc.getYaw());
|
||||||
|
ps.setFloat(8, loc.getPitch());
|
||||||
|
ps.setLong(9, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[Home] 保存家失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHome(String playerName, String homeName, Location loc) {
|
||||||
|
saveHome(playerName, homeName, loc); // INSERT OR REPLACE 自动更新
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteHome(String playerName, String homeName) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection db = plugin.getDb().getConnection();
|
||||||
|
if (db == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"DELETE FROM homes WHERE player_name=? AND home_name=?");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ps.setString(2, homeName);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[Home] 删除家失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传送玩家到家
|
||||||
|
*/
|
||||||
|
private void teleportToHome(Player player, HomeData home) {
|
||||||
|
World world = Bukkit.getWorld(home.world);
|
||||||
|
if (world == null) {
|
||||||
|
player.sendMessage("§c世界 §e" + home.world + " §c不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location loc = home.toLocation(world);
|
||||||
|
|
||||||
|
// 安全检查: 确保位置安全
|
||||||
|
if (loc.getBlock().getType().isSolid()) {
|
||||||
|
// 向上寻找安全位置
|
||||||
|
for (int y = (int) loc.getY(); y < 256; y++) {
|
||||||
|
Location safeLoc = new Location(world, loc.getX(), y + 1, loc.getZ());
|
||||||
|
if (!safeLoc.getBlock().getType().isSolid()) {
|
||||||
|
loc = safeLoc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player.teleport(loc);
|
||||||
|
player.sendMessage("§a已传送到家 §e" + home.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Tab补全 ====================
|
||||||
|
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = (Player) sender;
|
||||||
|
String cmdName = command.getName().toLowerCase();
|
||||||
|
|
||||||
|
if (cmdName.equals("home") || cmdName.equals("delhome")) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
List<String> homes = new ArrayList<>();
|
||||||
|
for (HomeData home : getHomes(player.getName())) {
|
||||||
|
if (home.name.toLowerCase().startsWith(args[0].toLowerCase())) {
|
||||||
|
homes.add(home.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return homes;
|
||||||
|
}
|
||||||
|
} else if (cmdName.equals("sethome") || cmdName.equals("homes")) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 清理 ====================
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
bedLocations.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ public class LoginManager {
|
|||||||
plugin.getDb().createUser(
|
plugin.getDb().createUser(
|
||||||
name, hash, salt);
|
name, hash, salt);
|
||||||
plugin.getLoggedIn().add(name);
|
plugin.getLoggedIn().add(name);
|
||||||
|
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
plugin.webManager.recordJavaLogin(name);
|
||||||
|
}
|
||||||
p.setAllowFlight(false);
|
p.setAllowFlight(false);
|
||||||
p.setFlying(false);
|
p.setFlying(false);
|
||||||
plugin.getDb().setLoggedIn(name, true);
|
plugin.getDb().setLoggedIn(name, true);
|
||||||
@@ -100,6 +104,17 @@ public class LoginManager {
|
|||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 注册后立即同步用户数据到PHP
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try { plugin.webManager.syncUserRegistrations(); }
|
||||||
|
catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +177,10 @@ public class LoginManager {
|
|||||||
name, hash);
|
name, hash);
|
||||||
if (matchMain) {
|
if (matchMain) {
|
||||||
plugin.getLoggedIn().add(name);
|
plugin.getLoggedIn().add(name);
|
||||||
|
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
plugin.webManager.recordJavaLogin(name);
|
||||||
|
}
|
||||||
plugin.getDb().setLoggedIn(
|
plugin.getDb().setLoggedIn(
|
||||||
name, true);
|
name, true);
|
||||||
plugin.getDb().setLoggedIn(
|
plugin.getDb().setLoggedIn(
|
||||||
@@ -201,12 +220,16 @@ public class LoginManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean matchTemp =
|
String pwdResult =
|
||||||
plugin.getDb()
|
plugin.getDb()
|
||||||
.checkPasswordOrTemp(
|
.checkPasswordWithFallback(
|
||||||
name, hash);
|
name, hash);
|
||||||
if (matchTemp) {
|
if (pwdResult != null) {
|
||||||
plugin.getLoggedIn().add(name);
|
plugin.getLoggedIn().add(name);
|
||||||
|
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||||
|
if (plugin.webManager != null) {
|
||||||
|
plugin.webManager.recordJavaLogin(name);
|
||||||
|
}
|
||||||
plugin.getDb().setLoggedIn(name, true);
|
plugin.getDb().setLoggedIn(name, true);
|
||||||
p.setAllowFlight(false);
|
p.setAllowFlight(false);
|
||||||
p.setFlying(false);
|
p.setFlying(false);
|
||||||
|
|||||||
+1086
-389
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||||
|
|||||||
@@ -259,20 +259,21 @@ public class PointsManager {
|
|||||||
switch (slot) {
|
switch (slot) {
|
||||||
case 10:
|
case 10:
|
||||||
if (points >= 100) {
|
if (points >= 100) {
|
||||||
plugin.getDb().deductPoints(
|
BondManager bm = plugin.getBonds();
|
||||||
p.getName(), 100);
|
if (bm != null) {
|
||||||
if (plugin.getEconomy() != null) {
|
int before = bm.getBonds(p.getName());
|
||||||
plugin.getEconomy()
|
bm.addBonds(p.getName(), 1000, "points_exchange", "", "积分商店", "积分兑换债券");
|
||||||
.depositPlayer(p, 1000);
|
int after = bm.getBonds(p.getName());
|
||||||
}
|
plugin.getDb().deductPoints(p.getName(), 100);
|
||||||
p.sendMessage(plugin.getConfig2()
|
p.sendMessage(plugin.getConfig2().msg("points_purchase_success")
|
||||||
.msg("points_purchase_success")
|
|
||||||
.replace("{count}", "100"));
|
.replace("{count}", "100"));
|
||||||
|
p.sendMessage("§a[积分] §f债券: §e" + before + " → §a" + after);
|
||||||
} else {
|
} else {
|
||||||
p.sendMessage(plugin.getConfig2()
|
p.sendMessage("§c债券系统不可用");
|
||||||
.msg("points_insufficient")
|
}
|
||||||
.replace("{points}",
|
} else {
|
||||||
String.valueOf(points)));
|
p.sendMessage(plugin.getConfig2().msg("points_insufficient")
|
||||||
|
.replace("{points}", String.valueOf(points)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
|
|||||||
@@ -1213,15 +1213,6 @@ public class QuestTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Main main = (Main) plugin;
|
Main main = (Main) plugin;
|
||||||
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
|
|
||||||
boolean useEconomy = "economy".equalsIgnoreCase(
|
|
||||||
main.getConfigMgr().rewardChannel);
|
|
||||||
|
|
||||||
// Vault 经济引用(仅 useEconomy 时使用)
|
|
||||||
net.milkbowl.vault.economy.Economy econ = null;
|
|
||||||
if (useEconomy) {
|
|
||||||
econ = main.getEconomy();
|
|
||||||
}
|
|
||||||
|
|
||||||
int sentCount = 0;
|
int sentCount = 0;
|
||||||
|
|
||||||
@@ -1247,53 +1238,17 @@ public class QuestTracker {
|
|||||||
// 尝试识别经济金额
|
// 尝试识别经济金额
|
||||||
double amt = extractEconomyAmount(part);
|
double amt = extractEconomyAmount(part);
|
||||||
if (amt > 0) {
|
if (amt > 0) {
|
||||||
// ★ 根据配置分发
|
// Economy已移除,全部转为债券发放
|
||||||
if (useEconomy && econ != null) {
|
// —— 债券 ——
|
||||||
// —— Vault 经济 ——
|
int bondAmt = (int) Math.round(amt);
|
||||||
double before =
|
|
||||||
econ.getBalance(player);
|
|
||||||
econ.depositPlayer(player, amt);
|
|
||||||
double after =
|
|
||||||
econ.getBalance(player);
|
|
||||||
player.sendMessage(
|
|
||||||
"§a§l[任务奖励] §f" + part
|
|
||||||
+ " §7(余额: §e"
|
|
||||||
+ String.format("%.2f", before)
|
|
||||||
+ " §7→ §a"
|
|
||||||
+ String.format("%.2f", after)
|
|
||||||
+ "§7)");
|
|
||||||
if (plugin instanceof Main) {
|
|
||||||
((Main) plugin).getCommission()
|
|
||||||
.payCommission(
|
|
||||||
player.getName(), amt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// —— 债券(默认) ——
|
|
||||||
int bondAmt =
|
|
||||||
(int) Math.round(amt);
|
|
||||||
BondManager bonds = main.getBonds();
|
BondManager bonds = main.getBonds();
|
||||||
if (bonds != null) {
|
if (bonds != null) {
|
||||||
int before = bonds.getBonds(
|
int before = bonds.getBonds(player.getName());
|
||||||
player.getName());
|
bonds.addBonds(player.getName(), bondAmt, "quest_reward", qf.displayName, "任务系统", "任务奖励:" + qf.displayName);
|
||||||
bonds.addBonds(
|
int after = bonds.getBonds(player.getName());
|
||||||
player.getName(),
|
player.sendMessage("§a§l[任务奖励] §f" + part + " §7(债券: §e" + before + " §7→ §a" + after + "§7)");
|
||||||
bondAmt,
|
|
||||||
"quest_reward",
|
|
||||||
qf.displayName,
|
|
||||||
"任务系统",
|
|
||||||
"任务奖励:" + qf.displayName);
|
|
||||||
int after = bonds.getBonds(
|
|
||||||
player.getName());
|
|
||||||
player.sendMessage(
|
|
||||||
"§a§l[任务奖励] §f" + part
|
|
||||||
+ " §7(债券: §e"
|
|
||||||
+ before
|
|
||||||
+ " §7→ §a"
|
|
||||||
+ after + "§7)");
|
|
||||||
} else {
|
} else {
|
||||||
player.sendMessage(
|
player.sendMessage("§c债券系统不可用");
|
||||||
"§c债券系统不可用");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sentCount++;
|
sentCount++;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速返回系统 (/back)
|
||||||
|
* - 每分钟记录一次在线玩家的坐标
|
||||||
|
* - /back 命令传送玩家到最近的一个记录坐标
|
||||||
|
*/
|
||||||
|
public class QuickBack implements Listener, CommandExecutor, TabCompleter {
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
// 内存缓存: player_name → Location (每分钟刷新)
|
||||||
|
private final Map<String, Location> lastKnownLocations = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 冷却: player_name → next_available_time
|
||||||
|
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// ★ 定时器引用(用于 shutdown 时取消)
|
||||||
|
private BukkitTask scheduleTask = null;
|
||||||
|
|
||||||
|
private static final long COOLDOWN_MS = 10 * 1000; // 10秒冷却
|
||||||
|
|
||||||
|
public QuickBack(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
loadAllFromDB();
|
||||||
|
|
||||||
|
// 每分钟记录所有在线玩家的坐标
|
||||||
|
scheduleTask = new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
|
recordLocation(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.runTaskTimer(plugin, 1200L, 1200L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 事件监听 ====================
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerMove(PlayerMoveEvent event) {
|
||||||
|
// 移动超过8格时才记录(减少数据库写入压力)
|
||||||
|
Location from = event.getFrom();
|
||||||
|
Location to = event.getTo();
|
||||||
|
if (to == null || from.getWorld() != to.getWorld()) return;
|
||||||
|
|
||||||
|
double distance = from.distance(to);
|
||||||
|
if (distance < 8.0) return;
|
||||||
|
|
||||||
|
recordLocation(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 命令处理 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player)) {
|
||||||
|
sender.sendMessage("§c此命令仅限玩家使用");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = (Player) sender;
|
||||||
|
|
||||||
|
// 检查冷却
|
||||||
|
Long nextAvailable = cooldowns.get(player.getName());
|
||||||
|
if (nextAvailable != null && System.currentTimeMillis() < nextAvailable) {
|
||||||
|
long remaining = (nextAvailable - System.currentTimeMillis()) / 1000;
|
||||||
|
player.sendMessage("§c快速返回冷却中,请等待 §e" + remaining + " §c秒");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近的已知位置
|
||||||
|
Location loc = getLastKnownLocation(player);
|
||||||
|
if (loc == null) {
|
||||||
|
player.sendMessage("§c没有找到可返回的位置记录");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传送
|
||||||
|
if (loc.getWorld() == null) {
|
||||||
|
player.sendMessage("§c位置世界信息无效");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location dest = loc.clone().add(0.5, 0, 0.5); // 对齐格子中心
|
||||||
|
player.teleport(dest);
|
||||||
|
|
||||||
|
// 设置冷却
|
||||||
|
cooldowns.put(player.getName(), System.currentTimeMillis() + COOLDOWN_MS);
|
||||||
|
player.sendMessage("§a§l[快速返回] §f已传送到最近记录点");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 位置记录 ====================
|
||||||
|
|
||||||
|
private void recordLocation(Player player) {
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
lastKnownLocations.put(player.getName(), loc);
|
||||||
|
saveToDB(player.getName(), loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLastKnownLocation(Player player) {
|
||||||
|
// 优先从内存获取
|
||||||
|
Location loc = lastKnownLocations.get(player.getName());
|
||||||
|
if (loc != null && loc.getWorld() != null) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取
|
||||||
|
return loadFromDB(player.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 数据库操作 ====================
|
||||||
|
|
||||||
|
private void loadAllFromDB() {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"SELECT player_name, world, x, y, z FROM quick_back_records");
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
String playerName = rs.getString("player_name");
|
||||||
|
Location loc = new Location(
|
||||||
|
Bukkit.getWorld(rs.getString("world")),
|
||||||
|
rs.getDouble("x"),
|
||||||
|
rs.getDouble("y"),
|
||||||
|
rs.getDouble("z")
|
||||||
|
);
|
||||||
|
lastKnownLocations.put(playerName, loc);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// 表可能不存在
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveToDB(String playerName, Location loc) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"INSERT OR REPLACE INTO quick_back_records (player_name, world, x, y, z) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ps.setString(2, loc.getWorld() != null ? loc.getWorld().getName() : "world");
|
||||||
|
ps.setDouble(3, loc.getX());
|
||||||
|
ps.setDouble(4, loc.getY());
|
||||||
|
ps.setDouble(5, loc.getZ());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location loadFromDB(String playerName) {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return null;
|
||||||
|
|
||||||
|
PreparedStatement ps = conn.prepareStatement(
|
||||||
|
"SELECT world, x, y, z FROM quick_back_records WHERE player_name=? LIMIT 1");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
|
||||||
|
if (rs.next()) {
|
||||||
|
Location loc = new Location(
|
||||||
|
Bukkit.getWorld(rs.getString("world")),
|
||||||
|
rs.getDouble("x"),
|
||||||
|
rs.getDouble("y"),
|
||||||
|
rs.getDouble("z")
|
||||||
|
);
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Tab补全 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
public void initTable() {
|
||||||
|
try {
|
||||||
|
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||||
|
if (conn == null) return;
|
||||||
|
|
||||||
|
java.sql.Statement st = conn.createStatement();
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS quick_back_records ("
|
||||||
|
+ "player_name TEXT PRIMARY KEY, "
|
||||||
|
+ "world TEXT, "
|
||||||
|
+ "x REAL, y REAL, z REAL)");
|
||||||
|
st.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().warning("[快速返回] 建表失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 关闭 ====================
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (scheduleTask != null) {
|
||||||
|
scheduleTask.cancel();
|
||||||
|
}
|
||||||
|
lastKnownLocations.clear();
|
||||||
|
cooldowns.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import java.io.*;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||||
import org.bukkit.potion.PotionEffect;
|
import org.bukkit.potion.PotionEffect;
|
||||||
@@ -30,6 +31,11 @@ public class ShopManager implements Listener {
|
|||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
private final List<ShopCategory> categories =
|
private final List<ShopCategory> categories =
|
||||||
new ArrayList<>();
|
new ArrayList<>();
|
||||||
|
// ===== 出售限额配置 =====
|
||||||
|
private String maxSellMessage = "";
|
||||||
|
private boolean maxSellMessageEnabled = false;
|
||||||
|
private int maxSellLimit = 15000; // 默认1.5万
|
||||||
|
private final Object maxSellLock = new Object();
|
||||||
private final List<RefundRecord> refundRecords =
|
private final List<RefundRecord> refundRecords =
|
||||||
new ArrayList<>();
|
new ArrayList<>();
|
||||||
private final Map<UUID, Integer> activeDiscount =
|
private final Map<UUID, Integer> activeDiscount =
|
||||||
@@ -140,6 +146,7 @@ public class ShopManager implements Listener {
|
|||||||
public ShopManager(Main plugin) {
|
public ShopManager(Main plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
loadMaxSellConfig();
|
||||||
startHourlyTask();
|
startHourlyTask();
|
||||||
loadItemNbtData();
|
loadItemNbtData();
|
||||||
startCleanupTask();
|
startCleanupTask();
|
||||||
@@ -1371,6 +1378,62 @@ public class ShopManager implements Listener {
|
|||||||
public void saveAll() {
|
public void saveAll() {
|
||||||
for (ShopCategory cat : categories)
|
for (ShopCategory cat : categories)
|
||||||
saveCategory(cat);
|
saveCategory(cat);
|
||||||
|
saveMaxSellConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== maxSell 配置持久化 =====
|
||||||
|
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
|
||||||
|
private void loadMaxSellConfig() {
|
||||||
|
File cfgFile = new File(plugin.getDataFolder(), "maxsell_config.txt");
|
||||||
|
try {
|
||||||
|
if (cfgFile.exists()) {
|
||||||
|
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (line.startsWith("maxSellLimit=")) {
|
||||||
|
try {
|
||||||
|
maxSellLimit = Integer.parseInt(line.substring("maxSellLimit=".length()));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
} else if (line.startsWith("maxSellMessageEnabled=")) {
|
||||||
|
maxSellMessageEnabled = Boolean.parseBoolean(line.substring("maxSellMessageEnabled=".length()));
|
||||||
|
} else if (line.startsWith("maxSellMessage=")) {
|
||||||
|
maxSellMessage = line.substring("maxSellMessage=".length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
} else {
|
||||||
|
// 配置文件不存在,使用默认值并保存
|
||||||
|
maxSellLimit = 15000;
|
||||||
|
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||||
|
maxSellMessageEnabled = true;
|
||||||
|
saveMaxSellConfig();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Shop] 加载maxsell配置失败: " + e.getMessage());
|
||||||
|
maxSellLimit = 15000;
|
||||||
|
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||||
|
maxSellMessageEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存 maxSell 配置到插件数据文件夹下的 maxsell_config.txt */
|
||||||
|
private void saveMaxSellConfig() {
|
||||||
|
synchronized (maxSellLock) {
|
||||||
|
try {
|
||||||
|
File dir = new File(plugin.getDataFolder(), "shop");
|
||||||
|
if (!dir.exists()) dir.mkdirs();
|
||||||
|
File cfgFile = new File(dir.getParentFile(), "maxsell_config.txt");
|
||||||
|
BufferedWriter bw = new BufferedWriter(new FileWriter(cfgFile, StandardCharsets.UTF_8));
|
||||||
|
bw.write("maxSellLimit=" + maxSellLimit);
|
||||||
|
bw.newLine();
|
||||||
|
bw.write("maxSellMessageEnabled=" + maxSellMessageEnabled);
|
||||||
|
bw.newLine();
|
||||||
|
bw.write("maxSellMessage=" + maxSellMessage);
|
||||||
|
bw.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Shop] 保存maxsell配置失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public ItemStack getShopStack(
|
public ItemStack getShopStack(
|
||||||
ShopItem item, int amount) {
|
ShopItem item, int amount) {
|
||||||
@@ -1555,6 +1618,179 @@ public class ShopManager implements Listener {
|
|||||||
|
|
||||||
// ===== 出售 =====
|
// ===== 出售 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML解码器,支持:
|
||||||
|
* - &颜色代码 (如 &c红色, &a绿色)
|
||||||
|
* - <b>加粗</b>, <u>下划线</u>, <i>斜体</i>
|
||||||
|
* - <br>换行
|
||||||
|
* - <p style="...">段落</p> (支持color属性)
|
||||||
|
*/
|
||||||
|
private String decodeHtml(String input) {
|
||||||
|
if (input == null || input.isEmpty()) return input;
|
||||||
|
String result = input;
|
||||||
|
|
||||||
|
// 1. 先处理 &颜色代码 → §颜色代码
|
||||||
|
result = result.replace("&0", "§0");
|
||||||
|
result = result.replace("&1", "§1");
|
||||||
|
result = result.replace("&2", "§2");
|
||||||
|
result = result.replace("&3", "§3");
|
||||||
|
result = result.replace("&4", "§4");
|
||||||
|
result = result.replace("&5", "§5");
|
||||||
|
result = result.replace("&6", "§6");
|
||||||
|
result = result.replace("&7", "§7");
|
||||||
|
result = result.replace("&8", "§8");
|
||||||
|
result = result.replace("&9", "§9");
|
||||||
|
result = result.replace("&a", "§a");
|
||||||
|
result = result.replace("&b", "§b");
|
||||||
|
result = result.replace("&c", "§c");
|
||||||
|
result = result.replace("&d", "§d");
|
||||||
|
result = result.replace("&e", "§e");
|
||||||
|
result = result.replace("&f", "§f");
|
||||||
|
// 样式代码
|
||||||
|
result = result.replace("&k", "§k");
|
||||||
|
result = result.replace("&l", "§l");
|
||||||
|
result = result.replace("&m", "§m");
|
||||||
|
result = result.replace("&n", "§n");
|
||||||
|
result = result.replace("&o", "§o");
|
||||||
|
result = result.replace("&r", "§r");
|
||||||
|
|
||||||
|
// 2. 处理HTML标签
|
||||||
|
// <u>标签:§n 开始下划线,§r 切断(只作用于标签内)
|
||||||
|
// <b>标签:§l 开始加粗,§r 切断
|
||||||
|
// <i>标签:§o 开始斜体,§r 切断
|
||||||
|
result = result.replaceAll("<u>(.*?)</u>", "§n$1§r");
|
||||||
|
result = result.replaceAll("<b>(.*?)</b>", "§l$1§r");
|
||||||
|
result = result.replaceAll("<i>(.*?)</i>", "§o$1§r");
|
||||||
|
result = result.replaceAll("<br>", "\n");
|
||||||
|
result = result.replaceAll("<br/>", "\n");
|
||||||
|
|
||||||
|
// 3. 处理 <p style="color:#xxx"> 或 <p style="color:xxx">
|
||||||
|
java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile(
|
||||||
|
"<p\\s+style=[\"']color:\\s*(#[0-9a-fA-F]{3,8}|[a-zA-Z_]+|rgb\\([^)]+\\)|rgba\\([^)]+\\)|hsl\\([^)]+\\)|hsla\\([^)]+\\))\\s*;?[\"']\\s*>(.*?)</p>",
|
||||||
|
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||||
|
java.util.regex.Matcher pMatcher = pPattern.matcher(result);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (pMatcher.find()) {
|
||||||
|
String color = pMatcher.group(1);
|
||||||
|
String text = pMatcher.group(2);
|
||||||
|
String mcColor = convertToMcColor(color);
|
||||||
|
pMatcher.appendReplacement(sb, mcColor + text + "§r");
|
||||||
|
}
|
||||||
|
pMatcher.appendTail(sb);
|
||||||
|
result = sb.toString();
|
||||||
|
|
||||||
|
// 4. 处理 <p>段落</p> (无style) → 换行分隔
|
||||||
|
result = result.replaceAll("<p>(.*?)</p>", "$1\n");
|
||||||
|
|
||||||
|
// 5. 移除残留的style标签属性
|
||||||
|
result = result.replaceAll("<p[^>]*>", "");
|
||||||
|
result = result.replaceAll("</p>", "");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将颜色值转换为Minecraft颜色代码
|
||||||
|
* 支持: #RRGGBB 十六进制, 或颜色名称
|
||||||
|
*/
|
||||||
|
private String convertToMcColor(String color) {
|
||||||
|
if (color == null || color.isEmpty()) return "";
|
||||||
|
// 处理 rgb/rgba 颜色函数
|
||||||
|
String lowerColor = color.toLowerCase();
|
||||||
|
if (lowerColor.startsWith("rgb(") || lowerColor.startsWith("rgba(")) {
|
||||||
|
try {
|
||||||
|
// 提取括号内的内容
|
||||||
|
int start = color.indexOf('(') + 1;
|
||||||
|
int end = color.lastIndexOf(')');
|
||||||
|
if (start > 0 && end > start) {
|
||||||
|
String content = color.substring(start, end);
|
||||||
|
String[] parts = content.split(",");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
int r = Integer.parseInt(parts[0].trim());
|
||||||
|
int g = Integer.parseInt(parts[1].trim());
|
||||||
|
int b = Integer.parseInt(parts[2].trim());
|
||||||
|
// 转换为十六进制
|
||||||
|
String hex = String.format("#%02x%02x%02x", r, g, b);
|
||||||
|
// 递归调用,使用十六进制逻辑
|
||||||
|
return convertToMcColor(hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 解析失败,返回默认白色
|
||||||
|
return "§f";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 hsl/hsla 颜色函数(简单映射到最接近的Minecraft颜色)
|
||||||
|
if (lowerColor.startsWith("hsl(") || lowerColor.startsWith("hsla(")) {
|
||||||
|
// 由于HSL解析复杂,暂时返回默认白色
|
||||||
|
return "§f";
|
||||||
|
}
|
||||||
|
// 十六进制颜色
|
||||||
|
if (color.startsWith("#")) {
|
||||||
|
String hex = color.substring(1).toLowerCase();
|
||||||
|
if (hex.equals("ff5555") || hex.equals("ff0000")) return "§c";
|
||||||
|
if (hex.equals("55ff55") || hex.equals("00ff00")) return "§a";
|
||||||
|
if (hex.equals("5555ff") || hex.equals("0000ff")) return "§9";
|
||||||
|
if (hex.equals("ffff55") || hex.equals("ffff00")) return "§e";
|
||||||
|
if (hex.equals("ffffff")) return "§f";
|
||||||
|
if (hex.equals("000000")) return "§0";
|
||||||
|
if (hex.equals("a0a0a0") || hex.equals("808080")) return "§7";
|
||||||
|
if (hex.equals("ffaa00")) return "§6";
|
||||||
|
if (hex.equals("55ffff")) return "§b";
|
||||||
|
if (hex.equals("ff55ff")) return "§d";
|
||||||
|
if (hex.equals("aa0000")) return "§4";
|
||||||
|
if (hex.equals("0000aa")) return "§1";
|
||||||
|
if (hex.equals("00aa00")) return "§2";
|
||||||
|
if (hex.equals("00aaaa")) return "§3";
|
||||||
|
if (hex.equals("aa00aa")) return "§5";
|
||||||
|
if (hex.equals("555555")) return "§8";
|
||||||
|
return "§f"; // 默认白色
|
||||||
|
}
|
||||||
|
// 颜色名称
|
||||||
|
String name = color.toLowerCase().replace("_", "");
|
||||||
|
if (name.equals("red") || name.equals("darkred") || name.equals("dark_red") || name.equals("crimson") || name.equals("salmon") || name.equals("tomato") || name.equals("coral")) return "§c";
|
||||||
|
if (name.equals("green") || name.equals("darkgreen") || name.equals("dark_green") || name.equals("lime") || name.equals("chartreuse") || name.equals("olive") || name.equals("olivegreen")) return "§a";
|
||||||
|
if (name.equals("blue") || name.equals("darkblue") || name.equals("dark_blue") || name.equals("darkaqua") || name.equals("navy") || name.equals("indigo")) return "§9";
|
||||||
|
if (name.equals("yellow") || name.equals("gold") || name.equals("goldenrod")) return "§e";
|
||||||
|
if (name.equals("white") || name.equals("silver") || name.equals("snow") || name.equals("ivory")) return "§f";
|
||||||
|
if (name.equals("black") || name.equals("onyx") || name.equals("jetblack")) return "§0";
|
||||||
|
if (name.equals("gray") || name.equals("grey") || name.equals("darkgray") || name.equals("darkgrey") || name.equals("dimgray") || name.equals("dimgrey")) return "§7";
|
||||||
|
if (name.equals("aqua") || name.equals("lightblue") || name.equals("turquoise") || name.equals("teal")) return "§b";
|
||||||
|
if (name.equals("lightpurple") || name.equals("purple") || name.equals("magenta") || name.equals("plum") || name.equals("orchid") || name.equals("violet")) return "§d";
|
||||||
|
if (name.equals("darkred")) return "§4";
|
||||||
|
if (name.equals("darkblue")) return "§1";
|
||||||
|
if (name.equals("darkgreen")) return "§2";
|
||||||
|
if (name.equals("darkaqua") || name.equals("cyan")) return "§3";
|
||||||
|
if (name.equals("darkpurple")) return "§5";
|
||||||
|
if (name.equals("darkgray") || name.equals("darkgrey")) return "§8";
|
||||||
|
if (name.equals("lightgray") || name.equals("lightgrey")) return "§7";
|
||||||
|
if (name.equals("orange") || name.equals("orangered")) return "§6";
|
||||||
|
if (name.equals("pink") || name.equals("hotpink") || name.equals("lightpink")) return "§d";
|
||||||
|
if (name.equals("brown") || name.equals("saddlebrown") || name.equals("sienna") || name.equals("chocolate")) return "§4";
|
||||||
|
return "§f"; // 默认白色
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 占位符替换 =====
|
||||||
|
private String replacePlaceholders(String msg, String player, int used, int limit, String time) {
|
||||||
|
if (msg == null || msg.isEmpty()) return msg;
|
||||||
|
// 支持中英文占位符,任意括号
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]玩家[(\\(\\)\\[\\]\\{\\}【】)]", player);
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]player[(\\(\\)\\[\\]\\{\\}【】)]", player);
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已出售金额[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]used[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已使用额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]剩余额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]limit[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]下次重置时间[(\\(\\)\\[\\]\\{\\}【】)]", time);
|
||||||
|
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]time[(\\(\\)\\[\\]\\{\\}【】)]", time);
|
||||||
|
//同时支持%player%等格式
|
||||||
|
msg = msg.replace("%player%", player);
|
||||||
|
msg = msg.replace("%used%", String.valueOf(used));
|
||||||
|
msg = msg.replace("%limit%", String.valueOf(limit));
|
||||||
|
msg = msg.replace("%time%", time);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean sellItem(Player p, ShopItem item,
|
public boolean sellItem(Player p, ShopItem item,
|
||||||
int amount) {
|
int amount) {
|
||||||
if (item == null) return false;
|
if (item == null) return false;
|
||||||
@@ -1565,6 +1801,31 @@ public class ShopManager implements Listener {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 出售限额检查
|
||||||
|
if (maxSellLimit != -1) {
|
||||||
|
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
|
||||||
|
int remaining = maxSellLimit - todaySold;
|
||||||
|
int saleAmount = item.getEffectiveSellPrice() * amount;
|
||||||
|
if (saleAmount > remaining) {
|
||||||
|
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
|
||||||
|
String msg = decodeHtml(maxSellMessage);
|
||||||
|
// 计算下次重置时间(最早交易 + 1小时)
|
||||||
|
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
|
||||||
|
String resetTime;
|
||||||
|
if (earliestTime > 0) {
|
||||||
|
resetTime = new java.text.SimpleDateFormat("HH:mm")
|
||||||
|
.format(new java.util.Date(earliestTime + 3600000L));
|
||||||
|
} else {
|
||||||
|
resetTime = "现在";
|
||||||
|
}
|
||||||
|
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
|
||||||
|
p.sendMessage(msg);
|
||||||
|
}
|
||||||
|
// 提示消息关闭时不打印任何消息
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!p.getInventory().containsAtLeast(
|
if (!p.getInventory().containsAtLeast(
|
||||||
getShopStack(item, 1), amount)) {
|
getShopStack(item, 1), amount)) {
|
||||||
p.sendMessage("§c你没有足够的 §e"
|
p.sendMessage("§c你没有足够的 §e"
|
||||||
@@ -1782,12 +2043,60 @@ public class ShopManager implements Listener {
|
|||||||
|
|
||||||
if (sub.equals("reload")) {
|
if (sub.equals("reload")) {
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
loadMaxSellConfig();
|
||||||
s.sendMessage("§a商店已重载,共 "
|
s.sendMessage("§a商店已重载,共 "
|
||||||
+ categories.size()
|
+ categories.size()
|
||||||
+ " 个分类");
|
+ " 个分类");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sub.equals("setmax")) {
|
||||||
|
if (a.length < 3) {
|
||||||
|
s.sendMessage(
|
||||||
|
"§e用法: shop setmax <金额(-1无限)>");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int limit = Integer.parseInt(a[2]);
|
||||||
|
maxSellLimit = limit;
|
||||||
|
saveMaxSellConfig();
|
||||||
|
if (limit == -1) {
|
||||||
|
s.sendMessage("§a出售限额已设为无限");
|
||||||
|
} else {
|
||||||
|
s.sendMessage("§a出售限额已设为 §e" + limit + " §a枚债券/小时");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
s.sendMessage("§c无效数字: " + a[2]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub.equals("maxmsg")) {
|
||||||
|
if (a.length < 3) {
|
||||||
|
s.sendMessage(
|
||||||
|
"§e用法: shop maxmsg <内容/关闭/默认>");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 拼接剩余参数作为消息内容
|
||||||
|
String msg = String.join(" ", Arrays.copyOfRange(a, 2, a.length));
|
||||||
|
if (msg.equalsIgnoreCase("关闭") || msg.equalsIgnoreCase("off")) {
|
||||||
|
maxSellMessageEnabled = false;
|
||||||
|
saveMaxSellConfig();
|
||||||
|
s.sendMessage("§a出售限额提示已关闭");
|
||||||
|
} else if (msg.equalsIgnoreCase("默认") || msg.equalsIgnoreCase("default")) {
|
||||||
|
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||||
|
maxSellMessageEnabled = true;
|
||||||
|
saveMaxSellConfig();
|
||||||
|
s.sendMessage("§a出售限额提示已设为默认内容");
|
||||||
|
} else {
|
||||||
|
maxSellMessage = msg;
|
||||||
|
maxSellMessageEnabled = true;
|
||||||
|
saveMaxSellConfig();
|
||||||
|
s.sendMessage("§a出售限额提示已设置");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (sub.equals("add")) {
|
if (sub.equals("add")) {
|
||||||
if (a.length < 7) {
|
if (a.length < 7) {
|
||||||
s.sendMessage(
|
s.sendMessage(
|
||||||
@@ -2527,6 +2836,30 @@ public class ShopManager implements Listener {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ★ 出售限额检查
|
||||||
|
if (maxSellLimit != -1 && total > 0) {
|
||||||
|
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
|
||||||
|
int remaining = maxSellLimit - todaySold;
|
||||||
|
int saleAmount = item.getEffectiveSellPrice() * total;
|
||||||
|
if (saleAmount > remaining) {
|
||||||
|
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
|
||||||
|
String msg = decodeHtml(maxSellMessage);
|
||||||
|
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
|
||||||
|
String resetTime;
|
||||||
|
if (earliestTime > 0) {
|
||||||
|
resetTime = new java.text.SimpleDateFormat("HH:mm")
|
||||||
|
.format(new java.util.Date(earliestTime + 3600000L));
|
||||||
|
} else {
|
||||||
|
resetTime = "现在";
|
||||||
|
}
|
||||||
|
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
|
||||||
|
p.sendMessage(msg);
|
||||||
|
}
|
||||||
|
// 提示消息关闭时不打印任何消息
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (total <= 0) {
|
if (total <= 0) {
|
||||||
p.sendMessage("§c你没有可出售的 §e"
|
p.sendMessage("§c你没有可出售的 §e"
|
||||||
+ item.getDisplayName());
|
+ item.getDisplayName());
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
# 非法域名后缀列表
|
||||||
|
# 每行一个后缀,支持以 * 开头表示匹配所有子域名
|
||||||
|
# 修改此文件后无需重启,插件会在每次检测时重新加载
|
||||||
|
# 默认包含常见非法注册域名后缀
|
||||||
|
|
||||||
|
com
|
||||||
|
cn
|
||||||
|
net
|
||||||
|
org
|
||||||
|
io
|
||||||
|
xyz
|
||||||
|
top
|
||||||
|
club
|
||||||
|
site
|
||||||
|
online
|
||||||
|
store
|
||||||
|
tech
|
||||||
|
info
|
||||||
|
biz
|
||||||
|
ru
|
||||||
|
tk
|
||||||
|
ml
|
||||||
|
ga
|
||||||
|
cf
|
||||||
|
cc
|
||||||
|
ws
|
||||||
|
tv
|
||||||
|
link
|
||||||
|
click
|
||||||
|
date
|
||||||
|
vip
|
||||||
|
party
|
||||||
|
fun
|
||||||
|
zone
|
||||||
|
space
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Sdf1_login
|
name: Sdf1_login
|
||||||
version: 2.5
|
version: 2.6
|
||||||
main: Sdf1_login.Main
|
main: Sdf1_login.Main
|
||||||
api-version: '1.21'
|
api-version: '1.21'
|
||||||
commands:
|
commands:
|
||||||
@@ -42,9 +42,9 @@ commands:
|
|||||||
签到:
|
签到:
|
||||||
description: 每日签到(中文)
|
description: 每日签到(中文)
|
||||||
usage: /签到
|
usage: /签到
|
||||||
#oa:
|
#oa:
|
||||||
# description: 申请删除账号
|
# description: 申请删除账号
|
||||||
# usage: /oa [玩家名]
|
# usage: /oa [玩家名]
|
||||||
删除账号:
|
删除账号:
|
||||||
description: 申请删除账号(中文)
|
description: 申请删除账号(中文)
|
||||||
usage: /删除账号 [玩家名]
|
usage: /删除账号 [玩家名]
|
||||||
@@ -97,11 +97,14 @@ commands:
|
|||||||
description: 打开商店
|
description: 打开商店
|
||||||
usage: /商店
|
usage: /商店
|
||||||
# testorder:
|
# testorder:
|
||||||
# description: 测试订单中心
|
# description: 测试订单中心
|
||||||
# usage: /testorder
|
# usage: /testorder
|
||||||
protect:
|
protect:
|
||||||
description: 区域保护
|
description: 区域保护
|
||||||
usage: /protect
|
usage: /protect
|
||||||
|
传送:
|
||||||
|
description: 传送至领地
|
||||||
|
usage: /传送 <领地名>
|
||||||
update:
|
update:
|
||||||
description: 检查所有插件更新(集群控制)
|
description: 检查所有插件更新(集群控制)
|
||||||
usage: /update
|
usage: /update
|
||||||
@@ -111,3 +114,42 @@ commands:
|
|||||||
控制台:
|
控制台:
|
||||||
description: 生成Web端登录令牌(中文版)
|
description: 生成Web端登录令牌(中文版)
|
||||||
usage: /控制台
|
usage: /控制台
|
||||||
|
tpa:
|
||||||
|
description: tp传送指令
|
||||||
|
usage: /tpa
|
||||||
|
tpaccept:
|
||||||
|
description: 接受传送
|
||||||
|
usage: /tpaccept
|
||||||
|
tpdeny:
|
||||||
|
description: 拒绝传送
|
||||||
|
usage: /tpdeny
|
||||||
|
tpauto:
|
||||||
|
description: 自动接收传送
|
||||||
|
usage: /tpauto
|
||||||
|
tpacancel:
|
||||||
|
description: 取消传送请求
|
||||||
|
usage: /tpacancel
|
||||||
|
tpaall:
|
||||||
|
description: 请求传送所有人
|
||||||
|
usage: /tpaall
|
||||||
|
tpahere:
|
||||||
|
description: 请求传送ta到自己身边
|
||||||
|
usage: /tpahere
|
||||||
|
sethome:
|
||||||
|
description: 设置一个家
|
||||||
|
usage: /sethome <名称>
|
||||||
|
delhome:
|
||||||
|
description: 删除一个家
|
||||||
|
usage: /delhome <名称>
|
||||||
|
home:
|
||||||
|
description: 传送到家
|
||||||
|
usage: /home [名称]
|
||||||
|
homes:
|
||||||
|
description: 查看所有家
|
||||||
|
usage: /homes
|
||||||
|
group:
|
||||||
|
description: 用户组管理
|
||||||
|
usage: /group <buy|renew|info> [组名]
|
||||||
|
back:
|
||||||
|
description: 快速返回最近记录点
|
||||||
|
usage: /back
|
||||||
|
|||||||
Reference in New Issue
Block a user