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/
|
||||
新建*.txt
|
||||
群戏归类.txt
|
||||
upload_sftp.py
|
||||
|
||||
# PHP Web 后端
|
||||
web/Web.db
|
||||
web/debug_log_tail.txt
|
||||
web/upload_debug.ps1
|
||||
web/api/*.bak*
|
||||
# 敏感文件 - 禁止提交上传/调试脚本
|
||||
web/upload_*.php
|
||||
web/self_update.php
|
||||
web/version_check.php
|
||||
web/debug_*.php
|
||||
web/db/debug_*.php
|
||||
|
||||
# Windows 保留设备名(无法用普通 rm 删除)
|
||||
nul
|
||||
|
||||
@@ -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) | 可选,推荐 | 云背包,积分商城可购买背包容量
|
||||
| EssentialsX | 可选 | 主流经济系统
|
||||
| [sdf1](https://github.com/ypsdf1/sdf1_plugn) | 可选,推荐 | CDK兑换系统
|
||||
| [CY_beibao](https://git.ypshidifu.cn/youpaishidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 |
|
||||
| EssentialsX | 可选 | 主流经济系统 |
|
||||
| [sdf1](https://git.ypshidifu.cn/youpaishidifu/Sdf1_plugin) | 可选,推荐 | CDK兑换系统 |
|
||||
|
||||
|
||||
|
||||
### 2.1 下载
|
||||
您可以从GitHub和Gitee下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||
您可以从GitHub和Gitee、gitcode下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||
|
||||
### 2.2. 配置文件
|
||||
|
||||
@@ -505,6 +506,47 @@ http-port=443
|
||||
|
||||
```
|
||||
|
||||
## php端说明:
|
||||
1. 默认密码ypshidifu2026<br>
|
||||
2. 工单系统全面支持markdown语法:
|
||||
```md
|
||||
# 一级标题
|
||||
## 二级标题
|
||||
### 三级标题
|
||||
#### 四级标题
|
||||
##### 五级标题
|
||||
###### 六级标题
|
||||
|
||||
表格语法
|
||||
|
||||
| 表格列1 | 表格列2 |
|
||||
| --- | --- |
|
||||
| 表格1 | 表格2 |
|
||||
|
||||
>单嵌套语法
|
||||
|
||||
>嵌套块
|
||||
>
|
||||
>>语法
|
||||
|
||||
 图片语法
|
||||
[]() 超链接语法
|
||||
<br>换行语法
|
||||
代码块语法
|
||||
```
|
||||
## 工单状态:
|
||||
|
||||
| 状态名 | 是否可回复 | 备注说明 |
|
||||
| :---: | :---: | :---: |
|
||||
| 已提交 | 可回复 | 初始化状态 |
|
||||
| 已回复 | 可回复 | 玩家自己回复了/管理员/服务商回复了 |
|
||||
| 已完结 | 不可回复 | 管理员驳回了工单请求 |
|
||||
| 已撤销 | 不可回复 | 用户撤销了工单请求 |
|
||||
|
||||
## 服务商说明
|
||||
- 若您需要授权某个玩家为服务商,请登录游戏,打开工单系统-管理面板-添加服务商,然后输入他的名字即可邀请服务商
|
||||
|
||||
|
||||
## 运行截图
|
||||
|
||||
主界面
|
||||
@@ -563,8 +605,8 @@ http-port=443
|
||||
商城退单
|
||||

|
||||
|
||||
|
||||
## 4. 下载地址
|
||||
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
|
||||
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
||||
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.MilkBowl</groupId>
|
||||
<artifactId>VaultAPI</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import org.bukkit.entity.Player;
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class BondManager {
|
||||
|
||||
@@ -14,20 +13,20 @@ public class BondManager {
|
||||
|
||||
// ===== 交易监听器 =====
|
||||
public interface TransactionListener {
|
||||
void onTransactionLogged(String playerName, String type, int amount);
|
||||
void onTransaction(String playerName, String type, int amount);
|
||||
}
|
||||
private final List<TransactionListener> transactionListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<TransactionListener> transactionListeners = new ArrayList<>();
|
||||
|
||||
public void addTransactionListener(TransactionListener listener) {
|
||||
transactionListeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyTransactionListeners(String playerName, String type, int amount) {
|
||||
private void fireTransactionEvent(String playerName, String type, int amount) {
|
||||
for (TransactionListener listener : transactionListeners) {
|
||||
try {
|
||||
listener.onTransactionLogged(playerName, type, amount);
|
||||
listener.onTransaction(playerName, type, amount);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[BondManager] 交易监听器异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,11 +381,11 @@ public class BondManager {
|
||||
ps.setLong(9, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
// 触发交易监听器
|
||||
fireTransactionEvent(player, type, amount);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// ★ 通知监听器:交易已记录
|
||||
notifyTransactionListeners(player, type, amount);
|
||||
}
|
||||
|
||||
|
||||
@@ -721,40 +720,87 @@ public class BondManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 同步用:获取指定时间之后的交易 ========================
|
||||
// ===== 获取玩家最近1小时出售总额 =====
|
||||
public int getTodaySellTotal(String player) {
|
||||
return getSellTotalInWindow(player, System.currentTimeMillis() - 3600000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间之后的所有交易记录(用于推送到Web端)
|
||||
* 使用时间而非ID追踪,确保shop_buy合并后的UPDATE记录也能被同步
|
||||
* @param afterTime 上次同步的最晚时间(毫秒),0表示从头开始
|
||||
* @return 交易记录列表
|
||||
*/
|
||||
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
private int getSellTotalInWindow(String player, long since) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE time > ? "
|
||||
+ "ORDER BY time ASC "
|
||||
+ "LIMIT 200");
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM bond_transaction "
|
||||
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, since);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int total = 0;
|
||||
if (rs.next()) {
|
||||
total = rs.getInt(1);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return total;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家最近1小时出售窗口中最早交易的时间戳。
|
||||
* 返回0表示窗口内无交易。
|
||||
*/
|
||||
public long getEarliestSellTimeInWindow(String player) {
|
||||
long since = System.currentTimeMillis() - 3600000L;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT MIN(time) FROM bond_transaction "
|
||||
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, since);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
long earliest = 0;
|
||||
if (rs.next()) {
|
||||
earliest = rs.getLong(1);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return earliest;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间之后的所有交易记录(用于Web同步)
|
||||
*/
|
||||
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
|
||||
List<Map<String, Object>> transactions = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
|
||||
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
|
||||
ps.setLong(1, afterTime);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("player_name", rs.getString("player_name"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount", rs.getInt("amount"));
|
||||
row.put("target_player", rs.getString("target_player"));
|
||||
row.put("operator", rs.getString("operator"));
|
||||
row.put("reason", rs.getString("reason"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("balance_after", rs.getInt("balance_after"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
Map<String, Object> tx = new LinkedHashMap<>();
|
||||
tx.put("player_name", rs.getString("player_name"));
|
||||
tx.put("type", rs.getString("type"));
|
||||
tx.put("amount", rs.getInt("amount"));
|
||||
tx.put("target_player", rs.getString("target_player"));
|
||||
tx.put("operator", rs.getString("operator"));
|
||||
tx.put("reason", rs.getString("reason"));
|
||||
tx.put("balance_before", rs.getInt("balance_before"));
|
||||
tx.put("balance_after", rs.getInt("balance_after"));
|
||||
tx.put("time", rs.getLong("time"));
|
||||
transactions.add(tx);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return list;
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,64 +170,29 @@ public class CDKManager {
|
||||
if ("points".equals(input.type)) {
|
||||
handlePointsInput(p, msg.trim(),
|
||||
input.context);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("gift_target".equals(input.type)) {
|
||||
// 赠予用户组:输入目标玩家名
|
||||
String targetName = msg.trim();
|
||||
if (!targetName.isEmpty()) {
|
||||
GUIManager guiMgr = plugin.getGui();
|
||||
if (guiMgr != null) {
|
||||
guiMgr.handleGiftTargetInput(p, targetName);
|
||||
}
|
||||
} else {
|
||||
p.sendMessage("§c请输入有效的玩家名");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEconInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
net.milkbowl.vault.economy.Economy econ
|
||||
= null;
|
||||
try {
|
||||
var reg = plugin.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault
|
||||
.economy
|
||||
.Economy.class);
|
||||
if (reg != null)
|
||||
econ = reg.getProvider();
|
||||
} catch (Exception ignored) {}
|
||||
if (econ == null) {
|
||||
admin.sendMessage("§c经济系统不可用");
|
||||
return;
|
||||
}
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String numStr =
|
||||
msg.substring(1).trim();
|
||||
double amount =
|
||||
Double.parseDouble(numStr);
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
if (msg.startsWith("-")) {
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp == null) {
|
||||
admin.sendMessage("§c" + target
|
||||
+ " 不在线");
|
||||
return;
|
||||
}
|
||||
if (!econ.has(tp, amount)) {
|
||||
admin.sendMessage("§c余额不足");
|
||||
return;
|
||||
}
|
||||
econ.withdrawPlayer(tp, amount);
|
||||
admin.sendMessage("§a扣款成功");
|
||||
} else {
|
||||
econ.depositPlayer(target, amount);
|
||||
admin.sendMessage("§a到账成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
// Economy已移除,所有经济操作改用债券系统
|
||||
admin.sendMessage("§c[经济操作] 功能已移除,请使用债券系统");
|
||||
}
|
||||
|
||||
private void handleBondInput(Player admin,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.*;
|
||||
@@ -34,10 +38,484 @@ public class ChatFilterManager {
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, String> messages =
|
||||
new LinkedHashMap<>();
|
||||
// ★ 广告机检测:记录每个玩家发送链接的频率
|
||||
private final Map<String, List<Long>> linkSendHistory =
|
||||
new ConcurrentHashMap<>();
|
||||
// ★ 挂机检测
|
||||
private final Map<String, Long> playerLastActiveTime =
|
||||
new ConcurrentHashMap<>();
|
||||
// ★ 管理员邮件地址
|
||||
private String adminEmail = "admin@example.com";
|
||||
private boolean notifyAdmin = true;
|
||||
private boolean notifyAll = false;
|
||||
private int muteDuration = 300;
|
||||
private boolean enabled = true;
|
||||
|
||||
// ★ 非法域名后缀列表(热更新)
|
||||
private final Set<String> illegalDomainSuffixes = new HashSet<>();
|
||||
|
||||
// ============================================================
|
||||
// ★ 新玩家验证码系统
|
||||
// ============================================================
|
||||
|
||||
/** 已验证过的玩家名单(永久,跨session持久化到内存) */
|
||||
private final Set<String> verifiedPlayers = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** 验证码数据存储(public供Main.java访问清理) */
|
||||
public final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
|
||||
|
||||
/** 验证码类型对外公开 */
|
||||
public enum VerificationType {
|
||||
MATH, // 数学题
|
||||
GUI // GUI物品选择
|
||||
}
|
||||
|
||||
static class VerificationData {
|
||||
long createTime;
|
||||
int a, b, op; // 0=加法, 1=减法, 2=乘法
|
||||
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
|
||||
VerificationType type; // 验证码类型
|
||||
boolean completed;
|
||||
|
||||
VerificationData(int a, int b, int op, VerificationType type) {
|
||||
this.createTime = System.currentTimeMillis();
|
||||
this.a = a; this.b = b; this.op = op;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - createTime > 30000;
|
||||
}
|
||||
}
|
||||
|
||||
/** 验证码结果:通过/失败 */
|
||||
public enum VerificationResult {
|
||||
NEED_VERIFICATION, // 需要验证码
|
||||
PENDING, // 验证码进行中
|
||||
VERIFIED, // 已验证
|
||||
FAILED // 验证失败(消息吞噬)
|
||||
}
|
||||
|
||||
/** 验证码答案缓存,供外部判断是否为答案 */
|
||||
public boolean isPendingVerificationAnswer(String playerName, String msg) {
|
||||
VerificationData vd = verificationData.get(playerName);
|
||||
if (vd == null || vd.completed) return false;
|
||||
// 尝试作为答案检查
|
||||
VerificationResult result = checkMathAnswer(playerName, msg);
|
||||
return result != VerificationResult.FAILED; // 通过或进行中都是有效的答案输入
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否需要验证码
|
||||
*/
|
||||
public VerificationResult checkNewPlayerVerification(Player p) {
|
||||
String name = p.getName();
|
||||
|
||||
// ★ 永久已验证的玩家 → 直接放行
|
||||
if (verifiedPlayers.contains(name)) {
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
// 正在验证中(等待答案)
|
||||
if (verificationData.containsKey(name)) {
|
||||
VerificationData vd = verificationData.get(name);
|
||||
if (vd.completed) {
|
||||
verifiedPlayers.add(name);
|
||||
verificationData.remove(name);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
// 验证码是否过期 — 过期后自动放行,不无限卡玩家
|
||||
if (vd.isExpired()) {
|
||||
verificationData.remove(name);
|
||||
verifiedPlayers.add(name);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
return VerificationResult.PENDING;
|
||||
}
|
||||
|
||||
// 检查玩家加入时间是否≤1小时
|
||||
if (isNewPlayerUnder1Hour(p)) {
|
||||
return VerificationResult.NEED_VERIFICATION;
|
||||
}
|
||||
|
||||
// 老玩家:直接永久验证通过
|
||||
verifiedPlayers.add(name);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
/** 全局共享的Random实例,避免频繁创建产生相同序列 */
|
||||
private static final ThreadLocal<Random> sharedRandom = ThreadLocal.withInitial(Random::new);
|
||||
|
||||
/**
|
||||
* 生成随机验证码:数学题或GUI随机50%概率
|
||||
*/
|
||||
public void generateMathVerification(String playerName) {
|
||||
Random rand = sharedRandom.get();
|
||||
// 50% 概率数学题,50% 概率GUI
|
||||
if (rand.nextBoolean()) {
|
||||
generateMathChallenge(playerName, rand);
|
||||
} else {
|
||||
generateGUIChallenge(playerName, rand);
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成数学题验证码 */
|
||||
private void generateMathChallenge(String playerName, Random rand) {
|
||||
int op = rand.nextInt(3); // 0=加, 1=减, 2=乘
|
||||
int a, b;
|
||||
|
||||
switch (op) {
|
||||
case 0: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
|
||||
case 1: a = rand.nextInt(10) + 2; b = rand.nextInt(a) + 1; break;
|
||||
default: a = rand.nextInt(10) + 1; b = rand.nextInt(10) + 1; break;
|
||||
}
|
||||
|
||||
VerificationData vd = new VerificationData(a, b, op, VerificationType.MATH);
|
||||
verificationData.put(playerName, vd);
|
||||
|
||||
String opSymbol = switch (op) {
|
||||
case 0 -> "+";
|
||||
case 1 -> "-";
|
||||
default -> "×";
|
||||
};
|
||||
|
||||
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Player p = org.bukkit.Bukkit.getPlayer(playerName);
|
||||
if (p != null && p.isOnline()) {
|
||||
p.sendMessage("§e§l[验证码] §f请回答: §e" + a + " " + opSymbol + " " + b + " = ?");
|
||||
p.sendMessage("§7(30秒内输入答案)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 中文物品名映射表 */
|
||||
private static final Map<String, String> MATERIAL_CN = new HashMap<>();
|
||||
static {
|
||||
MATERIAL_CN.put("EMERALD", "绿宝石");
|
||||
MATERIAL_CN.put("DIAMOND", "钻石");
|
||||
MATERIAL_CN.put("IRON_INGOT", "铁锭");
|
||||
MATERIAL_CN.put("GOLD_INGOT", "金锭");
|
||||
MATERIAL_CN.put("STONE", "石头");
|
||||
MATERIAL_CN.put("COBBLESTONE", "圆石");
|
||||
MATERIAL_CN.put("SAND", "沙子");
|
||||
MATERIAL_CN.put("GRAVEL", "砂砾");
|
||||
MATERIAL_CN.put("REDSTONE", "红石粉");
|
||||
MATERIAL_CN.put("LAPIS_LAZULI", "青金石");
|
||||
MATERIAL_CN.put("QUARTZ", "石英");
|
||||
MATERIAL_CN.put("APPLE", "苹果");
|
||||
MATERIAL_CN.put("BOWL", "碗");
|
||||
MATERIAL_CN.put("CAKE", "蛋糕");
|
||||
MATERIAL_CN.put("BOOK", "书");
|
||||
MATERIAL_CN.put("FLINT", "燧石");
|
||||
MATERIAL_CN.put("FLINT_AND_STEEL", "打火石");
|
||||
MATERIAL_CN.put("LEATHER_HELMET", "皮革头盔");
|
||||
MATERIAL_CN.put("LEATHER_CHESTPLATE", "皮革上衣");
|
||||
MATERIAL_CN.put("LEATHER_LEGGINGS", "皮革护腿");
|
||||
MATERIAL_CN.put("LEATHER_BOOTS", "皮革靴子");
|
||||
MATERIAL_CN.put("GOLDEN_APPLE", "附魔金苹果");
|
||||
MATERIAL_CN.put("OBSIDIAN", "黑曜石");
|
||||
MATERIAL_CN.put("DIAMOND_HELMET", "钻石头盔");
|
||||
MATERIAL_CN.put("DIAMOND_CHESTPLATE", "钻石护甲");
|
||||
MATERIAL_CN.put("DIAMOND_LEGGINGS", "钻石护腿");
|
||||
MATERIAL_CN.put("DIAMOND_BOOTS", "钻石靴子");
|
||||
MATERIAL_CN.put("IRON_HELMET", "铁头盔");
|
||||
MATERIAL_CN.put("IRON_CHESTPLATE", "铁护甲");
|
||||
MATERIAL_CN.put("IRON_LEGGINGS", "铁护腿");
|
||||
MATERIAL_CN.put("IRON_BOOTS", "铁靴子");
|
||||
MATERIAL_CN.put("WOODEN_SWORD", "木剑");
|
||||
MATERIAL_CN.put("STONE_SWORD", "石剑");
|
||||
MATERIAL_CN.put("IRON_SWORD", "铁剑");
|
||||
MATERIAL_CN.put("GOLDEN_SWORD", "金剑");
|
||||
MATERIAL_CN.put("DIAMOND_SWORD", "钻石剑");
|
||||
MATERIAL_CN.put("GREEN_CONCRETE", "绿色混凝土");
|
||||
MATERIAL_CN.put("LIME_CONCRETE", "淡绿色混凝土");
|
||||
MATERIAL_CN.put("JUNGLE_PLANKS", "丛林木板");
|
||||
MATERIAL_CN.put("ACACIA_PLANKS", "金合欢木板");
|
||||
MATERIAL_CN.put("BLAZE_ROD", "烈焰棒");
|
||||
MATERIAL_CN.put("MAGMA_CREAM", "熔浆膏");
|
||||
MATERIAL_CN.put("CLAY_BALL", "黏土球");
|
||||
MATERIAL_CN.put("MUSIC_DISC_13", "音乐唱片C13");
|
||||
MATERIAL_CN.put("MUSIC_DISC_PIGSTEP", "音乐唱片PigStep");
|
||||
MATERIAL_CN.put("END_CRYSTAL", "末影水晶");
|
||||
MATERIAL_CN.put("GLOWSTONE", "萤石粉");
|
||||
MATERIAL_CN.put("DRAGON_EGG", "龙蛋");
|
||||
MATERIAL_CN.put("NETHERITE_SCRAP", "下界合金碎片");
|
||||
MATERIAL_CN.put("CREEPER_HEAD", "爬行者头颅");
|
||||
MATERIAL_CN.put("GOLDEN_APPLE", "金苹果");
|
||||
MATERIAL_CN.put("ENCHANTED_GOLDEN_APPLE", "附魔金苹果");
|
||||
}
|
||||
|
||||
/** 生成中文物品名 */
|
||||
private String toChineseName(String materialName) {
|
||||
String upper = materialName.toUpperCase();
|
||||
return MATERIAL_CN.getOrDefault(upper, materialName.replace("_", " "));
|
||||
}
|
||||
|
||||
/** 生成GUI验证码:N个物品+1个正确答案,30秒超时 */
|
||||
private void generateGUIChallenge(String playerName, Random rand) {
|
||||
// 丰富物品池,保证足够多样性
|
||||
Material[] materials = {Material.STONE, Material.COBBLESTONE, Material.GRAVEL,
|
||||
Material.SAND, Material.CLAY_BALL, Material.REDSTONE,
|
||||
Material.LAPIS_LAZULI, Material.QUARTZ, Material.IRON_INGOT,
|
||||
Material.GOLD_INGOT, Material.DIAMOND, Material.EMERALD,
|
||||
Material.GREEN_CONCRETE, Material.LIME_CONCRETE,
|
||||
Material.JUNGLE_PLANKS, Material.ACACIA_PLANKS,
|
||||
Material.BLAZE_ROD, Material.CREEPER_HEAD, Material.DRAGON_EGG,
|
||||
Material.NETHERITE_SCRAP, Material.END_CRYSTAL, Material.GLOWSTONE,
|
||||
Material.MAGMA_CREAM, Material.MUSIC_DISC_13, Material.MUSIC_DISC_PIGSTEP,
|
||||
Material.BOOK, Material.BOWL, Material.CAKE, Material.FLINT,
|
||||
Material.FLINT_AND_STEEL, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
|
||||
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.APPLE,
|
||||
Material.GOLDEN_APPLE, Material.ENCHANTED_GOLDEN_APPLE,
|
||||
Material.OBSIDIAN, Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE,
|
||||
Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.IRON_HELMET,
|
||||
Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS,
|
||||
Material.WOODEN_SWORD, Material.STONE_SWORD, Material.IRON_SWORD,
|
||||
Material.GOLDEN_SWORD, Material.DIAMOND_SWORD};
|
||||
|
||||
// 随机选1个正确答案
|
||||
Material correctItem = materials[rand.nextInt(materials.length)];
|
||||
String chineseName = toChineseName(correctItem.name());
|
||||
|
||||
// 选N-1个干扰项(5~10个物品,其中1个正确,其余干扰)
|
||||
int totalItems = 5 + rand.nextInt(6); // 5~10
|
||||
List<Material> distractorPool = new ArrayList<>();
|
||||
for (Material m : materials) {
|
||||
if (m != correctItem) distractorPool.add(m);
|
||||
}
|
||||
Collections.shuffle(distractorPool, rand);
|
||||
|
||||
List<Material> itemsToPlace = new ArrayList<>();
|
||||
itemsToPlace.add(correctItem);
|
||||
for (int i = 0; i < Math.min(totalItems - 1, distractorPool.size()); i++) {
|
||||
itemsToPlace.add(distractorPool.get(i));
|
||||
}
|
||||
|
||||
// 随机打乱物品位置
|
||||
Collections.shuffle(itemsToPlace, rand);
|
||||
|
||||
// 存储:正确答案的Material名(用英文做校验),显示用中文名
|
||||
VerificationData vd = new VerificationData(0, 0, 0, VerificationType.GUI);
|
||||
vd.guiTargets = correctItem.name(); // 校验用英文
|
||||
vd.type = VerificationType.GUI;
|
||||
verificationData.put(playerName, vd);
|
||||
|
||||
org.bukkit.Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Player p = org.bukkit.Bukkit.getPlayer(playerName);
|
||||
if (p != null && p.isOnline()) {
|
||||
try {
|
||||
// 在GUI标题中明确显示中文目标物品名
|
||||
Inventory inv = Bukkit.createInventory(null, 27, "§e§l点击" + chineseName + "完成验证");
|
||||
|
||||
// 填充灰色玻璃半透明方块作为背景
|
||||
ItemStack glass = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
|
||||
ItemMeta gm = glass.getItemMeta();
|
||||
if (gm != null) { gm.setDisplayName(" "); glass.setItemMeta(gm); }
|
||||
for (int i = 0; i < 27; i++) inv.setItem(i, glass);
|
||||
|
||||
// 随机打乱物品放置位置(避开边框slot 0-8, 18-26, 9,14,19)
|
||||
List<Integer> availableSlots = Arrays.asList(9, 10, 11, 13, 14, 15, 17, 19, 20, 21);
|
||||
Collections.shuffle(availableSlots, rand);
|
||||
|
||||
int itemIdx = 0;
|
||||
for (int slot : availableSlots) {
|
||||
if (itemIdx >= itemsToPlace.size()) break;
|
||||
Material mat = itemsToPlace.get(itemIdx);
|
||||
String cnName = toChineseName(mat.name());
|
||||
inv.setItem(slot, mkItem(mat, "§f" + cnName, ""));
|
||||
itemIdx++;
|
||||
}
|
||||
|
||||
p.openInventory(inv);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning("[验证码] GUI挑战生成失败: " + ex.getMessage());
|
||||
// 降级为数学题
|
||||
generateMathChallenge(playerName, rand);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 辅助方法:创建带显示名的物品
|
||||
private ItemStack mkItem(Material mat, String displayName, String lore) {
|
||||
ItemStack stack = new ItemStack(mat);
|
||||
ItemMeta meta = stack.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(displayName);
|
||||
List<String> lores = new ArrayList<>();
|
||||
lores.add(lore);
|
||||
meta.setLore(lores);
|
||||
stack.setItemMeta(meta);
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
public VerificationResult checkAnswer(String playerName, String answerStr) {
|
||||
VerificationData vd = verificationData.get(playerName);
|
||||
if (vd == null) return VerificationResult.VERIFIED;
|
||||
|
||||
if (vd.isExpired()) {
|
||||
verificationData.remove(playerName);
|
||||
verifiedPlayers.add(playerName);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
if (vd.type == VerificationType.MATH) {
|
||||
return checkMathAnswer(playerName, answerStr);
|
||||
}
|
||||
|
||||
// GUI验证码 — 聊天输入不作为答案,返回PENDING等待GUI点击
|
||||
return VerificationResult.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家回答是否正确
|
||||
* @deprecated 使用 checkAnswer 代替
|
||||
*/
|
||||
@Deprecated
|
||||
public VerificationResult checkMathAnswer(String playerName, String answerStr) {
|
||||
VerificationData vd = verificationData.get(playerName);
|
||||
if (vd == null) return VerificationResult.VERIFIED; // 无验证码 → 放行
|
||||
|
||||
if (vd.isExpired()) {
|
||||
verificationData.remove(playerName);
|
||||
verifiedPlayers.add(playerName); // 过期后不再要求
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
if (vd.type == VerificationType.MATH) {
|
||||
try {
|
||||
int ans = Integer.parseInt(answerStr.trim());
|
||||
int expected;
|
||||
switch (vd.op) {
|
||||
case 0 -> expected = vd.a + vd.b;
|
||||
case 1 -> expected = vd.a - vd.b;
|
||||
default -> expected = vd.a * vd.b;
|
||||
}
|
||||
|
||||
if (ans == expected) {
|
||||
vd.completed = true;
|
||||
verificationData.remove(playerName);
|
||||
verifiedPlayers.add(playerName); // 永久记录 - 验证通过
|
||||
return VerificationResult.VERIFIED;
|
||||
} else {
|
||||
// ★ 回答错误 → 不清除验证码也不加入verifiedPlayers,让玩家重新答题
|
||||
return VerificationResult.FAILED;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 非数字答案 → 同样不放行,重新答题
|
||||
return VerificationResult.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
// GUI验证码 — answerStr 为空时表示未回答
|
||||
if (answerStr == null || answerStr.isEmpty()) {
|
||||
return VerificationResult.PENDING;
|
||||
}
|
||||
|
||||
return VerificationResult.FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理GUI物品点击验证码
|
||||
* @param playerName 玩家名
|
||||
* @param clickedItemName 点击的物品Material名(如"STONE")
|
||||
* @return 验证结果
|
||||
*/
|
||||
public VerificationResult checkGUIClick(String playerName, String clickedItemName) {
|
||||
String name = playerName;
|
||||
VerificationData vd = verificationData.get(name);
|
||||
if (vd == null) return VerificationResult.VERIFIED;
|
||||
if (!VerificationType.GUI.equals(vd.type)) return VerificationResult.PENDING;
|
||||
|
||||
if (vd.isExpired()) {
|
||||
verificationData.remove(name);
|
||||
verifiedPlayers.add(name);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
// ★ 关键修复:点击的物品名必须等于目标物品名才通过
|
||||
if (clickedItemName != null && clickedItemName.equals(vd.guiTargets)) {
|
||||
vd.completed = true;
|
||||
verificationData.remove(name);
|
||||
verifiedPlayers.add(name);
|
||||
return VerificationResult.VERIFIED;
|
||||
}
|
||||
|
||||
// 点击错误物品 → 消息拦截
|
||||
return VerificationResult.FAILED;
|
||||
}
|
||||
|
||||
// ★ 临时缓存玩家未验证的消息(N+1 机制:缓存列表,答对第N题后释放N条)
|
||||
public final Map<String, List<String>> pendingMessages = new ConcurrentHashMap<>();
|
||||
|
||||
/** 缓存玩家消息,等验证通过时代为广播 */
|
||||
public void cachePendingMessage(String playerName, String message) {
|
||||
pendingMessages.computeIfAbsent(playerName, k -> new ArrayList<>()).add(message);
|
||||
}
|
||||
|
||||
/** 广播所有缓存的消息(N+1机制:广播缓存的N-1条 + 当前第N条) */
|
||||
public void broadcastCachedMessages(String playerName) {
|
||||
List<String> messages = pendingMessages.remove(playerName);
|
||||
if (messages != null) {
|
||||
for (String msg : messages) {
|
||||
Bukkit.broadcastMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除玩家缓存的消息(验证失败时调用) */
|
||||
public void clearPendingMessages(String playerName) {
|
||||
pendingMessages.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的验证码
|
||||
*/
|
||||
public void cleanupExpiredVerifications() {
|
||||
long now = System.currentTimeMillis();
|
||||
verificationData.entrySet().removeIf(e -> {
|
||||
if (e.getValue().isExpired() && !e.getValue().completed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否为新玩家(register_time 在1小时内)
|
||||
*/
|
||||
public boolean isNewPlayerUnder1Hour(Player p) {
|
||||
if (p == null || !p.isOnline()) return false;
|
||||
try {
|
||||
Main mainPlugin = (Main) plugin;
|
||||
DatabaseManager db = mainPlugin.getDb();
|
||||
if (db == null) return false; // 查不到就不拦截,直接放行
|
||||
|
||||
// register_time 在数据库存的是秒级时间戳
|
||||
Object regTimeObj = db.getField(p.getName(), "register_time");
|
||||
if (regTimeObj == null) return false; // 没注册记录 → 放行
|
||||
|
||||
long regTimeSeconds = ((Number) regTimeObj).longValue();
|
||||
if (regTimeSeconds == 0) return false; // 注册时间为0 → 放行
|
||||
|
||||
long regTimeMs = regTimeSeconds * 1000;
|
||||
long elapsed = System.currentTimeMillis() - regTimeMs;
|
||||
boolean isNew = elapsed < 3600000;
|
||||
|
||||
// 日志调试:打印注册时间差值
|
||||
if (isNew) {
|
||||
plugin.getLogger().info("[验证码] 新玩家: " + p.getName() + " 注册" + (elapsed/1000) + "秒前");
|
||||
}
|
||||
|
||||
return isNew;
|
||||
} catch (Exception e) {
|
||||
return false; // 出错时默认放行,不卡玩家
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================== 广告机检测 ====================
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -50,12 +528,15 @@ public class ChatFilterManager {
|
||||
|
||||
private static final Pattern URL_PATTERN =
|
||||
Pattern.compile(
|
||||
"(?i)(?:https?://\\S+)"
|
||||
+ "|(?:(?:[a-zA-Z0-9]"
|
||||
+ "(?:[a-zA-Z0-9\\-]*"
|
||||
+ "[a-zA-Z0-9])?\\.)+"
|
||||
+ "[a-zA-Z]{2,})"
|
||||
+ "(?:/\\S*)?");
|
||||
"(?i)"
|
||||
// 1. 标准URL: http://xxx.com/...
|
||||
+ "(?:https?://\\S+)"
|
||||
// 2. 点分隔域名: xxx.com, my.site.cn
|
||||
+ "|(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,})(?:/\\S*)?"
|
||||
// 3. 下划线分隔域名: xxx_com, my_site.cn, 123_ypshidifu.cn
|
||||
+ "|(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)"
|
||||
+ "(?:[_.][a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*"
|
||||
+ "[_.][a-zA-Z]{2,}");
|
||||
private static final Pattern DOT_VARIANTS =
|
||||
Pattern.compile(
|
||||
"[\uff0e\u3002\u2025\u2026\u00b7]");
|
||||
@@ -609,4 +1090,178 @@ public class ChatFilterManager {
|
||||
|| s.contains("启用")
|
||||
|| s.contains("是");
|
||||
}
|
||||
|
||||
// ==================== 广告机检测 ====================
|
||||
|
||||
/**
|
||||
* 检查消息是否包含第三方推广内容(邮件、游戏内推广等)
|
||||
*/
|
||||
public boolean isAdvertisingContent(String msg) {
|
||||
String lower = msg.toLowerCase();
|
||||
// 检测关键词
|
||||
String[] keywords = {"群", "群号", "QQ", "qq群", "加群", "交流群", "服务器", "开服", "联机", "白嫖", "免费送", "折扣", "打折", "代充", "充值优惠", "外挂", "脚本", "辅助", "破解"};
|
||||
for (String kw : keywords) {
|
||||
if (lower.contains(kw)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录玩家发送链接的行为,检测广告机
|
||||
* @return 如果检测到广告机返回true
|
||||
*/
|
||||
public boolean checkAdBotBehavior(String playerName, String msg) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 提取URL
|
||||
List<String> urls = extractUrls(msg);
|
||||
if (urls.isEmpty()) return false;
|
||||
|
||||
// 检查是否全部非白名单
|
||||
boolean hasNonWhitelisted = false;
|
||||
for (String url : urls) {
|
||||
if (!isWhitelisted(url)) {
|
||||
hasNonWhitelisted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasNonWhitelisted) return false;
|
||||
|
||||
// 检查是否包含推广关键词
|
||||
if (!isAdvertisingContent(msg)) return false;
|
||||
|
||||
// 记录链接发送时间
|
||||
linkSendHistory.computeIfAbsent(playerName, k -> new ArrayList<>()).add(now);
|
||||
|
||||
// 清理超过10分钟前的记录
|
||||
List<Long> history = linkSendHistory.get(playerName);
|
||||
history.removeIf(t -> (now - t) > 600000);
|
||||
|
||||
// 10分钟内发送超过3次非白名单链接 → 广告机
|
||||
if (history.size() >= 3) {
|
||||
linkSendHistory.remove(playerName);
|
||||
return true; // 检测到广告机
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记玩家活跃(用于挂机检测)
|
||||
*/
|
||||
public void markPlayerActive(String playerName) {
|
||||
playerLastActiveTime.put(playerName, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否长时间挂机(超过5分钟)
|
||||
*/
|
||||
public boolean isPlayerIdle(String playerName, long idleThresholdMs) {
|
||||
Long lastActive = playerLastActiveTime.get(playerName);
|
||||
if (lastActive == null) return true;
|
||||
return (System.currentTimeMillis() - lastActive) > idleThresholdMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送管理员通知邮件
|
||||
*/
|
||||
public void sendAdminNotification(String subject, String body) {
|
||||
// 异步发送,不阻塞主线程
|
||||
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
EmailManager emailMgr = ((Main) plugin).getEmail();
|
||||
if (emailMgr != null) {
|
||||
emailMgr.sendBody(adminEmail, subject, body);
|
||||
plugin.getLogger().info("[广告机检测] 已发送管理员通知: " + subject);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[广告机检测] 发送管理员通知失败: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 非法域名后缀管理 ====================
|
||||
|
||||
/**
|
||||
* 加载非法域名后缀配置文件(支持热更新)
|
||||
* 配置文件: plugins/Sdf1_login/illegal_domains.txt
|
||||
* 每行一个后缀,如: com, cn, test 等
|
||||
*/
|
||||
public void loadIllegalDomains() {
|
||||
illegalDomainSuffixes.clear();
|
||||
File f = new File(plugin.getDataFolder(), "illegal_domains.txt");
|
||||
if (!f.exists()) {
|
||||
plugin.getLogger().info("[非法域名] 配置文件不存在,已加载默认内置后缀");
|
||||
// 内置默认后缀作为兜底
|
||||
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
|
||||
illegalDomainSuffixes.add(s.toLowerCase());
|
||||
}
|
||||
return;
|
||||
}
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
// 去掉可能的 * 前缀
|
||||
String suffix = line.replaceFirst("^\\*", "").toLowerCase();
|
||||
if (!suffix.isEmpty()) {
|
||||
illegalDomainSuffixes.add(suffix);
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[非法域名] 已加载 " + illegalDomainSuffixes.size() + " 个非法后缀(来自配置文件)");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[非法域名] 加载失败,使用内置默认后缀: " + e.getMessage());
|
||||
// 加载失败时用内置兜底
|
||||
for (String s : new String[]{"com", "cn", "net", "org", "io", "xyz", "top", "club", "site", "online", "store", "tech", "info", "biz"}) {
|
||||
illegalDomainSuffixes.add(s.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查名称是否包含非法域名格式(如 baidu_com, mysite.cn 等)
|
||||
*/
|
||||
public boolean containsIllegalDomain(String name) {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
String lowerName = name.toLowerCase();
|
||||
|
||||
// 支持两种格式:1) dot分隔 xxx.com 2) underscore分隔 xxx_com
|
||||
for (String separator : new String[]{".", "_"}) {
|
||||
int idx = lowerName.lastIndexOf(separator);
|
||||
if (idx < 0) continue;
|
||||
|
||||
String suffix = lowerName.substring(idx + 1);
|
||||
|
||||
// 去掉可能的前缀嵌套
|
||||
if (suffix.contains(separator)) {
|
||||
suffix = suffix.substring(suffix.lastIndexOf(separator) + 1);
|
||||
}
|
||||
|
||||
for (String domainSuffix : illegalDomainSuffixes) {
|
||||
if (suffix.equals(domainSuffix) && !suffix.isEmpty()) {
|
||||
// 前面必须有主体部分(排除纯后缀如 .com 本身)
|
||||
String before = lowerName.substring(0, idx);
|
||||
if (!before.isEmpty() && before.length() >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时检查空闲玩家
|
||||
*/
|
||||
public void checkIdlePlayers() {
|
||||
long idleThreshold = 5 * 60 * 1000L; // 5分钟
|
||||
org.bukkit.Bukkit.getOnlinePlayers().forEach(p -> {
|
||||
if (isPlayerIdle(p.getName(), idleThreshold)) {
|
||||
// 不再发送空闲消息,仅刷新计时
|
||||
playerLastActiveTime.put(p.getName(), System.currentTimeMillis()); // 刷新计时
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ public class ChatInputManager {
|
||||
name, "password_salt");
|
||||
String hash =
|
||||
PasswordUtils.hash(msg, salt);
|
||||
boolean mainOk =
|
||||
db.checkPassword(name, hash);
|
||||
if (mainOk) {
|
||||
String result = db.checkPasswordWithFallback(name, hash);
|
||||
if ("main".equals(result)) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "main";
|
||||
@@ -100,10 +99,7 @@ public class ChatInputManager {
|
||||
"§e请输入新密码:");
|
||||
return;
|
||||
}
|
||||
boolean tempOk =
|
||||
db.checkPasswordOrTemp(
|
||||
name, hash);
|
||||
if (tempOk) {
|
||||
if ("temp".equals(result)) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "temp";
|
||||
|
||||
@@ -149,18 +149,8 @@ public class CommissionManager {
|
||||
originalAmount * rate;
|
||||
if (commission <= 0) return;
|
||||
|
||||
// 通过 Vault 发放
|
||||
var rsp = main.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault
|
||||
.economy.Economy.class);
|
||||
if (rsp == null) return;
|
||||
net.milkbowl.vault.economy.Economy econ =
|
||||
rsp.getProvider();
|
||||
if (econ == null) return;
|
||||
|
||||
econ.depositPlayer(inviter, commission);
|
||||
// Economy已移除,佣金提成功能不可用
|
||||
// 邀请人可通过债券系统获取奖励
|
||||
|
||||
// 通知邀请人
|
||||
Player inviterP =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,6 +88,9 @@ public class DatabaseManager {
|
||||
"TEXT DEFAULT ''");
|
||||
safeAdd(st, "last_login_ip",
|
||||
"TEXT DEFAULT ''");
|
||||
// UI偏好:1=CLI(默认), 0=GUI
|
||||
safeAdd(st, "ui_mode",
|
||||
"INTEGER DEFAULT 1");
|
||||
|
||||
// ===== 背包备份表:安全迁移 =====
|
||||
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||
@@ -255,6 +258,30 @@ public class DatabaseManager {
|
||||
+ ")");
|
||||
safeAdd(st, "menu_snowball",
|
||||
"INTEGER DEFAULT 1");
|
||||
|
||||
// Home传送点表 — 只在不存在时创建,不删除已有数据
|
||||
st.execute("CREATE TABLE IF NOT EXISTS homes ("
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "home_name TEXT NOT NULL,"
|
||||
+ "world TEXT NOT NULL,"
|
||||
+ "x REAL NOT NULL,"
|
||||
+ "y REAL NOT NULL,"
|
||||
+ "z REAL NOT NULL,"
|
||||
+ "yaw REAL DEFAULT 0,"
|
||||
+ "pitch REAL DEFAULT 0,"
|
||||
+ "created_at INTEGER DEFAULT 0,"
|
||||
+ "PRIMARY KEY(player_name, home_name)"
|
||||
+ ")");
|
||||
safeAdd(st, "menu_snowball",
|
||||
"INTEGER DEFAULT 1");
|
||||
|
||||
// 传送请求表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS teleport_requests ("
|
||||
+ "sender TEXT NOT NULL,"
|
||||
+ "receiver TEXT NOT NULL,"
|
||||
+ "type TEXT NOT NULL DEFAULT 'tpa',"
|
||||
+ "timestamp INTEGER NOT NULL DEFAULT 0"
|
||||
+ ")");
|
||||
st.close();
|
||||
logger.info("[Sdf1_login] 数据库初始化完成");
|
||||
} catch (Exception e) {
|
||||
@@ -274,6 +301,56 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 自动续费偏好 ====================
|
||||
|
||||
/** 获取玩家的自动续费偏好 */
|
||||
public boolean getPlayerAutoRenew(String player, String group) {
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||
String sql = "SELECT auto_renew FROM player_auto_renew WHERE player_name = ? AND group_name = ?";
|
||||
PreparedStatement ps = db.prepareStatement(sql);
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, group);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) return rs.getInt("auto_renew") == 1;
|
||||
} catch (Exception ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 设置玩家的自动续费偏好 */
|
||||
public void setPlayerAutoRenew(String player, String group, boolean value) {
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||
String sql = "INSERT OR REPLACE INTO player_auto_renew (player_name, group_name, auto_renew) VALUES (?, ?, ?)";
|
||||
PreparedStatement ps = db.prepareStatement(sql);
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, group);
|
||||
ps.setInt(3, value ? 1 : 0);
|
||||
ps.executeUpdate();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/** 获取玩家所有自动续费开启的组 */
|
||||
public List<Map<String, Object>> getAutoRenewGroups(String player) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS player_auto_renew (player_name TEXT NOT NULL, group_name TEXT NOT NULL, auto_renew INTEGER DEFAULT 0, PRIMARY KEY(player_name, group_name))");
|
||||
String sql = "SELECT group_name FROM player_auto_renew WHERE player_name = ? AND auto_renew = 1";
|
||||
PreparedStatement ps = db.prepareStatement(sql);
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("group_name", rs.getString("group_name"));
|
||||
result.add(row);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (db != null && !db.isClosed())
|
||||
@@ -281,6 +358,51 @@ public class DatabaseManager {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
return db;
|
||||
}
|
||||
|
||||
// ==================== UI偏好 ====================
|
||||
|
||||
/**
|
||||
* 获取玩家UI偏好
|
||||
* @return 1=CLI(默认), 0=GUI
|
||||
*/
|
||||
public int getUiMode(String playerName) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT ui_mode FROM users WHERE player_name=?");
|
||||
ps.setString(1, playerName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int mode = 1; // 默认CLI
|
||||
if (rs.next()) {
|
||||
mode = rs.getInt("ui_mode");
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return mode;
|
||||
} catch (SQLException e) {
|
||||
return 1; // 默认CLI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置玩家UI偏好
|
||||
* @param mode 1=CLI, 0=GUI
|
||||
*/
|
||||
public void setUiMode(String playerName, int mode) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE users SET ui_mode=? WHERE player_name=?");
|
||||
ps.setInt(1, mode);
|
||||
ps.setString(2, playerName);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 静默
|
||||
}
|
||||
}
|
||||
// ==================== 菜单图标 ====================
|
||||
|
||||
public void saveMenuIcon(String name,
|
||||
@@ -438,9 +560,11 @@ public class DatabaseManager {
|
||||
? String.valueOf(ip) : null;
|
||||
}
|
||||
|
||||
public boolean checkPasswordOrTemp(
|
||||
String name, String hash) {
|
||||
// 先检查主密码
|
||||
/**
|
||||
* 验证密码(仅主密码)
|
||||
* @return true if matched
|
||||
*/
|
||||
public boolean checkPassword(String name, String hash) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT password_hash "
|
||||
@@ -453,8 +577,7 @@ public class DatabaseManager {
|
||||
rs.getString("password_hash");
|
||||
rs.close();
|
||||
ps.close();
|
||||
if (hash.equals(mainHash))
|
||||
return true;
|
||||
return hash.equals(mainHash);
|
||||
} else {
|
||||
rs.close();
|
||||
ps.close();
|
||||
@@ -462,47 +585,51 @@ public class DatabaseManager {
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// 再检查临时密码
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码(主密码优先,不通过再查临时密码)
|
||||
* 注意: 传入的 hash 必须是主密码的 hash
|
||||
* @return "main" | "temp" | null
|
||||
*/
|
||||
public String checkPasswordWithFallback(String name, String mainHash) {
|
||||
// 先检查主密码
|
||||
if (checkPassword(name, mainHash)) {
|
||||
return "main";
|
||||
}
|
||||
// 主密码不匹配,检查临时密码
|
||||
try {
|
||||
PreparedStatement ps2 = db.prepareStatement(
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT temp_password, "
|
||||
+ "temp_pw_expire, "
|
||||
+ "temp_pw_used "
|
||||
+ "FROM users "
|
||||
+ "WHERE player_name=?");
|
||||
ps2.setString(1, name);
|
||||
ResultSet rs2 = ps2.executeQuery();
|
||||
if (rs2.next()) {
|
||||
String tempHash =
|
||||
rs2.getString(
|
||||
"temp_password");
|
||||
long expire =
|
||||
rs2.getLong(
|
||||
"temp_pw_expire");
|
||||
int used =
|
||||
rs2.getInt(
|
||||
"temp_pw_used");
|
||||
rs2.close();
|
||||
ps2.close();
|
||||
ps.setString(1, name);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String tempHash = rs.getString("temp_password");
|
||||
long expire = rs.getLong("temp_pw_expire");
|
||||
int used = rs.getInt("temp_pw_used");
|
||||
rs.close();
|
||||
ps.close();
|
||||
if (tempHash != null
|
||||
&& !tempHash.isEmpty()
|
||||
&& hash.equals(tempHash)
|
||||
&& System.currentTimeMillis()
|
||||
< expire
|
||||
&& mainHash.equals(tempHash)
|
||||
&& System.currentTimeMillis() < expire
|
||||
&& used != 1) {
|
||||
return true;
|
||||
return "temp";
|
||||
}
|
||||
} else {
|
||||
rs2.close();
|
||||
ps2.close();
|
||||
rs.close();
|
||||
ps.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> getUser(String name) {
|
||||
Map<String, Object> r =
|
||||
new LinkedHashMap<>();
|
||||
@@ -569,32 +696,6 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkPassword(String name,
|
||||
String hash) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT password_hash "
|
||||
+ "FROM users "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, name);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String stored =
|
||||
rs.getString("password_hash");
|
||||
rs.close();
|
||||
ps.close();
|
||||
return hash.equals(stored);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean isUsingTempPassword(String name) {
|
||||
String h = (String) getField(name,
|
||||
"temp_password");
|
||||
|
||||
@@ -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邀请数据";
|
||||
public static final String T_USER_MGMT =
|
||||
"§6§l用户管理";
|
||||
public static final String T_USER_GROUP =
|
||||
"§6§l用户组管理";
|
||||
public static final String T_TASK_CENTER =
|
||||
"§6§l任务中心";
|
||||
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
||||
@@ -193,18 +195,7 @@ public class GUIManager implements Listener {
|
||||
}
|
||||
// 在 openMain 方法中,Row3 区域加:
|
||||
|
||||
// ★ 余额查询 - 所有人都能看 ★
|
||||
double myBal = 0;
|
||||
try {
|
||||
var reg = plugin.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault.economy
|
||||
.Economy.class);
|
||||
if (reg != null)
|
||||
myBal = reg.getProvider()
|
||||
.getBalance(p);
|
||||
} catch (Exception ignored) {}
|
||||
// 余额查询已移除Vault支持,仅显示债券余额
|
||||
int myBonds = plugin.getBonds().getBonds(p.getName());
|
||||
g.setItem(32, mkItem(Material.EMERALD,
|
||||
"§b§l我的钱包",
|
||||
@@ -217,6 +208,20 @@ public class GUIManager implements Listener {
|
||||
"§7给钱、扣钱"));
|
||||
}
|
||||
|
||||
// ★ 领地系统 - 39号位(读取用户偏好)
|
||||
int landUiMode = 1;
|
||||
try {
|
||||
landUiMode = plugin.getDb()
|
||||
.getUiMode(p.getName());
|
||||
} catch (Exception ignored) {}
|
||||
g.setItem(39, mkItem(Material.GRASS_BLOCK,
|
||||
"§a§l领地系统",
|
||||
"§7管理你的领地和权限",
|
||||
"",
|
||||
landUiMode == 0
|
||||
? "§e点击打开GUI"
|
||||
: "§e点击打开CLI"));
|
||||
|
||||
|
||||
|
||||
// ===== Row0(0-8): 自定义菜单项 =====
|
||||
@@ -327,28 +332,19 @@ public class GUIManager implements Listener {
|
||||
public void openBalanceOps(Player p) {
|
||||
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
|
||||
fillBg(g);
|
||||
|
||||
net.milkbowl.vault.economy.Economy eco = null;
|
||||
try {
|
||||
var reg = plugin.getServer().getServicesManager()
|
||||
.getRegistration(net.milkbowl.vault.economy.Economy.class);
|
||||
if (reg != null) eco = reg.getProvider();
|
||||
} catch (Exception ignored) {}
|
||||
// Vault经济支持已移除,仅显示债券余额
|
||||
|
||||
int slot = 0;
|
||||
for (Player op : Bukkit.getOnlinePlayers()) {
|
||||
if (slot >= 45) break;
|
||||
double bal = 0;
|
||||
if (eco != null) bal = eco.getBalance(op);
|
||||
boolean isSelf = op.getName().equals(p.getName());
|
||||
int bondBal = plugin.getBonds().getBonds(op.getName());
|
||||
g.setItem(slot, mkItem(
|
||||
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
|
||||
"§e" + op.getName(),
|
||||
"§7金币: §a$" + String.format("%.2f", bal),
|
||||
"§7债券: §e" + bondBal + " §6枚",
|
||||
"",
|
||||
"§a左键经济(±) §6右键债券(±)"));
|
||||
"§a仅显示债券余额(Vault经济支持已移除)"));
|
||||
slot++;
|
||||
}
|
||||
|
||||
@@ -399,6 +395,8 @@ public class GUIManager implements Listener {
|
||||
|
||||
public void openSubMenu(
|
||||
Player p, String fileName) {
|
||||
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
|
||||
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
|
||||
String menuTitle =
|
||||
"§6§l" + fileName
|
||||
.replace(".txt", "");
|
||||
@@ -406,21 +404,153 @@ public class GUIManager implements Listener {
|
||||
null, 54, menuTitle);
|
||||
fillBg(g);
|
||||
|
||||
java.io.File f = new java.io.File(
|
||||
plugin.getDataFolder(), fileName);
|
||||
if (!f.exists()) {
|
||||
p.sendMessage("§c文件不存在: "
|
||||
java.io.File dir = plugin.getDataFolder();
|
||||
java.io.File f = null;
|
||||
String cleanName = fileName.trim();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] openSubMenu: searching '"
|
||||
+ cleanName + "' in '"
|
||||
+ dir.getAbsolutePath() + "'");
|
||||
|
||||
// ★ 第1步:listFiles遍历匹配
|
||||
java.io.File[] allTxt = dir.listFiles(
|
||||
(d, n) -> n.toLowerCase()
|
||||
.endsWith(".txt"));
|
||||
if (allTxt != null) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 目录.txt文件数: "
|
||||
+ allTxt.length);
|
||||
for (java.io.File ff : allTxt) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] > " + ff.getName()
|
||||
+ " size=" + ff.length());
|
||||
if (f == null && ff.getName()
|
||||
.equalsIgnoreCase(cleanName)) {
|
||||
f = ff;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] listFiles返回null!");
|
||||
}
|
||||
|
||||
// ★ 第2步:直接构造路径
|
||||
if (f == null) {
|
||||
f = new java.io.File(dir, cleanName);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 回退构造路径: "
|
||||
+ f.getAbsolutePath()
|
||||
+ " exists=" + f.exists()
|
||||
+ " size=" + f.length());
|
||||
}
|
||||
|
||||
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
|
||||
List<String> lines = null;
|
||||
if (f != null) {
|
||||
// 方法1:NIO UTF-8
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] UTF-8读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] UTF-8失败: "
|
||||
+ t.getClass().getSimpleName()
|
||||
+ ": " + t.getMessage());
|
||||
}
|
||||
|
||||
// 方法2:NIO GBK
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.Charset
|
||||
.forName("GBK"));
|
||||
plugin.getLogger().info(
|
||||
"[Menu] GBK读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] GBK失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 方法3:FileInputStream原始字节
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
java.io.FileInputStream fis =
|
||||
new java.io.FileInputStream(f);
|
||||
byte[] raw = fis.readAllBytes();
|
||||
fis.close();
|
||||
if (raw.length > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始字节: "
|
||||
+ raw.length + "B");
|
||||
// 尝试UTF-8解码
|
||||
String content = new String(raw,
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
java.util.List<String> rawLines =
|
||||
java.util.Arrays.asList(
|
||||
content.split("\\r?\\n"));
|
||||
lines = rawLines;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始读取: "
|
||||
+ lines.size() + "行");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 原始读取失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 打印前5行调试
|
||||
if (lines != null && !lines.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 最终行数: "
|
||||
+ lines.size());
|
||||
for (int i = 0;
|
||||
i < Math.min(5, lines.size());
|
||||
i++) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + (i + 1)
|
||||
+ ": [" + lines.get(i)
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
p.sendMessage("§c§l子菜单读取失败§r §7"
|
||||
+ fileName);
|
||||
p.sendMessage("§7文件: "
|
||||
+ (f != null
|
||||
? f.getAbsolutePath()
|
||||
+ " (" + f.length() + "B)"
|
||||
: "null"));
|
||||
p.sendMessage("§7存在: "
|
||||
+ (f != null ? f.exists() : "N/A"));
|
||||
if (allTxt != null) {
|
||||
p.sendMessage("§7目录.txt文件:");
|
||||
for (java.io.File ff : allTxt) {
|
||||
p.sendMessage("§7 "
|
||||
+ ff.getName()
|
||||
+ " (" + ff.length()
|
||||
+ "B)");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<String> lines =
|
||||
java.nio.file.Files
|
||||
.readAllLines(
|
||||
f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
List<String[]> items =
|
||||
new ArrayList<>();
|
||||
StringBuilder buf =
|
||||
@@ -484,11 +614,6 @@ public class GUIManager implements Listener {
|
||||
it.setItemMeta(im);
|
||||
g.setItem(i, it);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 子菜单读取失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
g.setItem(49, mkItem(
|
||||
Material.ARROW,
|
||||
@@ -609,78 +734,12 @@ public class GUIManager implements Listener {
|
||||
Collections.shuffle(candidates);
|
||||
|
||||
// 3. 主线程分批检测,每tick处理30个坐标
|
||||
final int[] idx = {0};
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
World w = p.getWorld();
|
||||
int end = Math.min(
|
||||
idx[0] + BIOME_BATCH,
|
||||
candidates.size());
|
||||
|
||||
for (int i = idx[0]; i < end; i++) {
|
||||
int x = candidates.get(i)[0];
|
||||
int z = candidates.get(i)[1];
|
||||
|
||||
// 跳过未加载区块
|
||||
if (!w.isChunkLoaded(x >> 4, z >> 4))
|
||||
continue;
|
||||
|
||||
// 获取地表高度
|
||||
int surfaceY =
|
||||
w.getHighestBlockYAt(x, z);
|
||||
|
||||
// 在地表Y取群系(避免地下误判)
|
||||
String key = w.getBlockAt(
|
||||
x, surfaceY, z)
|
||||
.getBiome()
|
||||
.getKey().toString();
|
||||
String cat =
|
||||
mapBiomeCategory(
|
||||
key, w.getName());
|
||||
|
||||
if (category.equals(cat)) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () -> {
|
||||
Location tp =
|
||||
new Location(
|
||||
w,
|
||||
x + 0.5,
|
||||
surfaceY + 1,
|
||||
z + 0.5);
|
||||
p.teleport(tp);
|
||||
p.sendMessage(
|
||||
"§a已传送到 "
|
||||
+ category
|
||||
+ " ("
|
||||
+ x + ","
|
||||
+ (surfaceY + 1)
|
||||
+ ","
|
||||
+ z + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
idx[0] = end;
|
||||
if (idx[0] >= candidates.size()) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () ->
|
||||
p.sendMessage(
|
||||
"§c附近未找到 "
|
||||
+ category
|
||||
+ ",请换个位置再试"));
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 1L);
|
||||
new BiomeScanTask(plugin, p, category, candidates).runTaskTimer(plugin, 0L, 1L);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String mapBiomeCategory(
|
||||
private static String mapBiomeCategory(
|
||||
String biome, String worldName) {
|
||||
if (biome == null) return "平原";
|
||||
String b = biome.toLowerCase();
|
||||
@@ -863,7 +922,7 @@ public class GUIManager implements Listener {
|
||||
// ===== 余额操作 =====
|
||||
if ("§d§l余额操作".equals(title)) {
|
||||
event.setCancelled(true);
|
||||
if (raw == 49) { openAdmin(p); return; }
|
||||
if (raw == 49) { plugin.openMyWallet(p); return; }
|
||||
if (raw < 0 || raw >= 45) return;
|
||||
ItemStack item = event.getView()
|
||||
.getTopInventory().getItem(raw);
|
||||
@@ -927,6 +986,13 @@ public class GUIManager implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 用户组管理 =====
|
||||
if (T_USER_GROUP.equals(title)) {
|
||||
event.setCancelled(true);
|
||||
handleUserGroupClick(p, raw, event.isLeftClick(), event.isRightClick(), event.isShiftClick());
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 打包确认 =====
|
||||
if ("§6§l选择包装方式".equals(title)) {
|
||||
event.setCancelled(true);
|
||||
@@ -2711,6 +2777,279 @@ public class GUIManager implements Listener {
|
||||
plugin.getConfig2().adminTag);
|
||||
}
|
||||
|
||||
// ==================== 用户组管理GUI ====================
|
||||
|
||||
/** 打开用户组管理主界面 */
|
||||
public void openUserGroup(Player p) {
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||
|
||||
Inventory g = Bukkit.createInventory(null, 54, T_USER_GROUP);
|
||||
|
||||
int bonds = plugin.getBonds().getBonds(p.getName());
|
||||
g.setItem(4, mkItem(Material.EMERALD, "§b§l我的债券: " + bonds + " 张", "§7用于购买/续费用户组"));
|
||||
|
||||
// Row1 (9-17): 可购买的用户组
|
||||
List<UserGroupManager.UserGroupConfig> allGroups = ugm.getAllGroups();
|
||||
int slot = 9;
|
||||
for (UserGroupManager.UserGroupConfig cfg : allGroups) {
|
||||
if (slot > 17) break;
|
||||
if (cfg.joinPrice <= 0) continue; // 不开放付费的组不显示
|
||||
|
||||
boolean isMember = ugm.isPlayerInGroup(p.getName(), cfg.name);
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
|
||||
if (isMember) {
|
||||
// 已加入 → 显示在 Row2
|
||||
continue;
|
||||
}
|
||||
|
||||
Material mat = isMember ? Material.LIME_DYE : Material.WHITE_DYE;
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + cfg.joinPrice + " 张债券");
|
||||
if (cfg.durationMinutes > 0) {
|
||||
long days = cfg.durationMinutes / 1440;
|
||||
long hours = (cfg.durationMinutes % 1440) / 60;
|
||||
String dur = days > 0 ? days + "天" : hours + "小时";
|
||||
lore.add("§7有效期: §e" + dur);
|
||||
} else {
|
||||
lore.add("§7有效期: §a永久");
|
||||
}
|
||||
if (cfg.renewPrice > 0) {
|
||||
lore.add("§7续费价格: §e" + cfg.renewPrice + " 张债券");
|
||||
}
|
||||
lore.add("");
|
||||
lore.add("§a左键: 购买给自己");
|
||||
lore.add("§b右键: 赠送给他人");
|
||||
|
||||
g.setItem(slot, mkItem(mat, cfg.displayColor + "§l" + displayName + " §7(购买)", lore.toArray(new String[0])));
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Row2 (18-26): 我的用户组
|
||||
slot = 18;
|
||||
List<Map<String, Object>> myGroups = ugm.getPlayerGroups(p.getName());
|
||||
for (Map<String, Object> grp : myGroups) {
|
||||
if (slot > 26) break;
|
||||
String grpName = String.valueOf(grp.get("group_name"));
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) continue;
|
||||
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
long expiry = ((Number) grp.getOrDefault("expiry_time", 0)).longValue();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (expiry > 0) {
|
||||
if (expiry <= now) {
|
||||
lore.add("§c已过期");
|
||||
} else {
|
||||
long remainMs = expiry - now;
|
||||
long remainMin = remainMs / 60000;
|
||||
long rDays = remainMin / 1440;
|
||||
long rHours = (remainMin % 1440) / 60;
|
||||
lore.add("§7剩余: §e" + (rDays > 0 ? rDays + "天" : rHours + "小时"));
|
||||
lore.add("§7到期: §7" + new java.text.SimpleDateFormat("MM-dd HH:mm").format(new java.util.Date(expiry)));
|
||||
}
|
||||
} else {
|
||||
lore.add("§7有效期: §a永久");
|
||||
}
|
||||
// 自动续费状态
|
||||
boolean autoRenew = getAutoRenew(p.getName(), grpName);
|
||||
lore.add("§7自动续费: " + (autoRenew ? "§a开启" : "§c关闭"));
|
||||
lore.add("");
|
||||
lore.add("§a左键: 续费");
|
||||
lore.add("§b右键: 赠送给他人");
|
||||
lore.add("§eShift+右键: " + (autoRenew ? "§c关闭" : "§a开启") + "自动续费");
|
||||
|
||||
g.setItem(slot, mkItem(Material.LIME_DYE, cfg.displayColor + "§l" + displayName, lore.toArray(new String[0])));
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Row4 (40): 赠送入口
|
||||
g.setItem(40, mkItem(Material.SPECTRAL_ARROW, "§e§l赠予用户组", "§7为其他玩家购买/续费用户组"));
|
||||
|
||||
// Row5 (49): 返回
|
||||
g.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
|
||||
fillBg(g);
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/** 处理用户组GUI点击 */
|
||||
public boolean handleUserGroupClick(Player p, int raw, boolean left, boolean right, boolean shift) {
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) return false;
|
||||
|
||||
if (raw == 49) { openMain(p); return true; } // 返回
|
||||
|
||||
// 赠送按钮
|
||||
if (raw == 40) {
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Row1: 可购买的组 (9-17)
|
||||
if (raw >= 9 && raw <= 17) {
|
||||
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||
String grpName = extractGroupNameFromItem(item);
|
||||
if (grpName == null) return false;
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) return false;
|
||||
|
||||
if (left && !right) {
|
||||
// 购买给自己
|
||||
p.closeInventory();
|
||||
p.sendMessage("§7正在购买 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||
String err = ugm.joinGroupByPrice(p.getName(), cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c购买失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l购买成功! §7你已加入用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||
}
|
||||
return true;
|
||||
} else if (right && !left) {
|
||||
// 赠送给他人
|
||||
p.closeInventory();
|
||||
// 记住赠送的组名
|
||||
pendingGiftGroup.put(p.getName(), grpName);
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Row2: 我的组 (18-26)
|
||||
if (raw >= 18 && raw <= 26) {
|
||||
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||
String grpName = extractGroupNameFromItem(item);
|
||||
if (grpName == null) return false;
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) return false;
|
||||
|
||||
if (shift && right) {
|
||||
// Shift+右键: 切换自动续费
|
||||
boolean current = getAutoRenew(p.getName(), grpName);
|
||||
setAutoRenew(p.getName(), grpName, !current);
|
||||
p.sendMessage("§a自动续费已" + (!current ? "开启" : "关闭") + ": §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||
openUserGroup(p); // 刷新界面
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left && !right) {
|
||||
// 续费
|
||||
p.closeInventory();
|
||||
p.sendMessage("§7正在续费 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||
String err = ugm.renewGroup(p.getName(), cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c续费失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l续费成功! §7用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7已续期");
|
||||
}
|
||||
return true;
|
||||
} else if (right && !left && !shift) {
|
||||
// 赠送给他人
|
||||
p.closeInventory();
|
||||
pendingGiftGroup.put(p.getName(), grpName);
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 处理赠予玩家名输入 */
|
||||
public void handleGiftTargetInput(Player p, String targetName) {
|
||||
String grpName = pendingGiftGroup.remove(p.getName());
|
||||
if (grpName == null) return;
|
||||
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) { p.sendMessage("§c用户组不存在: " + grpName); return; }
|
||||
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
|
||||
// 检查目标玩家是否存在
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null && !dbMgr.userExists(targetName)) {
|
||||
p.sendMessage("§c玩家 §e" + targetName + " §c不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 使用新的赠送方法:送礼人(p)付费,收礼人(targetName)免费获得
|
||||
p.sendMessage("§7正在为 §e" + targetName + " §7购买用户组 §e" + displayName + " §7...");
|
||||
String err = ugm.giftGroupToPlayer(p.getName(), targetName, cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c赠予失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l赠予成功!");
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 赠予流程:记录赠送的组名
|
||||
private final Map<String, String> pendingGiftGroup = new ConcurrentHashMap<>();
|
||||
|
||||
// ★ 自动续费偏好(per-player)
|
||||
private final Map<String, Boolean> autoRenewCache = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean getAutoRenew(String player, String group) {
|
||||
String key = player + "|" + group;
|
||||
if (autoRenewCache.containsKey(key)) return autoRenewCache.get(key);
|
||||
// 从数据库读取
|
||||
boolean val = false;
|
||||
try {
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null) {
|
||||
val = dbMgr.getPlayerAutoRenew(player, group);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
autoRenewCache.put(key, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private void setAutoRenew(String player, String group, boolean value) {
|
||||
String key = player + "|" + group;
|
||||
autoRenewCache.put(key, value);
|
||||
try {
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null) {
|
||||
dbMgr.setPlayerAutoRenew(player, group, value);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/** 从GUI物品名称中提取用户组名(去掉格式化前缀和后缀) */
|
||||
private String extractGroupNameFromItem(ItemStack item) {
|
||||
if (item == null || !item.hasItemMeta()) return null;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null || meta.getDisplayName() == null) return null;
|
||||
String name = meta.getDisplayName();
|
||||
// 去掉所有§格式化代码
|
||||
name = name.replaceAll("§[0-9a-fk-orA-FK-OR]", "");
|
||||
// 去掉后缀 " (购买)"
|
||||
name = name.replaceAll("\\s*\\(购买\\)$", "").trim();
|
||||
// 匹配已知组名
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) return null;
|
||||
for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) {
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
if (name.equals(displayName) || name.equals(cfg.name)) {
|
||||
return cfg.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack glass = new ItemStack(
|
||||
Material.GRAY_STAINED_GLASS_PANE);
|
||||
@@ -2774,4 +3113,55 @@ public class GUIManager implements Listener {
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
/** 命名静态内部类替代匿名BukkitRunnable,修复Paper 26 ClassLoader兼容性 */
|
||||
private static class BiomeScanTask extends BukkitRunnable {
|
||||
private final Main plugin;
|
||||
private final Player p;
|
||||
private final String category;
|
||||
private final java.util.List<int[]> candidates;
|
||||
private int idx = 0;
|
||||
|
||||
BiomeScanTask(Main plugin, Player p, String category, java.util.List<int[]> candidates) {
|
||||
this.plugin = plugin;
|
||||
this.p = p;
|
||||
this.category = category;
|
||||
this.candidates = candidates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
World w = p.getWorld();
|
||||
int end = Math.min(idx + BIOME_BATCH, candidates.size());
|
||||
|
||||
for (int i = idx; i < end; i++) {
|
||||
int x = candidates.get(i)[0];
|
||||
int z = candidates.get(i)[1];
|
||||
|
||||
if (!w.isChunkLoaded(x >> 4, z >> 4)) continue;
|
||||
|
||||
int surfaceY = w.getHighestBlockYAt(x, z);
|
||||
String key = w.getBlockAt(x, surfaceY, z)
|
||||
.getBiome().getKey().toString();
|
||||
String cat = mapBiomeCategory(key, w.getName());
|
||||
|
||||
if (category.equals(cat)) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Location tp = new Location(w, x + 0.5, surfaceY + 1, z + 0.5);
|
||||
p.teleport(tp);
|
||||
p.sendMessage("§a已传送到 " + category + " (" + x + "," + (surfaceY + 1) + "," + z + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
idx = end;
|
||||
if (idx >= candidates.size()) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
p.sendMessage("§c附近未找到 " + category + ",请换个位置再试"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Item;
|
||||
@@ -12,7 +11,6 @@ import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.sql.Date;
|
||||
@@ -287,11 +285,8 @@ public class GarbageManager {
|
||||
.decode(base64);
|
||||
String json = new String(bytes,
|
||||
StandardCharsets.UTF_8);
|
||||
Type type = new TypeToken<
|
||||
Map<String, Object>>() {}
|
||||
.getType();
|
||||
Map<String, Object> map =
|
||||
new Gson().fromJson(json, type);
|
||||
new Gson().fromJson(json, Map.class);
|
||||
map = fixNumberTypes(map);
|
||||
return ItemStack.deserialize(map);
|
||||
} catch (Exception e) {
|
||||
@@ -425,7 +420,7 @@ public class GarbageManager {
|
||||
"§7已是最后一页"));
|
||||
}
|
||||
|
||||
// 52 格:返回
|
||||
// 52 格:返回主菜单
|
||||
g.setItem(52, mk(Material.ARROW,
|
||||
"§7返回",
|
||||
"§7返回主菜单"));
|
||||
|
||||
@@ -194,28 +194,21 @@ public class GiftManager {
|
||||
giveItem(p, Material.WOODEN_AXE);
|
||||
giveItem(p, Material.WOODEN_SHOVEL);
|
||||
} else if (stage == 2) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
// 游戏币改为债券
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 500, "gift_stage2", "", "新手礼包", "阶段2奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
} else if (stage == 3) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 1500, "gift_stage3", "", "新手礼包", "阶段3奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
giveItem(p, Material.IRON_PICKAXE);
|
||||
giveItem(p, Material.IRON_AXE);
|
||||
@@ -225,16 +218,12 @@ public class GiftManager {
|
||||
giveItem(p, Material.IRON_LEGGINGS);
|
||||
giveItem(p, Material.IRON_BOOTS);
|
||||
} else if (stage == 4) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 2000);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 2000, "gift_stage4", "", "新手礼包", "阶段4奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
giveItem(p, Material.DIAMOND_HELMET);
|
||||
giveItem(p, Material.DIAMOND_CHESTPLATE);
|
||||
|
||||
@@ -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(
|
||||
name, hash, salt);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
@@ -100,6 +104,17 @@ public class LoginManager {
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// ★ 注册后立即同步用户数据到PHP
|
||||
if (plugin.webManager != null) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try { plugin.webManager.syncUserRegistrations(); }
|
||||
catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -162,6 +177,10 @@ public class LoginManager {
|
||||
name, hash);
|
||||
if (matchMain) {
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
plugin.getDb().setLoggedIn(
|
||||
name, true);
|
||||
plugin.getDb().setLoggedIn(
|
||||
@@ -201,12 +220,16 @@ public class LoginManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean matchTemp =
|
||||
String pwdResult =
|
||||
plugin.getDb()
|
||||
.checkPasswordOrTemp(
|
||||
.checkPasswordWithFallback(
|
||||
name, hash);
|
||||
if (matchTemp) {
|
||||
if (pwdResult != null) {
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
|
||||
+1099
-402
File diff suppressed because it is too large
Load Diff
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ===== 读取菜单.txt =====
|
||||
// ===== 读取菜单.txt(主菜单唯一来源) =====
|
||||
public void loadMenu() {
|
||||
items.clear();
|
||||
|
||||
// 优先菜单.txt,备选menu.txt
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) createDefault(f);
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) {
|
||||
f = alt;
|
||||
} else {
|
||||
createDefault(f);
|
||||
}
|
||||
}
|
||||
parseFile(f);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 加载 "
|
||||
+ items.size()
|
||||
+ " 个菜单项");
|
||||
"[Menu] 加载 " + items.size()
|
||||
+ " 个菜单项 (源: "
|
||||
+ f.getName() + ")");
|
||||
}
|
||||
|
||||
private void parseFile(File f) {
|
||||
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
|
||||
|
||||
// ===== 保存 =====
|
||||
public void saveMenu() {
|
||||
// 保存到加载源文件
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) f = alt;
|
||||
}
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
|
||||
@@ -259,20 +259,21 @@ public class PointsManager {
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 1000, "points_exchange", "", "积分商店", "积分兑换债券");
|
||||
int after = bm.getBonds(p.getName());
|
||||
plugin.getDb().deductPoints(p.getName(), 100);
|
||||
p.sendMessage(plugin.getConfig2().msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
p.sendMessage("§a[积分] §f债券: §e" + before + " → §a" + after);
|
||||
} else {
|
||||
p.sendMessage("§c债券系统不可用");
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
p.sendMessage(plugin.getConfig2().msg("points_insufficient")
|
||||
.replace("{points}", String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
|
||||
@@ -1213,15 +1213,6 @@ public class QuestTracker {
|
||||
}
|
||||
|
||||
Main main = (Main) plugin;
|
||||
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
|
||||
boolean useEconomy = "economy".equalsIgnoreCase(
|
||||
main.getConfigMgr().rewardChannel);
|
||||
|
||||
// Vault 经济引用(仅 useEconomy 时使用)
|
||||
net.milkbowl.vault.economy.Economy econ = null;
|
||||
if (useEconomy) {
|
||||
econ = main.getEconomy();
|
||||
}
|
||||
|
||||
int sentCount = 0;
|
||||
|
||||
@@ -1247,53 +1238,17 @@ public class QuestTracker {
|
||||
// 尝试识别经济金额
|
||||
double amt = extractEconomyAmount(part);
|
||||
if (amt > 0) {
|
||||
// ★ 根据配置分发
|
||||
if (useEconomy && econ != null) {
|
||||
// —— Vault 经济 ——
|
||||
double before =
|
||||
econ.getBalance(player);
|
||||
econ.depositPlayer(player, amt);
|
||||
double after =
|
||||
econ.getBalance(player);
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(余额: §e"
|
||||
+ String.format("%.2f", before)
|
||||
+ " §7→ §a"
|
||||
+ String.format("%.2f", after)
|
||||
+ "§7)");
|
||||
if (plugin instanceof Main) {
|
||||
((Main) plugin).getCommission()
|
||||
.payCommission(
|
||||
player.getName(), amt);
|
||||
}
|
||||
// Economy已移除,全部转为债券发放
|
||||
// —— 债券 ——
|
||||
int bondAmt = (int) Math.round(amt);
|
||||
BondManager bonds = main.getBonds();
|
||||
if (bonds != null) {
|
||||
int before = bonds.getBonds(player.getName());
|
||||
bonds.addBonds(player.getName(), bondAmt, "quest_reward", qf.displayName, "任务系统", "任务奖励:" + qf.displayName);
|
||||
int after = bonds.getBonds(player.getName());
|
||||
player.sendMessage("§a§l[任务奖励] §f" + part + " §7(债券: §e" + before + " §7→ §a" + after + "§7)");
|
||||
} else {
|
||||
// —— 债券(默认) ——
|
||||
int bondAmt =
|
||||
(int) Math.round(amt);
|
||||
BondManager bonds = main.getBonds();
|
||||
if (bonds != null) {
|
||||
int before = bonds.getBonds(
|
||||
player.getName());
|
||||
bonds.addBonds(
|
||||
player.getName(),
|
||||
bondAmt,
|
||||
"quest_reward",
|
||||
qf.displayName,
|
||||
"任务系统",
|
||||
"任务奖励:" + qf.displayName);
|
||||
int after = bonds.getBonds(
|
||||
player.getName());
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(债券: §e"
|
||||
+ before
|
||||
+ " §7→ §a"
|
||||
+ after + "§7)");
|
||||
} else {
|
||||
player.sendMessage(
|
||||
"§c债券系统不可用");
|
||||
}
|
||||
player.sendMessage("§c债券系统不可用");
|
||||
}
|
||||
sentCount++;
|
||||
continue;
|
||||
|
||||
@@ -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.util.*;
|
||||
import java.util.Base64;
|
||||
import java.text.SimpleDateFormat;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
@@ -30,6 +31,11 @@ public class ShopManager implements Listener {
|
||||
private final Main plugin;
|
||||
private final List<ShopCategory> categories =
|
||||
new ArrayList<>();
|
||||
// ===== 出售限额配置 =====
|
||||
private String maxSellMessage = "";
|
||||
private boolean maxSellMessageEnabled = false;
|
||||
private int maxSellLimit = 15000; // 默认1.5万
|
||||
private final Object maxSellLock = new Object();
|
||||
private final List<RefundRecord> refundRecords =
|
||||
new ArrayList<>();
|
||||
private final Map<UUID, Integer> activeDiscount =
|
||||
@@ -140,6 +146,7 @@ public class ShopManager implements Listener {
|
||||
public ShopManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
loadCategories();
|
||||
loadMaxSellConfig();
|
||||
startHourlyTask();
|
||||
loadItemNbtData();
|
||||
startCleanupTask();
|
||||
@@ -1369,9 +1376,65 @@ public class ShopManager implements Listener {
|
||||
}
|
||||
|
||||
public void saveAll() {
|
||||
for (ShopCategory cat : categories)
|
||||
saveCategory(cat);
|
||||
}
|
||||
for (ShopCategory cat : categories)
|
||||
saveCategory(cat);
|
||||
saveMaxSellConfig();
|
||||
}
|
||||
|
||||
// ===== maxSell 配置持久化 =====
|
||||
/** 加载 maxSell 配置从插件数据文件夹下的 maxsell_config.txt */
|
||||
private void loadMaxSellConfig() {
|
||||
File cfgFile = new File(plugin.getDataFolder(), "maxsell_config.txt");
|
||||
try {
|
||||
if (cfgFile.exists()) {
|
||||
BufferedReader br = new BufferedReader(new FileReader(cfgFile));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.startsWith("maxSellLimit=")) {
|
||||
try {
|
||||
maxSellLimit = Integer.parseInt(line.substring("maxSellLimit=".length()));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} else if (line.startsWith("maxSellMessageEnabled=")) {
|
||||
maxSellMessageEnabled = Boolean.parseBoolean(line.substring("maxSellMessageEnabled=".length()));
|
||||
} else if (line.startsWith("maxSellMessage=")) {
|
||||
maxSellMessage = line.substring("maxSellMessage=".length());
|
||||
}
|
||||
}
|
||||
br.close();
|
||||
} else {
|
||||
// 配置文件不存在,使用默认值并保存
|
||||
maxSellLimit = 15000;
|
||||
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||
maxSellMessageEnabled = true;
|
||||
saveMaxSellConfig();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Shop] 加载maxsell配置失败: " + e.getMessage());
|
||||
maxSellLimit = 15000;
|
||||
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||
maxSellMessageEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存 maxSell 配置到插件数据文件夹下的 maxsell_config.txt */
|
||||
private void saveMaxSellConfig() {
|
||||
synchronized (maxSellLock) {
|
||||
try {
|
||||
File dir = new File(plugin.getDataFolder(), "shop");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
File cfgFile = new File(dir.getParentFile(), "maxsell_config.txt");
|
||||
BufferedWriter bw = new BufferedWriter(new FileWriter(cfgFile, StandardCharsets.UTF_8));
|
||||
bw.write("maxSellLimit=" + maxSellLimit);
|
||||
bw.newLine();
|
||||
bw.write("maxSellMessageEnabled=" + maxSellMessageEnabled);
|
||||
bw.newLine();
|
||||
bw.write("maxSellMessage=" + maxSellMessage);
|
||||
bw.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Shop] 保存maxsell配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
public ItemStack getShopStack(
|
||||
ShopItem item, int amount) {
|
||||
// 优先级1: 手动捕获的NBT数据
|
||||
@@ -1555,6 +1618,179 @@ public class ShopManager implements Listener {
|
||||
|
||||
// ===== 出售 =====
|
||||
|
||||
/**
|
||||
* HTML解码器,支持:
|
||||
* - &颜色代码 (如 &c红色, &a绿色)
|
||||
* - <b>加粗</b>, <u>下划线</u>, <i>斜体</i>
|
||||
* - <br>换行
|
||||
* - <p style="...">段落</p> (支持color属性)
|
||||
*/
|
||||
private String decodeHtml(String input) {
|
||||
if (input == null || input.isEmpty()) return input;
|
||||
String result = input;
|
||||
|
||||
// 1. 先处理 &颜色代码 → §颜色代码
|
||||
result = result.replace("&0", "§0");
|
||||
result = result.replace("&1", "§1");
|
||||
result = result.replace("&2", "§2");
|
||||
result = result.replace("&3", "§3");
|
||||
result = result.replace("&4", "§4");
|
||||
result = result.replace("&5", "§5");
|
||||
result = result.replace("&6", "§6");
|
||||
result = result.replace("&7", "§7");
|
||||
result = result.replace("&8", "§8");
|
||||
result = result.replace("&9", "§9");
|
||||
result = result.replace("&a", "§a");
|
||||
result = result.replace("&b", "§b");
|
||||
result = result.replace("&c", "§c");
|
||||
result = result.replace("&d", "§d");
|
||||
result = result.replace("&e", "§e");
|
||||
result = result.replace("&f", "§f");
|
||||
// 样式代码
|
||||
result = result.replace("&k", "§k");
|
||||
result = result.replace("&l", "§l");
|
||||
result = result.replace("&m", "§m");
|
||||
result = result.replace("&n", "§n");
|
||||
result = result.replace("&o", "§o");
|
||||
result = result.replace("&r", "§r");
|
||||
|
||||
// 2. 处理HTML标签
|
||||
// <u>标签:§n 开始下划线,§r 切断(只作用于标签内)
|
||||
// <b>标签:§l 开始加粗,§r 切断
|
||||
// <i>标签:§o 开始斜体,§r 切断
|
||||
result = result.replaceAll("<u>(.*?)</u>", "§n$1§r");
|
||||
result = result.replaceAll("<b>(.*?)</b>", "§l$1§r");
|
||||
result = result.replaceAll("<i>(.*?)</i>", "§o$1§r");
|
||||
result = result.replaceAll("<br>", "\n");
|
||||
result = result.replaceAll("<br/>", "\n");
|
||||
|
||||
// 3. 处理 <p style="color:#xxx"> 或 <p style="color:xxx">
|
||||
java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile(
|
||||
"<p\\s+style=[\"']color:\\s*(#[0-9a-fA-F]{3,8}|[a-zA-Z_]+|rgb\\([^)]+\\)|rgba\\([^)]+\\)|hsl\\([^)]+\\)|hsla\\([^)]+\\))\\s*;?[\"']\\s*>(.*?)</p>",
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
java.util.regex.Matcher pMatcher = pPattern.matcher(result);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pMatcher.find()) {
|
||||
String color = pMatcher.group(1);
|
||||
String text = pMatcher.group(2);
|
||||
String mcColor = convertToMcColor(color);
|
||||
pMatcher.appendReplacement(sb, mcColor + text + "§r");
|
||||
}
|
||||
pMatcher.appendTail(sb);
|
||||
result = sb.toString();
|
||||
|
||||
// 4. 处理 <p>段落</p> (无style) → 换行分隔
|
||||
result = result.replaceAll("<p>(.*?)</p>", "$1\n");
|
||||
|
||||
// 5. 移除残留的style标签属性
|
||||
result = result.replaceAll("<p[^>]*>", "");
|
||||
result = result.replaceAll("</p>", "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将颜色值转换为Minecraft颜色代码
|
||||
* 支持: #RRGGBB 十六进制, 或颜色名称
|
||||
*/
|
||||
private String convertToMcColor(String color) {
|
||||
if (color == null || color.isEmpty()) return "";
|
||||
// 处理 rgb/rgba 颜色函数
|
||||
String lowerColor = color.toLowerCase();
|
||||
if (lowerColor.startsWith("rgb(") || lowerColor.startsWith("rgba(")) {
|
||||
try {
|
||||
// 提取括号内的内容
|
||||
int start = color.indexOf('(') + 1;
|
||||
int end = color.lastIndexOf(')');
|
||||
if (start > 0 && end > start) {
|
||||
String content = color.substring(start, end);
|
||||
String[] parts = content.split(",");
|
||||
if (parts.length >= 3) {
|
||||
int r = Integer.parseInt(parts[0].trim());
|
||||
int g = Integer.parseInt(parts[1].trim());
|
||||
int b = Integer.parseInt(parts[2].trim());
|
||||
// 转换为十六进制
|
||||
String hex = String.format("#%02x%02x%02x", r, g, b);
|
||||
// 递归调用,使用十六进制逻辑
|
||||
return convertToMcColor(hex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败,返回默认白色
|
||||
return "§f";
|
||||
}
|
||||
}
|
||||
// 处理 hsl/hsla 颜色函数(简单映射到最接近的Minecraft颜色)
|
||||
if (lowerColor.startsWith("hsl(") || lowerColor.startsWith("hsla(")) {
|
||||
// 由于HSL解析复杂,暂时返回默认白色
|
||||
return "§f";
|
||||
}
|
||||
// 十六进制颜色
|
||||
if (color.startsWith("#")) {
|
||||
String hex = color.substring(1).toLowerCase();
|
||||
if (hex.equals("ff5555") || hex.equals("ff0000")) return "§c";
|
||||
if (hex.equals("55ff55") || hex.equals("00ff00")) return "§a";
|
||||
if (hex.equals("5555ff") || hex.equals("0000ff")) return "§9";
|
||||
if (hex.equals("ffff55") || hex.equals("ffff00")) return "§e";
|
||||
if (hex.equals("ffffff")) return "§f";
|
||||
if (hex.equals("000000")) return "§0";
|
||||
if (hex.equals("a0a0a0") || hex.equals("808080")) return "§7";
|
||||
if (hex.equals("ffaa00")) return "§6";
|
||||
if (hex.equals("55ffff")) return "§b";
|
||||
if (hex.equals("ff55ff")) return "§d";
|
||||
if (hex.equals("aa0000")) return "§4";
|
||||
if (hex.equals("0000aa")) return "§1";
|
||||
if (hex.equals("00aa00")) return "§2";
|
||||
if (hex.equals("00aaaa")) return "§3";
|
||||
if (hex.equals("aa00aa")) return "§5";
|
||||
if (hex.equals("555555")) return "§8";
|
||||
return "§f"; // 默认白色
|
||||
}
|
||||
// 颜色名称
|
||||
String name = color.toLowerCase().replace("_", "");
|
||||
if (name.equals("red") || name.equals("darkred") || name.equals("dark_red") || name.equals("crimson") || name.equals("salmon") || name.equals("tomato") || name.equals("coral")) return "§c";
|
||||
if (name.equals("green") || name.equals("darkgreen") || name.equals("dark_green") || name.equals("lime") || name.equals("chartreuse") || name.equals("olive") || name.equals("olivegreen")) return "§a";
|
||||
if (name.equals("blue") || name.equals("darkblue") || name.equals("dark_blue") || name.equals("darkaqua") || name.equals("navy") || name.equals("indigo")) return "§9";
|
||||
if (name.equals("yellow") || name.equals("gold") || name.equals("goldenrod")) return "§e";
|
||||
if (name.equals("white") || name.equals("silver") || name.equals("snow") || name.equals("ivory")) return "§f";
|
||||
if (name.equals("black") || name.equals("onyx") || name.equals("jetblack")) return "§0";
|
||||
if (name.equals("gray") || name.equals("grey") || name.equals("darkgray") || name.equals("darkgrey") || name.equals("dimgray") || name.equals("dimgrey")) return "§7";
|
||||
if (name.equals("aqua") || name.equals("lightblue") || name.equals("turquoise") || name.equals("teal")) return "§b";
|
||||
if (name.equals("lightpurple") || name.equals("purple") || name.equals("magenta") || name.equals("plum") || name.equals("orchid") || name.equals("violet")) return "§d";
|
||||
if (name.equals("darkred")) return "§4";
|
||||
if (name.equals("darkblue")) return "§1";
|
||||
if (name.equals("darkgreen")) return "§2";
|
||||
if (name.equals("darkaqua") || name.equals("cyan")) return "§3";
|
||||
if (name.equals("darkpurple")) return "§5";
|
||||
if (name.equals("darkgray") || name.equals("darkgrey")) return "§8";
|
||||
if (name.equals("lightgray") || name.equals("lightgrey")) return "§7";
|
||||
if (name.equals("orange") || name.equals("orangered")) return "§6";
|
||||
if (name.equals("pink") || name.equals("hotpink") || name.equals("lightpink")) return "§d";
|
||||
if (name.equals("brown") || name.equals("saddlebrown") || name.equals("sienna") || name.equals("chocolate")) return "§4";
|
||||
return "§f"; // 默认白色
|
||||
}
|
||||
|
||||
// ===== 占位符替换 =====
|
||||
private String replacePlaceholders(String msg, String player, int used, int limit, String time) {
|
||||
if (msg == null || msg.isEmpty()) return msg;
|
||||
// 支持中英文占位符,任意括号
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]玩家[(\\(\\)\\[\\]\\{\\}【】)]", player);
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]player[(\\(\\)\\[\\]\\{\\}【】)]", player);
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已出售金额[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]used[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]已使用额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(used));
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]剩余额度[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]limit[(\\(\\)\\[\\]\\{\\}【】)]", String.valueOf(limit));
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]下次重置时间[(\\(\\)\\[\\]\\{\\}【】)]", time);
|
||||
msg = msg.replaceAll("[(\\(\\)\\[\\]\\{\\}【】)]time[(\\(\\)\\[\\]\\{\\}【】)]", time);
|
||||
//同时支持%player%等格式
|
||||
msg = msg.replace("%player%", player);
|
||||
msg = msg.replace("%used%", String.valueOf(used));
|
||||
msg = msg.replace("%limit%", String.valueOf(limit));
|
||||
msg = msg.replace("%time%", time);
|
||||
return msg;
|
||||
}
|
||||
|
||||
public boolean sellItem(Player p, ShopItem item,
|
||||
int amount) {
|
||||
if (item == null) return false;
|
||||
@@ -1565,6 +1801,31 @@ public class ShopManager implements Listener {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ★ 出售限额检查
|
||||
if (maxSellLimit != -1) {
|
||||
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
|
||||
int remaining = maxSellLimit - todaySold;
|
||||
int saleAmount = item.getEffectiveSellPrice() * amount;
|
||||
if (saleAmount > remaining) {
|
||||
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
|
||||
String msg = decodeHtml(maxSellMessage);
|
||||
// 计算下次重置时间(最早交易 + 1小时)
|
||||
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
|
||||
String resetTime;
|
||||
if (earliestTime > 0) {
|
||||
resetTime = new java.text.SimpleDateFormat("HH:mm")
|
||||
.format(new java.util.Date(earliestTime + 3600000L));
|
||||
} else {
|
||||
resetTime = "现在";
|
||||
}
|
||||
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
|
||||
p.sendMessage(msg);
|
||||
}
|
||||
// 提示消息关闭时不打印任何消息
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!p.getInventory().containsAtLeast(
|
||||
getShopStack(item, 1), amount)) {
|
||||
p.sendMessage("§c你没有足够的 §e"
|
||||
@@ -1782,12 +2043,60 @@ public class ShopManager implements Listener {
|
||||
|
||||
if (sub.equals("reload")) {
|
||||
loadCategories();
|
||||
loadMaxSellConfig();
|
||||
s.sendMessage("§a商店已重载,共 "
|
||||
+ categories.size()
|
||||
+ " 个分类");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("setmax")) {
|
||||
if (a.length < 3) {
|
||||
s.sendMessage(
|
||||
"§e用法: shop setmax <金额(-1无限)>");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
int limit = Integer.parseInt(a[2]);
|
||||
maxSellLimit = limit;
|
||||
saveMaxSellConfig();
|
||||
if (limit == -1) {
|
||||
s.sendMessage("§a出售限额已设为无限");
|
||||
} else {
|
||||
s.sendMessage("§a出售限额已设为 §e" + limit + " §a枚债券/小时");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
s.sendMessage("§c无效数字: " + a[2]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("maxmsg")) {
|
||||
if (a.length < 3) {
|
||||
s.sendMessage(
|
||||
"§e用法: shop maxmsg <内容/关闭/默认>");
|
||||
return true;
|
||||
}
|
||||
// 拼接剩余参数作为消息内容
|
||||
String msg = String.join(" ", Arrays.copyOfRange(a, 2, a.length));
|
||||
if (msg.equalsIgnoreCase("关闭") || msg.equalsIgnoreCase("off")) {
|
||||
maxSellMessageEnabled = false;
|
||||
saveMaxSellConfig();
|
||||
s.sendMessage("§a出售限额提示已关闭");
|
||||
} else if (msg.equalsIgnoreCase("默认") || msg.equalsIgnoreCase("default")) {
|
||||
maxSellMessage = "[玩家]本小时已出售(used),剩余<u>[limit]</u>。下次重置时间:{下次重置时间}";
|
||||
maxSellMessageEnabled = true;
|
||||
saveMaxSellConfig();
|
||||
s.sendMessage("§a出售限额提示已设为默认内容");
|
||||
} else {
|
||||
maxSellMessage = msg;
|
||||
maxSellMessageEnabled = true;
|
||||
saveMaxSellConfig();
|
||||
s.sendMessage("§a出售限额提示已设置");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("add")) {
|
||||
if (a.length < 7) {
|
||||
s.sendMessage(
|
||||
@@ -2527,6 +2836,30 @@ public class ShopManager implements Listener {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
// ★ 出售限额检查
|
||||
if (maxSellLimit != -1 && total > 0) {
|
||||
int todaySold = plugin.getBonds().getTodaySellTotal(p.getName());
|
||||
int remaining = maxSellLimit - todaySold;
|
||||
int saleAmount = item.getEffectiveSellPrice() * total;
|
||||
if (saleAmount > remaining) {
|
||||
if (maxSellMessageEnabled && maxSellMessage != null && !maxSellMessage.isEmpty()) {
|
||||
String msg = decodeHtml(maxSellMessage);
|
||||
long earliestTime = plugin.getBonds().getEarliestSellTimeInWindow(p.getName());
|
||||
String resetTime;
|
||||
if (earliestTime > 0) {
|
||||
resetTime = new java.text.SimpleDateFormat("HH:mm")
|
||||
.format(new java.util.Date(earliestTime + 3600000L));
|
||||
} else {
|
||||
resetTime = "现在";
|
||||
}
|
||||
msg = replacePlaceholders(msg, p.getName(), todaySold, remaining, resetTime);
|
||||
p.sendMessage(msg);
|
||||
}
|
||||
// 提示消息关闭时不打印任何消息
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (total <= 0) {
|
||||
p.sendMessage("§c你没有可出售的 §e"
|
||||
+ item.getDisplayName());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3600
-1111
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
|
||||
version: 2.5
|
||||
version: 2.6
|
||||
main: Sdf1_login.Main
|
||||
api-version: '1.21'
|
||||
commands:
|
||||
@@ -42,9 +42,9 @@ commands:
|
||||
签到:
|
||||
description: 每日签到(中文)
|
||||
usage: /签到
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
删除账号:
|
||||
description: 申请删除账号(中文)
|
||||
usage: /删除账号 [玩家名]
|
||||
@@ -97,11 +97,14 @@ commands:
|
||||
description: 打开商店
|
||||
usage: /商店
|
||||
# testorder:
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
protect:
|
||||
description: 区域保护
|
||||
usage: /protect
|
||||
传送:
|
||||
description: 传送至领地
|
||||
usage: /传送 <领地名>
|
||||
update:
|
||||
description: 检查所有插件更新(集群控制)
|
||||
usage: /update
|
||||
@@ -110,4 +113,43 @@ commands:
|
||||
usage: /web
|
||||
控制台:
|
||||
description: 生成Web端登录令牌(中文版)
|
||||
usage: /控制台
|
||||
usage: /控制台
|
||||
tpa:
|
||||
description: tp传送指令
|
||||
usage: /tpa
|
||||
tpaccept:
|
||||
description: 接受传送
|
||||
usage: /tpaccept
|
||||
tpdeny:
|
||||
description: 拒绝传送
|
||||
usage: /tpdeny
|
||||
tpauto:
|
||||
description: 自动接收传送
|
||||
usage: /tpauto
|
||||
tpacancel:
|
||||
description: 取消传送请求
|
||||
usage: /tpacancel
|
||||
tpaall:
|
||||
description: 请求传送所有人
|
||||
usage: /tpaall
|
||||
tpahere:
|
||||
description: 请求传送ta到自己身边
|
||||
usage: /tpahere
|
||||
sethome:
|
||||
description: 设置一个家
|
||||
usage: /sethome <名称>
|
||||
delhome:
|
||||
description: 删除一个家
|
||||
usage: /delhome <名称>
|
||||
home:
|
||||
description: 传送到家
|
||||
usage: /home [名称]
|
||||
homes:
|
||||
description: 查看所有家
|
||||
usage: /homes
|
||||
group:
|
||||
description: 用户组管理
|
||||
usage: /group <buy|renew|info> [组名]
|
||||
back:
|
||||
description: 快速返回最近记录点
|
||||
usage: /back
|
||||
|
||||
Reference in New Issue
Block a user