Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182545de59 | ||
|
|
c4a5fc9a61 | ||
|
|
caaf09fce0 | ||
|
|
1133f37bf9 | ||
|
|
0ddc47b5d4 | ||
|
|
c0be75474f | ||
|
|
daa4a3cb7c | ||
|
|
09873425b9 | ||
|
|
2b20098f09 | ||
|
|
e2dd59e89e | ||
|
|
d66a792a2e | ||
|
|
87d190a013 | ||
|
|
481e2ea90b | ||
|
|
4b19b65a84 | ||
|
|
7f425f2c9f | ||
|
|
4a03608540 | ||
|
|
60504ebdf1 | ||
|
|
79e6e8354b | ||
|
|
b890480a55 | ||
|
|
104286a66e | ||
|
|
996fd41d8a | ||
|
|
c6e4e0a520 | ||
|
|
3f7f7f6c33 | ||
|
|
1f4f9e343a | ||
|
|
b96cf8c664 | ||
|
|
9d52051023 | ||
|
|
d96f16e337 | ||
|
|
535d32f2bb | ||
|
|
c79924f67d | ||
|
|
f5a3dfbda5 | ||
|
|
c568703996 | ||
|
|
3251a48b99 | ||
|
|
f83458fd4b | ||
|
|
c3d2f81e9e | ||
|
|
f8b44dd97b | ||
|
|
ab4fe441fe | ||
|
|
317d1fdbcc | ||
|
|
7ba43d820a | ||
|
|
a5edbfe314 | ||
|
|
e791548e87 | ||
|
|
501aa40677 | ||
|
|
181c2acb34 | ||
|
|
bf0688b291 | ||
|
|
765fbd74c1 | ||
|
|
d2180cec1c | ||
|
|
9ccda3f541 | ||
|
|
401fa2453a | ||
|
|
6946cfef6d | ||
|
|
89fa765351 | ||
|
|
a118aac18c | ||
|
|
da4299cc2f | ||
|
|
112f992fda | ||
|
|
3674e1e950 | ||
|
|
abdd3ad7c7 | ||
|
|
a3c8cec664 | ||
|
|
5a0166e90c | ||
|
|
6c6cc94905 | ||
|
|
f5a5b28f61 | ||
|
|
e3b459a00c | ||
|
|
3ef7fbdb7a | ||
|
|
3795dd90eb | ||
|
|
513e4c56ad | ||
|
|
8138b97b59 | ||
|
|
697b1069a2 | ||
|
|
c7cdb60b47 | ||
|
|
7489c60286 | ||
|
|
8ce2e727bb | ||
|
|
e7402985d5 | ||
|
|
c0ab91edba | ||
|
|
529b83666c | ||
|
|
569611c40f | ||
|
|
61e4ec967f | ||
|
|
57788016a4 | ||
|
|
ad935cbe09 | ||
|
|
ae94f8edef | ||
|
|
9af6c0cca0 | ||
|
|
49ad056a71 | ||
|
|
c0bb962a11 | ||
|
|
0db237ed04 | ||
|
|
5d994c0dad | ||
|
|
0ff79aa959 | ||
|
|
fa4402d426 | ||
|
|
2fb801a59b | ||
|
|
69868a4c5c | ||
|
|
4888bb414e | ||
|
|
890c637373 | ||
|
|
912bd62201 | ||
|
|
0bd4d454c3 | ||
|
|
bf32e528f1 | ||
|
|
5ef86ddfe8 | ||
|
|
e08a6bc4c7 | ||
|
|
5ec61a1d2e | ||
|
|
8c1e1a89bd | ||
|
|
bc155526ab | ||
|
|
4ae1589883 | ||
|
|
9121bb069d | ||
|
|
d306431c55 | ||
|
|
0354156eec | ||
|
|
aa8b7ce47a | ||
|
|
483acbdbb6 | ||
|
|
0775f2e84d | ||
|
|
ebd26ee123 | ||
|
|
0ff274896a | ||
|
|
f345f60ac0 | ||
|
|
babbe8936a | ||
|
|
0e0d6c6247 | ||
|
|
c6d0222c1e | ||
|
|
2bdb577c89 | ||
|
|
b29284bb1b | ||
|
|
97d96d747d | ||
|
|
2c9aa6bd8e | ||
|
|
89b3167d67 | ||
|
|
2e869a1c94 | ||
|
|
9d8d70f63f | ||
|
|
e03db026b9 | ||
|
|
36b8192dc9 | ||
|
|
2c12bbde8b | ||
|
|
9b725791aa | ||
|
|
0b8feab3e5 | ||
|
|
4c100a0c9f | ||
|
|
5fa12cddbf | ||
|
|
0ae5c8d16e | ||
|
|
d7afdd2a47 | ||
|
|
ac92cdb31e | ||
|
|
4812c093ea | ||
|
|
9916968152 | ||
|
|
41d8ffce3d | ||
|
|
2e417c67b3 | ||
|
|
626fbcd591 | ||
|
|
e4e74f1cb8 | ||
|
|
8fd5b06960 | ||
|
|
70bbdba16d | ||
|
|
dfd8fee381 | ||
|
|
7a14457713 | ||
|
|
a8451a4ccf | ||
|
|
2df8c44cda | ||
|
|
46a3dc2515 | ||
|
|
9fab58afe8 | ||
|
|
33ea4292b7 | ||
|
|
f4c5c0049a | ||
|
|
ee22930efd | ||
|
|
612ec7a66b | ||
|
|
b6fb3c3ab2 | ||
|
|
5ed006538e | ||
|
|
dec41c7b67 | ||
|
|
8b03ff4dcb | ||
|
|
2f29f3beb1 | ||
|
|
2d84153571 | ||
|
|
ebd35f5c1c | ||
|
|
0eca13f5a1 | ||
|
|
e658aa3274 | ||
|
|
121c12c71d | ||
|
|
7631e30e2a | ||
|
|
3c15e6b060 | ||
|
|
3c3d7cdbf2 | ||
|
|
caa58977a4 | ||
|
|
e18587c432 | ||
|
|
6bf3fceed6 | ||
|
|
ebece189e1 | ||
|
|
a5a63dc5d8 | ||
|
|
4297e0e02d | ||
|
|
3ebd8f0d5f | ||
|
|
7c8c7f7818 | ||
|
|
49461ac042 | ||
|
|
dd7166d73e | ||
|
|
6d655787ce | ||
|
|
d18884a3ee | ||
|
|
751eeecfd5 | ||
|
|
79b5af66b2 | ||
|
|
164267208a | ||
|
|
4cdd32913e | ||
|
|
122850f6f4 | ||
|
|
035dd418fc | ||
|
|
24e5248cec | ||
|
|
28198114e6 | ||
|
|
f0b0f23827 | ||
|
|
d62b61ef51 | ||
|
|
273a462cd3 | ||
|
|
893b431757 | ||
|
|
e847a72079 | ||
|
|
18000efb62 | ||
|
|
b403e4298d |
@@ -0,0 +1,100 @@
|
|||||||
|
name: Auto Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup JDK 25 (Temurin)
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '25'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Verify Java
|
||||||
|
run: |
|
||||||
|
echo "JAVA_HOME=$JAVA_HOME"
|
||||||
|
java -version
|
||||||
|
|
||||||
|
- name: Build with Maven
|
||||||
|
run: mvn -B clean package -DskipTests
|
||||||
|
|
||||||
|
- name: Verify JAR
|
||||||
|
run: ls -la target/*.jar
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
# 从 plugin.yml 提取版本号作为标签
|
||||||
|
VERSION=$(grep '^version:' src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
echo "Version: ${VERSION}, Tag: ${TAG}"
|
||||||
|
echo "Token prefix: ${GITEA_TOKEN:0:8}..."
|
||||||
|
|
||||||
|
# 测试 token 权限
|
||||||
|
echo "=== Testing API access ==="
|
||||||
|
curl -s -w "\nHTTP_CODE:%{http_code}\n" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"${BASE}" | tail -5
|
||||||
|
|
||||||
|
# 如果同版本标签已存在,先删除旧 release
|
||||||
|
echo "=== Checking existing tag ${TAG} ==="
|
||||||
|
RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"${BASE}/releases/tags/${TAG}")
|
||||||
|
HTTP_CODE=$(echo "$RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
|
||||||
|
echo "Check tag HTTP: ${HTTP_CODE}"
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
OLD_ID=$(echo "$RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
|
echo "Tag ${TAG} exists (release #${OLD_ID}), deleting..."
|
||||||
|
curl -s -X DELETE \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"${BASE}/releases/${OLD_ID}"
|
||||||
|
curl -s -X DELETE \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"${BASE}/tags/${TAG}"
|
||||||
|
sleep 1
|
||||||
|
elif [ "$HTTP_CODE" = "404" ]; then
|
||||||
|
echo "Tag ${TAG} not found, will create new"
|
||||||
|
else
|
||||||
|
echo "Warning: unexpected response checking tag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建新 release
|
||||||
|
echo "=== Creating release ${TAG} ==="
|
||||||
|
RELEASE_RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Sdf1_login ${TAG}\",\"body\":\"Version: ${VERSION}\\nCommit: ${{ gitea.sha }}\\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
|
||||||
|
"${BASE}/releases")
|
||||||
|
RELEASE_HTTP=$(echo "$RELEASE_RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
|
||||||
|
echo "Create release HTTP: ${RELEASE_HTTP}"
|
||||||
|
|
||||||
|
if [ "$RELEASE_HTTP" != "201" ]; then
|
||||||
|
echo "=== Release creation failed! ==="
|
||||||
|
echo "$RELEASE_RESP" | head -1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 上传 JAR 资产
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
echo "=== Uploading JAR to release #${RELEASE_ID} ==="
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@target/Sdf1_login.jar" \
|
||||||
|
"${BASE}/releases/${RELEASE_ID}/assets"
|
||||||
|
echo "✅ Release ${TAG} created with JAR"
|
||||||
|
else
|
||||||
|
echo "⚠️ Release created but could not get release ID for JAR upload"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Auto Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 26
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '26'
|
||||||
|
distribution: 'zulu'
|
||||||
|
|
||||||
|
- name: Build with Maven
|
||||||
|
run: mvn -B clean package -DskipTests
|
||||||
|
|
||||||
|
- name: Verify JAR exists
|
||||||
|
run: ls -la target/Sdf1_login.jar
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: build-${{ github.run_number }}
|
||||||
|
name: Build #${{ github.run_number }}
|
||||||
|
files: target/Sdf1_login.jar
|
||||||
|
generate_release_notes: true
|
||||||
+3
-1
@@ -81,4 +81,6 @@ lpt5
|
|||||||
lpt6
|
lpt6
|
||||||
lpt7
|
lpt7
|
||||||
lpt8
|
lpt8
|
||||||
lpt9
|
lpt9hs_err_pid*.log
|
||||||
|
hs_err_pid*.log
|
||||||
|
replay_pid*.log
|
||||||
|
|||||||
Generated
+1
-3
@@ -8,7 +8,5 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" project-jdk-name="26" project-jdk-type="JavaSDK" />
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
</project>
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
## 1. 功能介绍
|
## 1. 功能介绍
|
||||||
|
|
||||||
| 名字 | 描述/作用 | 施工状态 |
|
| 名字 | 描述/作用 | 施工状态 |
|
||||||
| :--- | :--- | :--- |
|
| :---: | :---: | :---: |
|
||||||
| 登录 | 用户登录/注册 | 已完成 |
|
| 登录 | 用户登录/注册 | 已完成 |
|
||||||
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||||
| 工单系统 | 用户提交工单 | 已完成 |
|
| 工单系统 | 用户提交工单 | 已完成 |
|
||||||
|
|||||||
@@ -49,10 +49,9 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.14.0</version>
|
<version>3.15.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>25</source>
|
<release>25</release>
|
||||||
<target>25</target>
|
|
||||||
<encoding>UTF-8</encoding>
|
<encoding>UTF-8</encoding>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public class AreaCLIManager {
|
|||||||
return Component.text(text).color(color);
|
return Component.text(text).color(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Component header(String title) {
|
public static Component header(String title) {
|
||||||
return Component.text("\n§6§l═══════ " + title + " ═══════\n");
|
return Component.text("\n§6§l═══════ " + title + " ═══════\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +110,10 @@ public class AreaCLIManager {
|
|||||||
p.sendMessage(clickableAction("🏛", "公共设施列表/传送",
|
p.sendMessage(clickableAction("🏛", "公共设施列表/传送",
|
||||||
"/protect listpublic"));
|
"/protect listpublic"));
|
||||||
|
|
||||||
|
// ★ 回声碎片:领地记录查询工具
|
||||||
|
p.sendMessage(clickableAction("🔮", "回声碎片(领地记录工具)",
|
||||||
|
"/protect echoshard"));
|
||||||
|
|
||||||
// ★ 管理员配置入口
|
// ★ 管理员配置入口
|
||||||
if (areaProtect.isAreaAdmin(p)) {
|
if (areaProtect.isAreaAdmin(p)) {
|
||||||
p.sendMessage(clickableAction("⚙", "全局配置",
|
p.sendMessage(clickableAction("⚙", "全局配置",
|
||||||
@@ -333,6 +337,22 @@ public class AreaCLIManager {
|
|||||||
"/protect settp " + land.name));
|
"/protect settp " + land.name));
|
||||||
p.sendMessage(clickableAction("🔄", "过户领地(需要所有者权限)",
|
p.sendMessage(clickableAction("🔄", "过户领地(需要所有者权限)",
|
||||||
"/protect transfeland " + land.name));
|
"/protect transfeland " + land.name));
|
||||||
|
// ★ 新增:领地公告管理
|
||||||
|
p.sendMessage(clickableAction("📢", "领地公告(进出提示消息)",
|
||||||
|
"/protect cli announcement " + land.name + " 1"));
|
||||||
|
// ★ 新增:领地改名
|
||||||
|
long[] cooldown = areaProtect.getRenameCooldown(p);
|
||||||
|
String renameHint;
|
||||||
|
if (cooldown != null) {
|
||||||
|
long remainMin = cooldown[0] / 60;
|
||||||
|
long remainSecR = cooldown[0] % 60;
|
||||||
|
renameHint = "§c冷却中(已用" + cooldown[1] + "/" + 5 + "次,"
|
||||||
|
+ remainMin + "分" + remainSecR + "秒后解锁)";
|
||||||
|
} else {
|
||||||
|
renameHint = "§a点击输入新名字(1小时限5次)";
|
||||||
|
}
|
||||||
|
p.sendMessage(clickableAction("✏️", "领地改名",
|
||||||
|
"/protect cli rename " + land.name));
|
||||||
|
|
||||||
p.sendMessage(Component.empty()
|
p.sendMessage(Component.empty()
|
||||||
.append(Component.text("§a[◀ 返回列表]")
|
.append(Component.text("§a[◀ 返回列表]")
|
||||||
@@ -520,6 +540,7 @@ public class AreaCLIManager {
|
|||||||
case "move": oldState = land.denyMove; land.denyMove = !land.denyMove; break;
|
case "move": oldState = land.denyMove; land.denyMove = !land.denyMove; break;
|
||||||
case "block_place": oldState = land.denyBlockPlace; land.denyBlockPlace = !land.denyBlockPlace; break;
|
case "block_place": oldState = land.denyBlockPlace; land.denyBlockPlace = !land.denyBlockPlace; break;
|
||||||
case "block_break": oldState = land.denyBlockBreak; land.denyBlockBreak = !land.denyBlockBreak; break;
|
case "block_break": oldState = land.denyBlockBreak; land.denyBlockBreak = !land.denyBlockBreak; break;
|
||||||
|
case "entity_interact": oldState = land.denyEntityInteract; land.denyEntityInteract = !land.denyEntityInteract; break;
|
||||||
case "container": oldState = land.denyContainer; land.denyContainer = !land.denyContainer; break;
|
case "container": oldState = land.denyContainer; land.denyContainer = !land.denyContainer; break;
|
||||||
case "pvp": oldState = land.denyPVP; land.denyPVP = !land.denyPVP; break;
|
case "pvp": oldState = land.denyPVP; land.denyPVP = !land.denyPVP; break;
|
||||||
case "mount": oldState = land.denyMount; land.denyMount = !land.denyMount; break;
|
case "mount": oldState = land.denyMount; land.denyMount = !land.denyMount; break;
|
||||||
@@ -540,6 +561,7 @@ public class AreaCLIManager {
|
|||||||
case "item_frame": oldState = land.denyItemFrame; land.denyItemFrame = !land.denyItemFrame; break;
|
case "item_frame": oldState = land.denyItemFrame; land.denyItemFrame = !land.denyItemFrame; break;
|
||||||
case "redstone": oldState = land.denyRedstoneInteraction; land.denyRedstoneInteraction = !land.denyRedstoneInteraction; break;
|
case "redstone": oldState = land.denyRedstoneInteraction; land.denyRedstoneInteraction = !land.denyRedstoneInteraction; break;
|
||||||
case "door": oldState = land.denyDoorInteraction; land.denyDoorInteraction = !land.denyDoorInteraction; break;
|
case "door": oldState = land.denyDoorInteraction; land.denyDoorInteraction = !land.denyDoorInteraction; break;
|
||||||
|
case "sign_edit": oldState = land.denySignEdit; land.denySignEdit = !land.denySignEdit; break;
|
||||||
case "audio": oldState = land.denyNoteblockJukebox; land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
|
case "audio": oldState = land.denyNoteblockJukebox; land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
|
||||||
case "lead": oldState = land.denyLead; land.denyLead = !land.denyLead; break;
|
case "lead": oldState = land.denyLead; land.denyLead = !land.denyLead; break;
|
||||||
case "crop_harvest": oldState = land.denyCropHarvest; land.denyCropHarvest = !land.denyCropHarvest; break;
|
case "crop_harvest": oldState = land.denyCropHarvest; land.denyCropHarvest = !land.denyCropHarvest; break;
|
||||||
@@ -547,6 +569,7 @@ public class AreaCLIManager {
|
|||||||
case "animal_feed": oldState = land.denyAnimalFeeding; land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
|
case "animal_feed": oldState = land.denyAnimalFeeding; land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
|
||||||
case "mob_attack": oldState = land.denyMobAttack; land.denyMobAttack = !land.denyMobAttack; break;
|
case "mob_attack": oldState = land.denyMobAttack; land.denyMobAttack = !land.denyMobAttack; break;
|
||||||
case "glowing": oldState = land.denyGlowing; land.denyGlowing = !land.denyGlowing; break;
|
case "glowing": oldState = land.denyGlowing; land.denyGlowing = !land.denyGlowing; break;
|
||||||
|
case "visitor_teleport": oldState = land.allowVisitorTeleport; land.allowVisitorTeleport = !land.allowVisitorTeleport; break;
|
||||||
case "peace_mode": oldState = land.peaceMode; land.peaceMode = !land.peaceMode; break;
|
case "peace_mode": oldState = land.peaceMode; land.peaceMode = !land.peaceMode; break;
|
||||||
case "public_building": oldState = land.isPublicBuilding; land.isPublicBuilding = !land.isPublicBuilding; break;
|
case "public_building": oldState = land.isPublicBuilding; land.isPublicBuilding = !land.isPublicBuilding; break;
|
||||||
}
|
}
|
||||||
@@ -575,6 +598,7 @@ public class AreaCLIManager {
|
|||||||
case "move": return "denyMove";
|
case "move": return "denyMove";
|
||||||
case "block_place": return "denyBlockPlace";
|
case "block_place": return "denyBlockPlace";
|
||||||
case "block_break": return "denyBlockBreak";
|
case "block_break": return "denyBlockBreak";
|
||||||
|
case "entity_interact": return "denyEntityInteract";
|
||||||
case "container": return "denyContainer";
|
case "container": return "denyContainer";
|
||||||
case "pvp": return "denyPVP";
|
case "pvp": return "denyPVP";
|
||||||
case "mount": return "denyMount";
|
case "mount": return "denyMount";
|
||||||
@@ -604,6 +628,8 @@ public class AreaCLIManager {
|
|||||||
case "glowing": return "denyGlowing";
|
case "glowing": return "denyGlowing";
|
||||||
case "peace_mode": return "peaceMode";
|
case "peace_mode": return "peaceMode";
|
||||||
case "public_building": return "isPublicBuilding";
|
case "public_building": return "isPublicBuilding";
|
||||||
|
case "visitor_teleport": return "allowVisitorTeleport";
|
||||||
|
case "tp": return "allowTeleport";
|
||||||
default: return shortKey;
|
default: return shortKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,6 +657,7 @@ public class AreaCLIManager {
|
|||||||
perms.add(new PermItem("move", "移动", !land.denyMove));
|
perms.add(new PermItem("move", "移动", !land.denyMove));
|
||||||
perms.add(new PermItem("block_place", "放置方块", !land.denyBlockPlace));
|
perms.add(new PermItem("block_place", "放置方块", !land.denyBlockPlace));
|
||||||
perms.add(new PermItem("block_break", "破坏方块", !land.denyBlockBreak));
|
perms.add(new PermItem("block_break", "破坏方块", !land.denyBlockBreak));
|
||||||
|
perms.add(new PermItem("entity_interact", "实体交互(船/矿车/盔甲架/展示框)", !land.denyEntityInteract));
|
||||||
perms.add(new PermItem("container", "容器管理", !land.denyContainer));
|
perms.add(new PermItem("container", "容器管理", !land.denyContainer));
|
||||||
perms.add(new PermItem("pvp", "玩家对战", !land.denyPVP));
|
perms.add(new PermItem("pvp", "玩家对战", !land.denyPVP));
|
||||||
perms.add(new PermItem("mount", "骑乘坐具", !land.denyMount));
|
perms.add(new PermItem("mount", "骑乘坐具", !land.denyMount));
|
||||||
@@ -651,6 +678,7 @@ public class AreaCLIManager {
|
|||||||
perms.add(new PermItem("item_frame", "禁止展示框", !land.denyItemFrame));
|
perms.add(new PermItem("item_frame", "禁止展示框", !land.denyItemFrame));
|
||||||
perms.add(new PermItem("redstone", "红石电路(中继器/比较器)", !land.denyRedstoneInteraction));
|
perms.add(new PermItem("redstone", "红石电路(中继器/比较器)", !land.denyRedstoneInteraction));
|
||||||
perms.add(new PermItem("door", "禁止门禁(门/按钮/压力板)", !land.denyDoorInteraction));
|
perms.add(new PermItem("door", "禁止门禁(门/按钮/压力板)", !land.denyDoorInteraction));
|
||||||
|
perms.add(new PermItem("sign_edit", land.denySignEdit ? "禁止编辑告示牌" : "允许编辑告示牌", !land.denySignEdit));
|
||||||
perms.add(new PermItem("audio", "音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
|
perms.add(new PermItem("audio", "音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
|
||||||
perms.add(new PermItem("lead", "拴绳使用", !land.denyLead));
|
perms.add(new PermItem("lead", "拴绳使用", !land.denyLead));
|
||||||
perms.add(new PermItem("crop_harvest", land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
|
perms.add(new PermItem("crop_harvest", land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
|
||||||
@@ -658,6 +686,7 @@ public class AreaCLIManager {
|
|||||||
perms.add(new PermItem("animal_feed", land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
|
perms.add(new PermItem("animal_feed", land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
|
||||||
perms.add(new PermItem("mob_attack", land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
perms.add(new PermItem("mob_attack", land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
||||||
perms.add(new PermItem("glowing", "玩家发光", !land.denyGlowing));
|
perms.add(new PermItem("glowing", "玩家发光", !land.denyGlowing));
|
||||||
|
perms.add(new PermItem("visitor_teleport", "传送", land.allowVisitorTeleport));
|
||||||
perms.add(new PermItem("peace_mode", "和平模式", land.peaceMode));
|
perms.add(new PermItem("peace_mode", "和平模式", land.peaceMode));
|
||||||
perms.add(new PermItem("public_building", "公共建筑设施", land.isPublicBuilding));
|
perms.add(new PermItem("public_building", "公共建筑设施", land.isPublicBuilding));
|
||||||
return perms;
|
return perms;
|
||||||
@@ -688,6 +717,7 @@ public class AreaCLIManager {
|
|||||||
case "item_frame": return "禁止展示框";
|
case "item_frame": return "禁止展示框";
|
||||||
case "redstone": return "红石电路";
|
case "redstone": return "红石电路";
|
||||||
case "door": return "禁止门禁";
|
case "door": return "禁止门禁";
|
||||||
|
case "sign_edit": return "告示牌编辑";
|
||||||
case "audio": return "音频";
|
case "audio": return "音频";
|
||||||
case "lead": return "拴绳使用";
|
case "lead": return "拴绳使用";
|
||||||
case "crop_harvest": return "农作物收获";
|
case "crop_harvest": return "农作物收获";
|
||||||
@@ -697,6 +727,8 @@ public class AreaCLIManager {
|
|||||||
case "glowing": return "玩家发光";
|
case "glowing": return "玩家发光";
|
||||||
case "peace_mode": return "和平模式";
|
case "peace_mode": return "和平模式";
|
||||||
case "public_building": return "公共建筑设施";
|
case "public_building": return "公共建筑设施";
|
||||||
|
case "visitor_teleport": return "传送";
|
||||||
|
case "tp": return "传送";
|
||||||
default: return key;
|
default: return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -815,7 +847,7 @@ public class AreaCLIManager {
|
|||||||
// 获取该成员的per-player权限
|
// 获取该成员的per-player权限
|
||||||
Map<String, Boolean> playerPerms = areaProtect.getPlayerPermMap(landId, targetPlayer);
|
Map<String, Boolean> playerPerms = areaProtect.getPlayerPermMap(landId, targetPlayer);
|
||||||
|
|
||||||
// ★ 管理员简化面板:管理员自动获得所有权限,只显示身份切换+发光开关
|
// ★ 管理员简化面板:只要玩家是管理员,隐藏全部权限配置,只保留玩家发光
|
||||||
boolean isTargetAdmin = areaProtect.isLandAdmin(land.name, targetPlayer);
|
boolean isTargetAdmin = areaProtect.isLandAdmin(land.name, targetPlayer);
|
||||||
if (isTargetAdmin) {
|
if (isTargetAdmin) {
|
||||||
p.sendMessage(header(targetPlayer + " 的独立权限"));
|
p.sendMessage(header(targetPlayer + " 的独立权限"));
|
||||||
@@ -824,8 +856,8 @@ public class AreaCLIManager {
|
|||||||
p.sendMessage(Component.text("§7管理员不受任何权限限制(发光效果除外)"));
|
p.sendMessage(Component.text("§7管理员不受任何权限限制(发光效果除外)"));
|
||||||
p.sendMessage(Component.text(""));
|
p.sendMessage(Component.text(""));
|
||||||
|
|
||||||
// ★ 领地主:可切换管理员身份
|
// ★ 领地主或插件管理员:可切换管理员身份(撤职)
|
||||||
if (isOwner && !targetPlayer.equalsIgnoreCase(land.owner)) {
|
if ((isOwner || isAdmin) && !targetPlayer.equalsIgnoreCase(land.owner)) {
|
||||||
Component adminLine = Component.empty();
|
Component adminLine = Component.empty();
|
||||||
adminLine = adminLine.append(Component.text("§6✔ §f身份: §6管理员"));
|
adminLine = adminLine.append(Component.text("§6✔ §f身份: §6管理员"));
|
||||||
adminLine = adminLine.append(Component.text(" "));
|
adminLine = adminLine.append(Component.text(" "));
|
||||||
@@ -926,7 +958,7 @@ public class AreaCLIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ★ 设为管理员按钮(全局置顶,打印在最后=聊天窗口最底部)
|
// ★ 设为管理员按钮(全局置顶,打印在最后=聊天窗口最底部)
|
||||||
if (isOwner && !targetPlayer.equalsIgnoreCase(land.owner)) {
|
if ((isOwner || isAdmin) && !targetPlayer.equalsIgnoreCase(land.owner)) {
|
||||||
Component adminLine = Component.empty();
|
Component adminLine = Component.empty();
|
||||||
adminLine = adminLine.append(Component.text("§7✘ §f身份: §7普通成员"));
|
adminLine = adminLine.append(Component.text("§7✘ §f身份: §7普通成员"));
|
||||||
adminLine = adminLine.append(Component.text(" "));
|
adminLine = adminLine.append(Component.text(" "));
|
||||||
@@ -965,8 +997,9 @@ public class AreaCLIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isOwner = p.getName().equalsIgnoreCase(land.owner);
|
boolean isOwner = p.getName().equalsIgnoreCase(land.owner);
|
||||||
if (!isOwner) {
|
boolean isAdmin = areaProtect.isAreaAdmin(p);
|
||||||
p.sendMessage(Component.text("§c只有领地所有者才能操作管理员身份"));
|
if (!isOwner && !isAdmin) {
|
||||||
|
p.sendMessage(Component.text("§c只有领地所有者或插件管理员才能操作管理员身份"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,6 +1110,7 @@ public class AreaCLIManager {
|
|||||||
{"move", "移动", "denyMove"},
|
{"move", "移动", "denyMove"},
|
||||||
{"block_place", "放置方块", "denyBlockPlace"},
|
{"block_place", "放置方块", "denyBlockPlace"},
|
||||||
{"block_break", "破坏方块", "denyBlockBreak"},
|
{"block_break", "破坏方块", "denyBlockBreak"},
|
||||||
|
{"entity_interact", "实体交互", "denyEntityInteract"},
|
||||||
{"container", "容器管理", "denyContainer"},
|
{"container", "容器管理", "denyContainer"},
|
||||||
{"pvp", "玩家对战", "denyPVP"},
|
{"pvp", "玩家对战", "denyPVP"},
|
||||||
{"mount", "骑乘坐具", "denyMount"},
|
{"mount", "骑乘坐具", "denyMount"},
|
||||||
@@ -1097,6 +1131,7 @@ public class AreaCLIManager {
|
|||||||
{"item_frame", "禁止展示框", "denyItemFrame"},
|
{"item_frame", "禁止展示框", "denyItemFrame"},
|
||||||
{"redstone", "红石电路(中继器/比较器)", "denyRedstoneInteraction"},
|
{"redstone", "红石电路(中继器/比较器)", "denyRedstoneInteraction"},
|
||||||
{"door", "禁止门禁(门/按钮/压力板)", "denyDoorInteraction"},
|
{"door", "禁止门禁(门/按钮/压力板)", "denyDoorInteraction"},
|
||||||
|
{"sign_edit", "禁止编辑告示牌", "denySignEdit"},
|
||||||
{"audio", "音频(音符盒/唱片机)", "denyNoteblockJukebox"},
|
{"audio", "音频(音符盒/唱片机)", "denyNoteblockJukebox"},
|
||||||
{"lead", "拴绳使用", "denyLead"},
|
{"lead", "拴绳使用", "denyLead"},
|
||||||
{"crop_harvest", "农作物收获", "denyCropHarvest"},
|
{"crop_harvest", "农作物收获", "denyCropHarvest"},
|
||||||
@@ -1104,7 +1139,8 @@ public class AreaCLIManager {
|
|||||||
{"animal_feed", "投喂动物", "denyAnimalFeeding"},
|
{"animal_feed", "投喂动物", "denyAnimalFeeding"},
|
||||||
{"mob_attack", "攻击生物", "denyMobAttack"},
|
{"mob_attack", "攻击生物", "denyMobAttack"},
|
||||||
{"glowing", "玩家发光", "denyGlowing"},
|
{"glowing", "玩家发光", "denyGlowing"},
|
||||||
{"peace_mode", "和平模式", "peaceMode"}
|
{"peace_mode", "和平模式", "peaceMode"},
|
||||||
|
{"tp", "传送", "allowTeleport"}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String[] def : permDefs) {
|
for (String[] def : permDefs) {
|
||||||
@@ -1133,6 +1169,10 @@ public class AreaCLIManager {
|
|||||||
if (field.equals("denyMobAttack")) {
|
if (field.equals("denyMobAttack")) {
|
||||||
name = landDefault ? "禁止攻击生物" : "允许攻击生物";
|
name = landDefault ? "禁止攻击生物" : "允许攻击生物";
|
||||||
}
|
}
|
||||||
|
// ★ denySignEdit显示名称
|
||||||
|
if (field.equals("denySignEdit")) {
|
||||||
|
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有per-player覆盖,使用覆盖值;否则使用领地默认
|
// 如果有per-player覆盖,使用覆盖值;否则使用领地默认
|
||||||
if (playerPerms.containsKey(field)) {
|
if (playerPerms.containsKey(field)) {
|
||||||
@@ -1155,6 +1195,7 @@ public class AreaCLIManager {
|
|||||||
case "denyMove": return land.denyMove;
|
case "denyMove": return land.denyMove;
|
||||||
case "denyBlockPlace": return land.denyBlockPlace;
|
case "denyBlockPlace": return land.denyBlockPlace;
|
||||||
case "denyBlockBreak": return land.denyBlockBreak;
|
case "denyBlockBreak": return land.denyBlockBreak;
|
||||||
|
case "denyEntityInteract": return land.denyEntityInteract;
|
||||||
case "denyContainer": return land.denyContainer;
|
case "denyContainer": return land.denyContainer;
|
||||||
case "denyPVP": return land.denyPVP;
|
case "denyPVP": return land.denyPVP;
|
||||||
case "denyMount": return land.denyMount;
|
case "denyMount": return land.denyMount;
|
||||||
@@ -1175,6 +1216,7 @@ public class AreaCLIManager {
|
|||||||
case "denyItemFrame": return land.denyItemFrame;
|
case "denyItemFrame": return land.denyItemFrame;
|
||||||
case "denyRedstoneInteraction": return land.denyRedstoneInteraction;
|
case "denyRedstoneInteraction": return land.denyRedstoneInteraction;
|
||||||
case "denyDoorInteraction": return land.denyDoorInteraction;
|
case "denyDoorInteraction": return land.denyDoorInteraction;
|
||||||
|
case "denySignEdit": return land.denySignEdit;
|
||||||
case "denyNoteblockJukebox": return land.denyNoteblockJukebox;
|
case "denyNoteblockJukebox": return land.denyNoteblockJukebox;
|
||||||
case "denyLead": return land.denyLead;
|
case "denyLead": return land.denyLead;
|
||||||
case "denyCropHarvest": return land.denyCropHarvest;
|
case "denyCropHarvest": return land.denyCropHarvest;
|
||||||
@@ -1182,6 +1224,9 @@ public class AreaCLIManager {
|
|||||||
case "denyAnimalFeeding": return land.denyAnimalFeeding;
|
case "denyAnimalFeeding": return land.denyAnimalFeeding;
|
||||||
case "denyGlowing": return land.denyGlowing;
|
case "denyGlowing": return land.denyGlowing;
|
||||||
case "peaceMode": return land.peaceMode;
|
case "peaceMode": return land.peaceMode;
|
||||||
|
case "isPublicBuilding": return land.isPublicBuilding;
|
||||||
|
case "allowVisitorTeleport": return land.allowVisitorTeleport;
|
||||||
|
case "allowTeleport": return false; // per-player 默认关闭
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1579,4 +1624,155 @@ public class AreaCLIManager {
|
|||||||
p.sendMessage(Component.text("§a已添加增益效果: §f" + desc));
|
p.sendMessage(Component.text("§a已添加增益效果: §f" + desc));
|
||||||
areaProtect.saveAreaToDb(land);
|
areaProtect.saveAreaToDb(land);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 领地公告管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领地公告管理主页面
|
||||||
|
* @param subPage 1=主菜单, 2=编辑进入消息, 3=编辑离开消息
|
||||||
|
*/
|
||||||
|
public void showAnnouncementManagement(Player p, String landName, int subPage) {
|
||||||
|
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
|
||||||
|
if (land == null) {
|
||||||
|
p.sendMessage(Component.text("§c领地不存在: " + landName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
boolean isOwner = p.getName().equalsIgnoreCase(land.owner);
|
||||||
|
boolean isAdmin = areaProtect.isAreaAdmin(p);
|
||||||
|
if (!isOwner && !isAdmin) {
|
||||||
|
p.sendMessage(Component.text("§c需要领地所有者或管理员权限"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 主菜单 ==========
|
||||||
|
if (subPage == 1) {
|
||||||
|
p.sendMessage(header("领地公告: " + landName));
|
||||||
|
p.sendMessage(Component.text("§7配置玩家进出领地时的提示消息"));
|
||||||
|
p.sendMessage(Component.text("§7§l───────────────────────────────"));
|
||||||
|
|
||||||
|
// 显示当前进入消息(含变量说明)
|
||||||
|
String enterMsg = (land.enterMsg != null && !land.enterMsg.isEmpty()) ? land.enterMsg : "§7(未设置,默认: 欢迎(玩家)来到(领地))";
|
||||||
|
p.sendMessage(Component.text("§a进入消息: §f" + enterMsg));
|
||||||
|
p.sendMessage(Component.empty()
|
||||||
|
.append(Component.text("§e[编辑进入消息] ")
|
||||||
|
.hoverEvent(HoverEvent.showText(Component.text("§e点击编辑进入领地时的提示消息")))
|
||||||
|
.clickEvent(ClickEvent.runCommand("/protect cli announcement " + landName + " 2")))
|
||||||
|
.append(Component.text("§7点击编辑")));
|
||||||
|
|
||||||
|
p.sendMessage(Component.text(""));
|
||||||
|
|
||||||
|
// 显示当前离开消息(含变量说明)
|
||||||
|
String leaveMsg = (land.leaveMsg != null && !land.leaveMsg.isEmpty()) ? land.leaveMsg : "§7(未设置,默认: 感谢(玩家)光临(领地))";
|
||||||
|
p.sendMessage(Component.text("§a离开消息: §f" + leaveMsg));
|
||||||
|
p.sendMessage(Component.empty()
|
||||||
|
.append(Component.text("§e[编辑离开消息] ")
|
||||||
|
.hoverEvent(HoverEvent.showText(Component.text("§e点击编辑离开领地时的提示消息")))
|
||||||
|
.clickEvent(ClickEvent.runCommand("/protect cli announcement " + landName + " 3")))
|
||||||
|
.append(Component.text("§7点击编辑")));
|
||||||
|
|
||||||
|
p.sendMessage(Component.text("§7§l───────────────────────────────"));
|
||||||
|
p.sendMessage(Component.text("§7变量: §f(玩家) §7= 玩家名, §f(领地) §7= 领地名"));
|
||||||
|
p.sendMessage(Component.text("§7支持括号: §f() [] {} 【】 <> 《》"));
|
||||||
|
p.sendMessage(Component.text("§7颜色: §f&a绿色 §6金色 §c红色 §7灰色"));
|
||||||
|
|
||||||
|
// 返回按钮
|
||||||
|
p.sendMessage(clickableAction("◀", "返回领地管理", "/protect cli manage " + landName));
|
||||||
|
p.sendMessage(Component.text("§7§l───────────────────────────────"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 编辑进入消息 ==========
|
||||||
|
else if (subPage == 2) {
|
||||||
|
p.sendMessage(header("编辑进入消息: " + landName));
|
||||||
|
p.sendMessage(Component.text("§7当前进入消息: §f" + (land.enterMsg != null && !land.enterMsg.isEmpty() ? land.enterMsg : "(空)")));
|
||||||
|
p.sendMessage(Component.text("§7§l───────────────────────────────"));
|
||||||
|
p.sendMessage(Component.text("§e请在聊天栏输入新的进入消息:"));
|
||||||
|
p.sendMessage(Component.text("§7输入 §c清空 §7可清除消息,输入 §c取消 §7或 §c0 §7放弃修改"));
|
||||||
|
|
||||||
|
// 标记等待输入
|
||||||
|
pendingEffectInput.put(p.getUniqueId(), new String[]{"announcement_enter", landName, ""});
|
||||||
|
savePageInfo(p, "announcement", landName, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 编辑离开消息 ==========
|
||||||
|
else if (subPage == 3) {
|
||||||
|
p.sendMessage(header("编辑离开消息: " + landName));
|
||||||
|
p.sendMessage(Component.text("§7当前离开消息: §f" + (land.leaveMsg != null && !land.leaveMsg.isEmpty() ? land.leaveMsg : "(空)")));
|
||||||
|
p.sendMessage(Component.text("§7§l───────────────────────────────"));
|
||||||
|
p.sendMessage(Component.text("§e请在聊天栏输入新的离开消息:"));
|
||||||
|
p.sendMessage(Component.text("§7输入 §c清空 §7可清除消息,输入 §c取消 §7或 §c0 §7放弃修改"));
|
||||||
|
|
||||||
|
// 标记等待输入
|
||||||
|
pendingEffectInput.put(p.getUniqueId(), new String[]{"announcement_leave", landName, ""});
|
||||||
|
savePageInfo(p, "announcement", landName, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置待公告输入状态(GUI调用入口)
|
||||||
|
* @param type "announcement_enter" 或 "announcement_leave"
|
||||||
|
*/
|
||||||
|
public void setPendingAnnouncementInput(java.util.UUID uuid, String landName, String type) {
|
||||||
|
pendingEffectInput.put(uuid, new String[]{type, landName, ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理公告消息输入(在ChatEvent中调用)
|
||||||
|
*/
|
||||||
|
public boolean handleAnnouncementInput(Player p, String message) {
|
||||||
|
String[] pending = pendingEffectInput.get(p.getUniqueId());
|
||||||
|
if (pending == null) return false;
|
||||||
|
|
||||||
|
String type = pending[0];
|
||||||
|
String landName = pending[1];
|
||||||
|
|
||||||
|
if (!type.startsWith("announcement_")) return false;
|
||||||
|
|
||||||
|
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
|
||||||
|
if (land == null) {
|
||||||
|
p.sendMessage(Component.text("§c领地不存在: " + landName));
|
||||||
|
pendingEffectInput.remove(p.getUniqueId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String field = type.equals("announcement_enter") ? "enter_msg" : "leave_msg";
|
||||||
|
// ★ 取消/0 = 保持原样放弃修改
|
||||||
|
if (message.equals("取消") || message.equals("0")) {
|
||||||
|
p.sendMessage(Component.text("§e已取消修改,保持原样"));
|
||||||
|
pendingEffectInput.remove(p.getUniqueId());
|
||||||
|
showAnnouncementManagement(p, landName, 1); // 返回主菜单,不再进入编辑子页面
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ URL过滤:包含链接则拒绝,不保存
|
||||||
|
ChatFilterManager cf = plugin.getChatFilter();
|
||||||
|
if (cf != null) {
|
||||||
|
java.util.List<String> urls = cf.extractUrls(message);
|
||||||
|
if (!urls.isEmpty()) {
|
||||||
|
p.sendMessage(Component.text("§c消息不能包含链接!"));
|
||||||
|
// 保持pending状态,允许用户重新输入
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = message.equals("清空") ? "" : message;
|
||||||
|
|
||||||
|
// 保存到内存
|
||||||
|
if (type.equals("announcement_enter")) {
|
||||||
|
land.enterMsg = value;
|
||||||
|
} else {
|
||||||
|
land.leaveMsg = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到DB
|
||||||
|
areaProtect.saveAreaToDb(land);
|
||||||
|
|
||||||
|
p.sendMessage(Component.text("§a已保存" + (type.equals("announcement_enter") ? "进入" : "离开") + "消息: §f" + (value.isEmpty() ? "(已清空)" : value)));
|
||||||
|
pendingEffectInput.remove(p.getUniqueId());
|
||||||
|
|
||||||
|
// 返回公告管理页面
|
||||||
|
showAnnouncementManagement(p, landName, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ public class AreaGUIManager implements Listener {
|
|||||||
"",
|
"",
|
||||||
"§e点击切换"));
|
"§e点击切换"));
|
||||||
|
|
||||||
|
// ★ 回声碎片:领地记录查询工具(位置33)
|
||||||
|
inv.setItem(33, createItem(Material.ECHO_SHARD, "§b§l回声碎片",
|
||||||
|
"§7获取领地记录查询工具",
|
||||||
|
"§7手持后右键/左键方块或容器",
|
||||||
|
"§7查看该处的破坏/拿取记录",
|
||||||
|
"",
|
||||||
|
"§e点击获取"));
|
||||||
|
|
||||||
// 返回按钮(位置49)
|
// 返回按钮(位置49)
|
||||||
inv.setItem(49, createItem(Material.ARROW, "§c§l返回主菜单", ""));
|
inv.setItem(49, createItem(Material.ARROW, "§c§l返回主菜单", ""));
|
||||||
|
|
||||||
@@ -245,6 +253,19 @@ public class AreaGUIManager implements Listener {
|
|||||||
"§7将领地所有权转移给他人",
|
"§7将领地所有权转移给他人",
|
||||||
"§7需要有领地所有者权限"));
|
"§7需要有领地所有者权限"));
|
||||||
|
|
||||||
|
// 领地改名(位置33)
|
||||||
|
long[] cd = areaProtect.getRenameCooldown(p);
|
||||||
|
String renameLore;
|
||||||
|
if (cd != null) {
|
||||||
|
long rm = cd[0] / 60;
|
||||||
|
long rs = cd[0] % 60;
|
||||||
|
renameLore = "§c冷却中(已用" + cd[1] + "/5次)\n§c" + rm + "分" + rs + "秒后解锁";
|
||||||
|
} else {
|
||||||
|
renameLore = "§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5\n§71小时限5次,自然滚动窗口";
|
||||||
|
}
|
||||||
|
inv.setItem(33, createItem(Material.NAME_TAG, "§e§l领地改名",
|
||||||
|
renameLore, "", "§e点击输入新名字"));
|
||||||
|
|
||||||
// 返回按钮(位置48)
|
// 返回按钮(位置48)
|
||||||
inv.setItem(48, createItem(Material.ARROW, "§c§l返回领地列表", ""));
|
inv.setItem(48, createItem(Material.ARROW, "§c§l返回领地列表", ""));
|
||||||
|
|
||||||
@@ -435,14 +456,15 @@ public class AreaGUIManager implements Listener {
|
|||||||
Map<String, Boolean> playerPerms = areaProtect.getPlayerPermMap(landId, targetPlayer);
|
Map<String, Boolean> playerPerms = areaProtect.getPlayerPermMap(landId, targetPlayer);
|
||||||
int slot = 0;
|
int slot = 0;
|
||||||
|
|
||||||
// ★ 权限检查:只有所有者或管理员能编辑成员权限
|
// ★ 权限检查:只有所有者、全局tag管理员、或领地管理员能编辑成员权限
|
||||||
boolean isOwner = p.getName().equalsIgnoreCase(land.owner);
|
boolean isOwner = p.getName().equalsIgnoreCase(land.owner);
|
||||||
|
boolean isGlobalAdmin = areaProtect.isAreaAdmin(p);
|
||||||
|
|
||||||
// ★ 管理员状态检测(领地级管理员,非全局tag)
|
// ★ 管理员状态检测(领地级管理员,非全局tag)
|
||||||
boolean isAdmin = areaProtect.isLandAdmin(landName, targetPlayer);
|
boolean isAdmin = areaProtect.isLandAdmin(landName, targetPlayer);
|
||||||
|
|
||||||
// ★ 管理员提示:非领地主操作管理员时简化显示
|
// ★ 管理员提示:非领地主且非全局tag管理员操作管理员时简化显示
|
||||||
if (isAdmin && !isOwner) {
|
if (isAdmin && !isOwner && !isGlobalAdmin) {
|
||||||
inv.setItem(22, createItem(Material.GOLD_BLOCK, "§6§l管理员权限",
|
inv.setItem(22, createItem(Material.GOLD_BLOCK, "§6§l管理员权限",
|
||||||
"§7该玩家是此领地管理员,自动获得所有权限",
|
"§7该玩家是此领地管理员,自动获得所有权限",
|
||||||
"§7点击切换管理员身份"));
|
"§7点击切换管理员身份"));
|
||||||
@@ -465,16 +487,18 @@ public class AreaGUIManager implements Listener {
|
|||||||
// 权限列表
|
// 权限列表
|
||||||
String[][] permDefs = {
|
String[][] permDefs = {
|
||||||
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
||||||
|
{"实体交互", "denyEntityInteract"},
|
||||||
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
||||||
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
||||||
{"药水效果", "denyPotion"}, {"点燃", "denyFire"}, {"火焰蔓延", "denyFireSpread"},
|
{"药水效果", "denyPotion"}, {"点燃", "denyFire"}, {"火焰蔓延", "denyFireSpread"},
|
||||||
{"允许拾取", "allowPickup"}, {"允许丢弃", "allowDrop"}, {"爆炸", "denyExplosion"},
|
{"允许拾取", "allowPickup"}, {"允许丢弃", "allowDrop"}, {"爆炸", "denyExplosion"},
|
||||||
{"摔落伤害", "denyFallDamage"}, {"饥饿", "denyHunger"}, {"所有伤害", "denyAllDamage"},
|
{"摔落伤害", "denyFallDamage"}, {"饥饿", "denyHunger"}, {"所有伤害", "denyAllDamage"},
|
||||||
{"所有效果", "denyAllEffects"}, {"禁止展示框", "denyItemFrame"}, {"红石电路", "denyRedstoneInteraction"},
|
{"所有效果", "denyAllEffects"}, {"禁止展示框", "denyItemFrame"}, {"红石电路", "denyRedstoneInteraction"},
|
||||||
{"禁止门禁", "denyDoorInteraction"}, {"音频", "denyNoteblockJukebox"}, {"拴绳使用", "denyLead"},
|
{"门禁交互", "denyDoorInteraction"}, {"音频", "denyNoteblockJukebox"}, {"拴绳使用", "denyLead"},
|
||||||
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
||||||
{"攻击生物", "denyMobAttack"},
|
{"攻击生物", "denyMobAttack"},
|
||||||
{"玩家发光", "denyGlowing"}, {"和平模式", "peaceMode"}
|
{"禁止编辑告示牌", "denySignEdit"},
|
||||||
|
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String[] def : permDefs) {
|
for (String[] def : permDefs) {
|
||||||
@@ -502,6 +526,9 @@ public class AreaGUIManager implements Listener {
|
|||||||
if (field.equals("denyMobAttack")) {
|
if (field.equals("denyMobAttack")) {
|
||||||
name = landDefault ? "禁止攻击生物" : "允许攻击生物";
|
name = landDefault ? "禁止攻击生物" : "允许攻击生物";
|
||||||
}
|
}
|
||||||
|
if (field.equals("denySignEdit")) {
|
||||||
|
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
|
||||||
|
}
|
||||||
|
|
||||||
// ★ 对于 allowPickup/allowDrop,enabled=true 表示"允许操作"(即 allow=true)
|
// ★ 对于 allowPickup/allowDrop,enabled=true 表示"允许操作"(即 allow=true)
|
||||||
// 其他权限,enabled = true 表示"允许操作"(即 deny=false)
|
// 其他权限,enabled = true 表示"允许操作"(即 deny=false)
|
||||||
@@ -551,6 +578,7 @@ public class AreaGUIManager implements Listener {
|
|||||||
case "denyMove": return land.denyMove;
|
case "denyMove": return land.denyMove;
|
||||||
case "denyBlockPlace": return land.denyBlockPlace;
|
case "denyBlockPlace": return land.denyBlockPlace;
|
||||||
case "denyBlockBreak": return land.denyBlockBreak;
|
case "denyBlockBreak": return land.denyBlockBreak;
|
||||||
|
case "denyEntityInteract": return land.denyEntityInteract;
|
||||||
case "denyContainer": return land.denyContainer;
|
case "denyContainer": return land.denyContainer;
|
||||||
case "denyPVP": return land.denyPVP;
|
case "denyPVP": return land.denyPVP;
|
||||||
case "denyMount": return land.denyMount;
|
case "denyMount": return land.denyMount;
|
||||||
@@ -577,6 +605,7 @@ public class AreaGUIManager implements Listener {
|
|||||||
case "denyWoolShear": return land.denyWoolShear;
|
case "denyWoolShear": return land.denyWoolShear;
|
||||||
case "denyAnimalFeeding": return land.denyAnimalFeeding;
|
case "denyAnimalFeeding": return land.denyAnimalFeeding;
|
||||||
case "denyGlowing": return land.denyGlowing;
|
case "denyGlowing": return land.denyGlowing;
|
||||||
|
case "denySignEdit": return land.denySignEdit;
|
||||||
case "peaceMode": return land.peaceMode;
|
case "peaceMode": return land.peaceMode;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
@@ -633,6 +662,7 @@ public class AreaGUIManager implements Listener {
|
|||||||
perms.add(new PermEntry("移动", !land.denyMove));
|
perms.add(new PermEntry("移动", !land.denyMove));
|
||||||
perms.add(new PermEntry("放置方块", !land.denyBlockPlace));
|
perms.add(new PermEntry("放置方块", !land.denyBlockPlace));
|
||||||
perms.add(new PermEntry("破坏方块", !land.denyBlockBreak));
|
perms.add(new PermEntry("破坏方块", !land.denyBlockBreak));
|
||||||
|
perms.add(new PermEntry("实体交互(船/矿车/盔甲架/展示框)", !land.denyEntityInteract));
|
||||||
perms.add(new PermEntry("容器管理", !land.denyContainer));
|
perms.add(new PermEntry("容器管理", !land.denyContainer));
|
||||||
perms.add(new PermEntry("玩家对战", !land.denyPVP));
|
perms.add(new PermEntry("玩家对战", !land.denyPVP));
|
||||||
perms.add(new PermEntry("骑乘坐具", !land.denyMount));
|
perms.add(new PermEntry("骑乘坐具", !land.denyMount));
|
||||||
@@ -651,15 +681,17 @@ public class AreaGUIManager implements Listener {
|
|||||||
perms.add(new PermEntry("所有伤害", !land.denyAllDamage));
|
perms.add(new PermEntry("所有伤害", !land.denyAllDamage));
|
||||||
perms.add(new PermEntry("所有效果", !land.denyAllEffects));
|
perms.add(new PermEntry("所有效果", !land.denyAllEffects));
|
||||||
perms.add(new PermEntry("禁止展示框", !land.denyItemFrame));
|
perms.add(new PermEntry("禁止展示框", !land.denyItemFrame));
|
||||||
perms.add(new PermEntry("红石电路(中继器/比较器)", !land.denyRedstoneInteraction));
|
perms.add(new PermEntry("红石电路(按钮/压力板/中继器/比较器)", !land.denyRedstoneInteraction));
|
||||||
perms.add(new PermEntry("禁止门禁(门/按钮/压力板)", !land.denyDoorInteraction));
|
perms.add(new PermEntry("门禁交互(门/栅栏门)", !land.denyDoorInteraction));
|
||||||
perms.add(new PermEntry("音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
|
perms.add(new PermEntry("音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
|
||||||
perms.add(new PermEntry("拴绳使用", !land.denyLead));
|
perms.add(new PermEntry("拴绳使用", !land.denyLead));
|
||||||
perms.add(new PermEntry(land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
|
perms.add(new PermEntry(land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
|
||||||
perms.add(new PermEntry("剪切羊毛/生物", !land.denyWoolShear));
|
perms.add(new PermEntry("剪切羊毛/生物", !land.denyWoolShear));
|
||||||
perms.add(new PermEntry(land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
|
perms.add(new PermEntry(land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
|
||||||
perms.add(new PermEntry(land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
perms.add(new PermEntry(land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
||||||
|
perms.add(new PermEntry(land.denySignEdit ? "禁止编辑告示牌" : "允许编辑告示牌", !land.denySignEdit));
|
||||||
perms.add(new PermEntry("玩家发光", !land.denyGlowing));
|
perms.add(new PermEntry("玩家发光", !land.denyGlowing));
|
||||||
|
perms.add(new PermEntry("传送", land.allowVisitorTeleport));
|
||||||
perms.add(new PermEntry("和平模式", land.peaceMode));
|
perms.add(new PermEntry("和平模式", land.peaceMode));
|
||||||
perms.add(new PermEntry("公共建筑设施", land.isPublicBuilding));
|
perms.add(new PermEntry("公共建筑设施", land.isPublicBuilding));
|
||||||
return perms;
|
return perms;
|
||||||
@@ -686,6 +718,7 @@ public class AreaGUIManager implements Listener {
|
|||||||
case "移动": land.denyMove = !land.denyMove; break;
|
case "移动": land.denyMove = !land.denyMove; break;
|
||||||
case "放置方块": land.denyBlockPlace = !land.denyBlockPlace; break;
|
case "放置方块": land.denyBlockPlace = !land.denyBlockPlace; break;
|
||||||
case "破坏方块": land.denyBlockBreak = !land.denyBlockBreak; break;
|
case "破坏方块": land.denyBlockBreak = !land.denyBlockBreak; break;
|
||||||
|
case "实体交互(船/矿车/盔甲架/展示框)": land.denyEntityInteract = !land.denyEntityInteract; break;
|
||||||
case "容器管理": land.denyContainer = !land.denyContainer; break;
|
case "容器管理": land.denyContainer = !land.denyContainer; break;
|
||||||
case "玩家对战": land.denyPVP = !land.denyPVP; break;
|
case "玩家对战": land.denyPVP = !land.denyPVP; break;
|
||||||
case "骑乘坐具": land.denyMount = !land.denyMount; break;
|
case "骑乘坐具": land.denyMount = !land.denyMount; break;
|
||||||
@@ -707,8 +740,8 @@ public class AreaGUIManager implements Listener {
|
|||||||
case "所有伤害": land.denyAllDamage = !land.denyAllDamage; break;
|
case "所有伤害": land.denyAllDamage = !land.denyAllDamage; break;
|
||||||
case "所有效果": land.denyAllEffects = !land.denyAllEffects; break;
|
case "所有效果": land.denyAllEffects = !land.denyAllEffects; break;
|
||||||
case "禁止展示框": land.denyItemFrame = !land.denyItemFrame; break;
|
case "禁止展示框": land.denyItemFrame = !land.denyItemFrame; break;
|
||||||
case "红石电路(中继器/比较器)": land.denyRedstoneInteraction = !land.denyRedstoneInteraction; break;
|
case "红石电路(按钮/压力板/中继器/比较器)": land.denyRedstoneInteraction = !land.denyRedstoneInteraction; break;
|
||||||
case "禁止门禁(门/按钮/压力板)": land.denyDoorInteraction = !land.denyDoorInteraction; break;
|
case "门禁交互(门/栅栏门)": land.denyDoorInteraction = !land.denyDoorInteraction; break;
|
||||||
case "音频(音符盒/唱片机)": land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
|
case "音频(音符盒/唱片机)": land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
|
||||||
case "拴绳使用": land.denyLead = !land.denyLead; break;
|
case "拴绳使用": land.denyLead = !land.denyLead; break;
|
||||||
case "农作物收获": land.denyCropHarvest = !land.denyCropHarvest; break;
|
case "农作物收获": land.denyCropHarvest = !land.denyCropHarvest; break;
|
||||||
@@ -717,7 +750,9 @@ public class AreaGUIManager implements Listener {
|
|||||||
case "剪切羊毛/生物": land.denyWoolShear = !land.denyWoolShear; break;
|
case "剪切羊毛/生物": land.denyWoolShear = !land.denyWoolShear; break;
|
||||||
case "禁止投喂": case "允许投喂动物": land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
|
case "禁止投喂": case "允许投喂动物": land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
|
||||||
case "禁止攻击生物": case "允许攻击生物": land.denyMobAttack = !land.denyMobAttack; break;
|
case "禁止攻击生物": case "允许攻击生物": land.denyMobAttack = !land.denyMobAttack; break;
|
||||||
|
case "禁止编辑告示牌": case "允许编辑告示牌": land.denySignEdit = !land.denySignEdit; break;
|
||||||
case "玩家发光": land.denyGlowing = !land.denyGlowing; break;
|
case "玩家发光": land.denyGlowing = !land.denyGlowing; break;
|
||||||
|
case "传送": land.allowVisitorTeleport = !land.allowVisitorTeleport; break;
|
||||||
case "和平模式": land.peaceMode = !land.peaceMode; break;
|
case "和平模式": land.peaceMode = !land.peaceMode; break;
|
||||||
case "公共建筑设施": land.isPublicBuilding = !land.isPublicBuilding; break;
|
case "公共建筑设施": land.isPublicBuilding = !land.isPublicBuilding; break;
|
||||||
}
|
}
|
||||||
@@ -856,6 +891,10 @@ public class AreaGUIManager implements Listener {
|
|||||||
openPublicBuildingList(p);
|
openPublicBuildingList(p);
|
||||||
} else if (raw == 49) {
|
} else if (raw == 49) {
|
||||||
plugin.getGui().openMain(p);
|
plugin.getGui().openMain(p);
|
||||||
|
} else if (raw == 33) {
|
||||||
|
// ★ 回声碎片:发放领地记录查询工具
|
||||||
|
p.closeInventory();
|
||||||
|
plugin.landRecordManager.giveEchoShard(p);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -865,15 +904,24 @@ public class AreaGUIManager implements Listener {
|
|||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
if (raw < 0 || raw >= 54) return;
|
if (raw < 0 || raw >= 54) return;
|
||||||
|
|
||||||
// 获取当前页的领地
|
// ★ 获取可管理领地列表(和openLandList一致,包含管理员可见领地)
|
||||||
|
String playerName = p.getName();
|
||||||
|
List<AreaProtection.AreaConfig> allManageableLands = new ArrayList<>(areaProtect.getLandsByOwner(playerName));
|
||||||
|
if (areaProtect.isAreaAdmin(p)) {
|
||||||
|
for (AreaProtection.AreaConfig ac : areaProtect.getAllLands().values()) {
|
||||||
|
if (!allManageableLands.contains(ac)) {
|
||||||
|
allManageableLands.add(ac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int page = landListPages.getOrDefault(p.getUniqueId(), 0);
|
int page = landListPages.getOrDefault(p.getUniqueId(), 0);
|
||||||
List<AreaProtection.AreaConfig> lands = areaProtect.getLandsByOwner(p.getName());
|
|
||||||
int start = page * PAGE_SIZE;
|
int start = page * PAGE_SIZE;
|
||||||
int end = Math.min(start + PAGE_SIZE, lands.size());
|
int end = Math.min(start + PAGE_SIZE, allManageableLands.size());
|
||||||
|
|
||||||
if (raw >= 0 && raw < (end - start)) {
|
if (raw >= 0 && raw < (end - start)) {
|
||||||
// 点击领地
|
// 点击领地
|
||||||
AreaProtection.AreaConfig land = lands.get(start + raw);
|
AreaProtection.AreaConfig land = allManageableLands.get(start + raw);
|
||||||
if (event.isLeftClick() && !event.isShiftClick()) {
|
if (event.isLeftClick() && !event.isShiftClick()) {
|
||||||
openLandManage(p, land.name);
|
openLandManage(p, land.name);
|
||||||
} else if (event.isRightClick()) {
|
} else if (event.isRightClick()) {
|
||||||
@@ -900,7 +948,7 @@ public class AreaGUIManager implements Listener {
|
|||||||
}
|
}
|
||||||
} else if (raw == 45 && page > 0) {
|
} else if (raw == 45 && page > 0) {
|
||||||
openLandList(p, page - 1);
|
openLandList(p, page - 1);
|
||||||
} else if (raw == 53 && page < (int) Math.ceil((double) lands.size() / PAGE_SIZE) - 1) {
|
} else if (raw == 53 && page < (int) Math.ceil((double) allManageableLands.size() / PAGE_SIZE) - 1) {
|
||||||
openLandList(p, page + 1);
|
openLandList(p, page + 1);
|
||||||
} else if (raw == 48) {
|
} else if (raw == 48) {
|
||||||
openMainMenu(p);
|
openMainMenu(p);
|
||||||
@@ -925,6 +973,22 @@ public class AreaGUIManager implements Listener {
|
|||||||
} else if (raw == 32) {
|
} else if (raw == 32) {
|
||||||
// 过户领地
|
// 过户领地
|
||||||
openTransferLandPanel(p, landName);
|
openTransferLandPanel(p, landName);
|
||||||
|
} else if (raw == 33) {
|
||||||
|
// ★ 领地改名
|
||||||
|
p.closeInventory();
|
||||||
|
areaProtect.pendingEffectInput.put(p.getUniqueId(),
|
||||||
|
new String[]{landName, "rename_land"});
|
||||||
|
p.sendMessage(AreaCLIManager.header("领地改名: " + landName));
|
||||||
|
long[] cooldownInfo = areaProtect.getRenameCooldown(p);
|
||||||
|
if (cooldownInfo != null) {
|
||||||
|
long rm = cooldownInfo[0] / 60;
|
||||||
|
long rs = cooldownInfo[0] % 60;
|
||||||
|
p.sendMessage("§c冷却中!已用" + cooldownInfo[1] + "/5次," + rm + "分" + rs + "秒后解锁");
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5");
|
||||||
|
}
|
||||||
|
p.sendMessage("§e请在聊天栏输入新的领地名:");
|
||||||
|
p.sendMessage("§7输入 §c取消 §7放弃修改");
|
||||||
} else if (raw == 31) {
|
} else if (raw == 31) {
|
||||||
// 传送 - 优先传送点,否则安全搜索中心
|
// 传送 - 优先传送点,否则安全搜索中心
|
||||||
p.closeInventory();
|
p.closeInventory();
|
||||||
@@ -1078,16 +1142,18 @@ public class AreaGUIManager implements Listener {
|
|||||||
// 切换权限
|
// 切换权限
|
||||||
String[][] permDefs = {
|
String[][] permDefs = {
|
||||||
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
||||||
|
{"实体交互", "denyEntityInteract"},
|
||||||
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
||||||
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
||||||
{"药水效果", "denyPotion"}, {"点燃", "denyFire"}, {"火焰蔓延", "denyFireSpread"},
|
{"药水效果", "denyPotion"}, {"点燃", "denyFire"}, {"火焰蔓延", "denyFireSpread"},
|
||||||
{"允许拾取", "allowPickup"}, {"允许丢弃", "allowDrop"}, {"爆炸", "denyExplosion"},
|
{"允许拾取", "allowPickup"}, {"允许丢弃", "allowDrop"}, {"爆炸", "denyExplosion"},
|
||||||
{"摔落伤害", "denyFallDamage"}, {"饥饿", "denyHunger"}, {"所有伤害", "denyAllDamage"},
|
{"摔落伤害", "denyFallDamage"}, {"饥饿", "denyHunger"}, {"所有伤害", "denyAllDamage"},
|
||||||
{"所有效果", "denyAllEffects"}, {"禁止展示框", "denyItemFrame"}, {"红石电路", "denyRedstoneInteraction"},
|
{"所有效果", "denyAllEffects"}, {"禁止展示框", "denyItemFrame"}, {"红石电路", "denyRedstoneInteraction"},
|
||||||
{"禁止门禁", "denyDoorInteraction"}, {"音频", "denyNoteblockJukebox"}, {"拴绳使用", "denyLead"},
|
{"门禁交互", "denyDoorInteraction"}, {"音频", "denyNoteblockJukebox"}, {"拴绳使用", "denyLead"},
|
||||||
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
||||||
{"攻击生物", "denyMobAttack"},
|
{"攻击生物", "denyMobAttack"},
|
||||||
{"玩家发光", "denyGlowing"}, {"和平模式", "peaceMode"}
|
{"禁止编辑告示牌", "denySignEdit"},
|
||||||
|
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (raw < permDefs.length) {
|
if (raw < permDefs.length) {
|
||||||
@@ -1193,7 +1259,12 @@ public class AreaGUIManager implements Listener {
|
|||||||
// ★ GUI模式不发送聊天消息
|
// ★ GUI模式不发送聊天消息
|
||||||
openLandSettings(p, landName);
|
openLandSettings(p, landName);
|
||||||
} else if (raw == 15) {
|
} else if (raw == 15) {
|
||||||
// 切换强制游戏模式
|
// 切换强制游戏模式 —— 仅服务器管理员可操作
|
||||||
|
if (!areaProtect.isAreaAdmin(p)) {
|
||||||
|
p.sendMessage("§c仅服务器管理员可操作强制游戏模式");
|
||||||
|
openLandSettings(p, landName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (land.enforceGameMode == null || land.enforceGameMode.isEmpty()) {
|
if (land.enforceGameMode == null || land.enforceGameMode.isEmpty()) {
|
||||||
land.enforceGameMode = "SURVIVAL";
|
land.enforceGameMode = "SURVIVAL";
|
||||||
} else if ("SURVIVAL".equals(land.enforceGameMode)) {
|
} else if ("SURVIVAL".equals(land.enforceGameMode)) {
|
||||||
@@ -1209,6 +1280,32 @@ public class AreaGUIManager implements Listener {
|
|||||||
String modeStr = land.enforceGameMode != null ? land.enforceGameMode : "无";
|
String modeStr = land.enforceGameMode != null ? land.enforceGameMode : "无";
|
||||||
p.sendMessage("§a强制游戏模式: §f" + modeStr);
|
p.sendMessage("§a强制游戏模式: §f" + modeStr);
|
||||||
openLandSettings(p, landName);
|
openLandSettings(p, landName);
|
||||||
|
} else if (raw == 19) {
|
||||||
|
// ★ 编辑进入消息 → 关闭GUI,提示聊天输入
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§a请输入§f进入消息§a(支持HTML标签和颜色代码):");
|
||||||
|
p.sendMessage("§7当前: " + (land.enterMsg != null && !land.enterMsg.isEmpty() ? land.enterMsg : "(默认: 欢迎(玩家)来到(领地))"));
|
||||||
|
p.sendMessage("§7输入 §c清空 §7可恢复默认。§7输入 §c取消 §7放弃修改");
|
||||||
|
p.sendMessage("§7变量: §f(玩家) §7= 玩家名, §f(领地) §7= 领地名");
|
||||||
|
p.sendMessage("§7括号: §f() [] {} 【】 <> 《》");
|
||||||
|
p.sendMessage("§7标签: §f<b>加粗</b> §o<i>斜体</i> §n<u>下划线</u> §m<s>删除线</s>");
|
||||||
|
p.sendMessage("§7HTML: & < > " ' ");
|
||||||
|
if (plugin.areaCLIManager != null) {
|
||||||
|
plugin.areaCLIManager.setPendingAnnouncementInput(p.getUniqueId(), landName, "announcement_enter");
|
||||||
|
}
|
||||||
|
} else if (raw == 21) {
|
||||||
|
// ★ 编辑离开消息 → 关闭GUI,提示聊天输入
|
||||||
|
p.closeInventory();
|
||||||
|
p.sendMessage("§a请输入§f离开消息§a(支持HTML标签和颜色代码):");
|
||||||
|
p.sendMessage("§7当前: " + (land.leaveMsg != null && !land.leaveMsg.isEmpty() ? land.leaveMsg : "(默认: 感谢(玩家)光临(领地))"));
|
||||||
|
p.sendMessage("§7输入 §c清空 §7可恢复默认。§7输入 §c取消 §7或 §c0 §7放弃修改");
|
||||||
|
p.sendMessage("§7变量: §f(玩家) §7= 玩家名, §f(领地) §7= 领地名");
|
||||||
|
p.sendMessage("§7括号: §f() [] {} 【】 <> 《》");
|
||||||
|
p.sendMessage("§7标签: <b>加粗 <i>斜体 <u>下划线 <s>删除线");
|
||||||
|
p.sendMessage("§7HTML: & < > " ' ");
|
||||||
|
if (plugin.areaCLIManager != null) {
|
||||||
|
plugin.areaCLIManager.setPendingAnnouncementInput(p.getUniqueId(), landName, "announcement_leave");
|
||||||
|
}
|
||||||
} else if (raw == 31) {
|
} else if (raw == 31) {
|
||||||
// ★ 设置传送点
|
// ★ 设置传送点
|
||||||
p.closeInventory();
|
p.closeInventory();
|
||||||
@@ -1240,15 +1337,16 @@ public class AreaGUIManager implements Listener {
|
|||||||
String groupName = names.get(raw);
|
String groupName = names.get(raw);
|
||||||
if (p.hasPermission("sdf1.admin")) {
|
if (p.hasPermission("sdf1.admin")) {
|
||||||
ugm.deleteGroupConfig(groupName);
|
ugm.deleteGroupConfig(groupName);
|
||||||
// ★ 通知PHP同步
|
// ★ 通知PHP同步(从插件配置读取地址和密钥,不写死)
|
||||||
try {
|
try {
|
||||||
java.net.URL url = new java.net.URL("https://caoyuan.ypshidifu.cn/plugin/api/land_api.php");
|
String landApiUrl = plugin.webManager.getWebBaseUrl() + "/api/land_api.php";
|
||||||
|
java.net.URL url = new java.net.URL(landApiUrl);
|
||||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setConnectTimeout(5000);
|
conn.setConnectTimeout(5000);
|
||||||
conn.setReadTimeout(5000);
|
conn.setReadTimeout(5000);
|
||||||
String body = "action=delete_user_group&name=" + groupName + "&secret=sdf1_web_comm_2026_ypshidifu";
|
String body = "action=delete_user_group&name=" + groupName + "&secret=" + java.net.URLEncoder.encode(plugin.webManager.getSecretKey(), "UTF-8");
|
||||||
java.io.OutputStream os = conn.getOutputStream();
|
java.io.OutputStream os = conn.getOutputStream();
|
||||||
os.write(body.getBytes());
|
os.write(body.getBytes());
|
||||||
os.flush();
|
os.flush();
|
||||||
@@ -1476,6 +1574,28 @@ public class AreaGUIManager implements Listener {
|
|||||||
"",
|
"",
|
||||||
"§e点击切换"));
|
"§e点击切换"));
|
||||||
|
|
||||||
|
// 进入消息编辑(位置19)
|
||||||
|
String enterPreview = (land.enterMsg != null && !land.enterMsg.isEmpty()) ? land.enterMsg : "§7(默认: 欢迎(玩家)来到(领地))";
|
||||||
|
inv.setItem(19, createItem(Material.WRITTEN_BOOK, "§a§l编辑进入消息",
|
||||||
|
"§7当前: " + enterPreview,
|
||||||
|
"",
|
||||||
|
"§7变量: §f(玩家) §7= 玩家名, §f(领地) §7= 领地名",
|
||||||
|
"§7支持: §f() [] {} 【】 <> 《》",
|
||||||
|
"§7颜色: §f&a绿色 §6金色 §c红色 §7灰色",
|
||||||
|
"",
|
||||||
|
"§e点击编辑(聊天栏输入)"));
|
||||||
|
|
||||||
|
// 离开消息编辑(位置21)
|
||||||
|
String leavePreview = (land.leaveMsg != null && !land.leaveMsg.isEmpty()) ? land.leaveMsg : "§7(默认: 感谢(玩家)光临(领地))";
|
||||||
|
inv.setItem(21, createItem(Material.WRITTEN_BOOK, "§b§l编辑离开消息",
|
||||||
|
"§7当前: " + leavePreview,
|
||||||
|
"",
|
||||||
|
"§7变量: §f(玩家) §7= 玩家名, §f(领地) §7= 领地名",
|
||||||
|
"§7支持: §f() [] {} 【】 <> 《》",
|
||||||
|
"§7颜色: §f&a绿色 §6金色 §c红色 §7灰色",
|
||||||
|
"",
|
||||||
|
"§e点击编辑(聊天栏输入)"));
|
||||||
|
|
||||||
// 和平模式(位置13)
|
// 和平模式(位置13)
|
||||||
Material peaceMat = land.peaceMode ? Material.LIME_DYE : Material.GRAY_DYE;
|
Material peaceMat = land.peaceMode ? Material.LIME_DYE : Material.GRAY_DYE;
|
||||||
inv.setItem(13, createItem(peaceMat, "§e§l和平模式",
|
inv.setItem(13, createItem(peaceMat, "§e§l和平模式",
|
||||||
@@ -1484,12 +1604,20 @@ public class AreaGUIManager implements Listener {
|
|||||||
"",
|
"",
|
||||||
"§e点击切换"));
|
"§e点击切换"));
|
||||||
|
|
||||||
// 强制游戏模式(位置15)
|
// 强制游戏模式(位置15)—— 仅服务器管理员可操作
|
||||||
String modeStr = land.enforceGameMode != null ? land.enforceGameMode : "无";
|
boolean isAdmin = areaProtect.isAreaAdmin(p);
|
||||||
inv.setItem(15, createItem(Material.COMPASS, "§e§l强制游戏模式",
|
if (isAdmin) {
|
||||||
"§7当前模式: §f" + modeStr,
|
String modeStr = land.enforceGameMode != null ? land.enforceGameMode : "无";
|
||||||
"",
|
inv.setItem(15, createItem(Material.COMPASS, "§e§l强制游戏模式",
|
||||||
"§e点击切换: 生存→创造→冒险→旁观→无"));
|
"§7当前模式: §f" + modeStr,
|
||||||
|
"",
|
||||||
|
"§e点击切换: 生存→创造→冒险→旁观→无"));
|
||||||
|
} else {
|
||||||
|
inv.setItem(15, createItem(Material.BARRIER, "§7§l强制游戏模式",
|
||||||
|
"§7当前模式: §f" + (land.enforceGameMode != null ? land.enforceGameMode : "无"),
|
||||||
|
"",
|
||||||
|
"§c仅服务器管理员可操作"));
|
||||||
|
}
|
||||||
|
|
||||||
// ★ 设置传送点(位置31)
|
// ★ 设置传送点(位置31)
|
||||||
inv.setItem(31, createItem(Material.ENDER_PEARL, "§a§l设置传送点",
|
inv.setItem(31, createItem(Material.ENDER_PEARL, "§a§l设置传送点",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -779,12 +779,13 @@ public class BondManager {
|
|||||||
List<Map<String, Object>> transactions = new ArrayList<>();
|
List<Map<String, Object>> transactions = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = db.prepareStatement(
|
PreparedStatement ps = db.prepareStatement(
|
||||||
"SELECT player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
|
"SELECT id, player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
|
||||||
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
|
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
|
||||||
ps.setLong(1, afterTime);
|
ps.setLong(1, afterTime);
|
||||||
ResultSet rs = ps.executeQuery();
|
ResultSet rs = ps.executeQuery();
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
Map<String, Object> tx = new LinkedHashMap<>();
|
Map<String, Object> tx = new LinkedHashMap<>();
|
||||||
|
tx.put("id", rs.getInt("id"));
|
||||||
tx.put("player_name", rs.getString("player_name"));
|
tx.put("player_name", rs.getString("player_name"));
|
||||||
tx.put("type", rs.getString("type"));
|
tx.put("type", rs.getString("type"));
|
||||||
tx.put("amount", rs.getInt("amount"));
|
tx.put("amount", rs.getInt("amount"));
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ public class ChatFilterManager {
|
|||||||
private final Map<String, Long>
|
private final Map<String, Long>
|
||||||
mutedPlayers =
|
mutedPlayers =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
// ★ 禁言理由(内存镜像,持久化到 users.muted_until / mute_reason)
|
||||||
|
private final Map<String, String>
|
||||||
|
muteReasons =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
private final Map<String, String> messages =
|
private final Map<String, String> messages =
|
||||||
new LinkedHashMap<>();
|
new LinkedHashMap<>();
|
||||||
// ★ 广告机检测:记录每个玩家发送链接的频率
|
// ★ 广告机检测:记录每个玩家发送链接的频率
|
||||||
@@ -889,6 +893,8 @@ public class ChatFilterManager {
|
|||||||
if (System.currentTimeMillis()
|
if (System.currentTimeMillis()
|
||||||
>= expiry) {
|
>= expiry) {
|
||||||
mutedPlayers.remove(name);
|
mutedPlayers.remove(name);
|
||||||
|
muteReasons.remove(name);
|
||||||
|
clearMuteDb(name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -981,6 +987,84 @@ public class ChatFilterManager {
|
|||||||
|
|
||||||
public void unmutePlayer(String name) {
|
public void unmutePlayer(String name) {
|
||||||
mutedPlayers.remove(name);
|
mutedPlayers.remove(name);
|
||||||
|
muteReasons.remove(name);
|
||||||
|
clearMuteDb(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 禁言持久化(重启后仍生效) ==========
|
||||||
|
|
||||||
|
/** 禁言指定玩家,到期时间戳为 expireMs(绝对毫秒) */
|
||||||
|
public void mutePlayer(String name, long expireMs,
|
||||||
|
String reason) {
|
||||||
|
mutedPlayers.put(name, expireMs);
|
||||||
|
if (reason != null && !reason.isEmpty())
|
||||||
|
muteReasons.put(name, reason);
|
||||||
|
persistMute(name, expireMs, reason);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[聊天] 已禁言 " + name + " 至 "
|
||||||
|
+ new java.util.Date(expireMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistMute(String name, long expireMs,
|
||||||
|
String reason) {
|
||||||
|
try {
|
||||||
|
Main mainPlugin = (Main) plugin;
|
||||||
|
DatabaseManager db = mainPlugin.getDb();
|
||||||
|
if (db == null) return;
|
||||||
|
db.setField(name, "muted_until", expireMs);
|
||||||
|
db.setField(name, "mute_reason",
|
||||||
|
reason == null ? "" : reason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearMuteDb(String name) {
|
||||||
|
try {
|
||||||
|
Main mainPlugin = (Main) plugin;
|
||||||
|
DatabaseManager db = mainPlugin.getDb();
|
||||||
|
if (db == null) return;
|
||||||
|
db.setField(name, "muted_until", 0);
|
||||||
|
db.setField(name, "mute_reason", "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 插件启动时从数据库加载仍有效的禁言 */
|
||||||
|
public void loadMutes() {
|
||||||
|
try {
|
||||||
|
Main mainPlugin = (Main) plugin;
|
||||||
|
DatabaseManager db = mainPlugin.getDb();
|
||||||
|
if (db == null) return;
|
||||||
|
for (Object[] row : db.getActiveMutes()) {
|
||||||
|
String name = (String) row[0];
|
||||||
|
long expire = (Long) row[1];
|
||||||
|
String reason = (String) row[2];
|
||||||
|
mutedPlayers.put(name, expire);
|
||||||
|
if (reason != null
|
||||||
|
&& !reason.isEmpty())
|
||||||
|
muteReasons.put(name, reason);
|
||||||
|
}
|
||||||
|
if (!mutedPlayers.isEmpty())
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[聊天] 已加载 " + mutedPlayers.size()
|
||||||
|
+ " 条有效禁言");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMuteReason(String name) {
|
||||||
|
return muteReasons.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回当前仍有效的禁言到期时间戳(ms),已过期或未禁言返回 0 */
|
||||||
|
public long getMuteExpire(String name) {
|
||||||
|
Long expiry = mutedPlayers.get(name);
|
||||||
|
if (expiry == null) return 0;
|
||||||
|
if (System.currentTimeMillis() >= expiry) return 0;
|
||||||
|
return expiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetPlayer(String name) {
|
public void resetPlayer(String name) {
|
||||||
|
|||||||
@@ -160,10 +160,21 @@ public class ChatInputManager {
|
|||||||
reset(p);
|
reset(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String emailAddr = msg.trim();
|
||||||
|
// 邮箱后缀黑白名单校验
|
||||||
|
if (!config.isEmailSuffixAllowed(emailAddr)) {
|
||||||
|
String mode = config.getEmailValidationMode();
|
||||||
|
if ("白名单".equals(mode)) {
|
||||||
|
p.sendMessage("§c该邮箱后缀不在允许列表中");
|
||||||
|
} else if ("黑名单".equals(mode)) {
|
||||||
|
p.sendMessage("§c该邮箱后缀被禁止使用");
|
||||||
|
}
|
||||||
|
reset(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
String oldEmail = (String)
|
String oldEmail = (String)
|
||||||
db.getField(p.getName(),
|
db.getField(p.getName(),
|
||||||
"email");
|
"email");
|
||||||
String emailAddr = msg.trim();
|
|
||||||
// 已绑定邮箱:验证登录密码
|
// 已绑定邮箱:验证登录密码
|
||||||
if (oldEmail != null
|
if (oldEmail != null
|
||||||
&& !oldEmail.isEmpty()) {
|
&& !oldEmail.isEmpty()) {
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ package Sdf1_login;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
public class ConfigManager {
|
public class ConfigManager {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger("Sdf1_login");
|
||||||
private final File dataFolder;
|
private final File dataFolder;
|
||||||
private final Map<String, String> messages
|
private final Map<String, String> messages
|
||||||
= new LinkedHashMap<>();
|
= new LinkedHashMap<>();
|
||||||
@@ -45,6 +49,10 @@ public class ConfigManager {
|
|||||||
public double providerEconomyPerScore = 50.0;
|
public double providerEconomyPerScore = 50.0;
|
||||||
public int requesterPointsReward = 10;
|
public int requesterPointsReward = 10;
|
||||||
|
|
||||||
|
// ===== 密码错误封禁配置 =====
|
||||||
|
public int passwordFailThreshold = 5; // 密码错误阈值(达到后封禁)
|
||||||
|
public int passwordBanDuration = 300; // 封禁时长(秒),默认5分钟
|
||||||
|
|
||||||
public boolean garbageEnabled = true;
|
public boolean garbageEnabled = true;
|
||||||
public int garbageInterval = 300;
|
public int garbageInterval = 300;
|
||||||
public int garbageMaxRounds = 1;
|
public int garbageMaxRounds = 1;
|
||||||
@@ -59,6 +67,10 @@ public class ConfigManager {
|
|||||||
public int tpRequestValidSeconds = 90; // 请求有效时间(秒)
|
public int tpRequestValidSeconds = 90; // 请求有效时间(秒)
|
||||||
public int tpSendIntervalSeconds = 10; // 发送间隔(秒)
|
public int tpSendIntervalSeconds = 10; // 发送间隔(秒)
|
||||||
|
|
||||||
|
// ===== 商店打包配置(来自 PHP shop_config,定时拉取)=====
|
||||||
|
public double packingFee = 5.0; // 彩色潜影盒打包加收债券数(原色免费,支持小数)
|
||||||
|
public double greenDiscount = 10.0; // 环保单折扣率(折数):10=不打折,9.9=9.9折(支付99%),9.8=9.8折(支付98%),支持小数
|
||||||
|
|
||||||
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
|
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
|
||||||
public ConfigManager(File dataFolder) {
|
public ConfigManager(File dataFolder) {
|
||||||
this.dataFolder = dataFolder;
|
this.dataFolder = dataFolder;
|
||||||
@@ -113,8 +125,72 @@ public class ConfigManager {
|
|||||||
public void loadSmtp() {
|
public void loadSmtp() {
|
||||||
smtpSettings.clear();
|
smtpSettings.clear();
|
||||||
File file = new File(dataFolder, "SMTP设置.txt");
|
File file = new File(dataFolder, "SMTP设置.txt");
|
||||||
if (!file.exists()) createDefaultSmtp(file);
|
if (!file.exists()) {
|
||||||
|
createDefaultSmtp(file);
|
||||||
|
}
|
||||||
|
// 加载配置文件
|
||||||
loadMap(file, smtpSettings);
|
loadMap(file, smtpSettings);
|
||||||
|
// 检查并补全缺失的配置项
|
||||||
|
ensureSmtpConfigComplete(file);
|
||||||
|
logger.info("[Sdf1_login] 加载邮件配置: 已加载 " + smtpSettings.size() + " 个配置项");
|
||||||
|
for (String key : smtpSettings.keySet()) {
|
||||||
|
logger.info("[Sdf1_login] " + key + " = " + smtpSettings.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载邮件配置文件(热更新)
|
||||||
|
*/
|
||||||
|
public void reloadSmtp() {
|
||||||
|
loadSmtp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保SMTP配置文件包含所有必要的配置项
|
||||||
|
* 如果缺少配置项,自动补充默认值并保存
|
||||||
|
*/
|
||||||
|
private void ensureSmtpConfigComplete(File file) {
|
||||||
|
boolean changed = false;
|
||||||
|
|
||||||
|
// 检查必要的SMTP配置项
|
||||||
|
String[] requiredSmtpKeys = {"smtp地址", "smtp端口", "smtp账号", "smtp密码", "发件人名称", "smtp加密"};
|
||||||
|
for (String key : requiredSmtpKeys) {
|
||||||
|
if (!smtpSettings.containsKey(key)) {
|
||||||
|
logger.info("[Sdf1_login] 邮件配置缺失: " + key + ",自动补全");
|
||||||
|
// 根据key提供默认值
|
||||||
|
String defaultValue = "";
|
||||||
|
switch (key) {
|
||||||
|
case "smtp地址": defaultValue = "smtp.example.com"; break;
|
||||||
|
case "smtp端口": defaultValue = "465"; break;
|
||||||
|
case "发件人名称": defaultValue = "Sdf1_login"; break;
|
||||||
|
case "smtp加密": defaultValue = "true"; break;
|
||||||
|
}
|
||||||
|
smtpSettings.put(key, defaultValue);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱验证模式配置项
|
||||||
|
if (!smtpSettings.containsKey("邮箱验证模式")) {
|
||||||
|
logger.info("[Sdf1_login] 邮件配置缺失: 邮箱验证模式,自动补全");
|
||||||
|
smtpSettings.put("邮箱验证模式", "默认");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱后缀列表配置项
|
||||||
|
if (!smtpSettings.containsKey("邮箱后缀列表")) {
|
||||||
|
logger.info("[Sdf1_login] 邮件配置缺失: 邮箱后缀列表,自动补全");
|
||||||
|
smtpSettings.put("邮箱后缀列表", "");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有变更,保存配置文件
|
||||||
|
if (changed) {
|
||||||
|
logger.info("[Sdf1_login] 邮件配置有变更,保存配置文件");
|
||||||
|
saveSmtp();
|
||||||
|
} else {
|
||||||
|
logger.info("[Sdf1_login] 邮件配置完整,无需补全");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSmtp(String key) {
|
public String getSmtp(String key) {
|
||||||
@@ -125,26 +201,181 @@ public class ConfigManager {
|
|||||||
smtpSettings.put(key, value);
|
smtpSettings.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邮箱验证模式
|
||||||
|
* @return "默认" / "白名单" / "黑名单"
|
||||||
|
*/
|
||||||
|
public String getEmailValidationMode() {
|
||||||
|
return smtpSettings.getOrDefault("邮箱验证模式", "默认");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邮箱后缀列表
|
||||||
|
* @return 后缀列表(每行一个,不含@)
|
||||||
|
*/
|
||||||
|
public String getEmailSuffixList() {
|
||||||
|
return smtpSettings.getOrDefault("邮箱后缀列表", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查邮箱后缀是否允许
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @return true=允许, false=禁止
|
||||||
|
*/
|
||||||
|
public boolean isEmailSuffixAllowed(String email) {
|
||||||
|
String mode = getEmailValidationMode();
|
||||||
|
if ("默认".equals(mode)) {
|
||||||
|
return true; // 默认模式不校验
|
||||||
|
}
|
||||||
|
|
||||||
|
String suffixList = getEmailSuffixList();
|
||||||
|
if (suffixList == null || suffixList.trim().isEmpty()) {
|
||||||
|
return true; // 列表为空,不校验
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取邮箱后缀
|
||||||
|
String suffix = "";
|
||||||
|
int atIndex = email.indexOf('@');
|
||||||
|
if (atIndex >= 0 && atIndex < email.length() - 1) {
|
||||||
|
suffix = email.substring(atIndex + 1).toLowerCase().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析后缀列表
|
||||||
|
String[] suffixes = suffixList.split("[,;\\n]+");
|
||||||
|
boolean found = false;
|
||||||
|
for (String s : suffixes) {
|
||||||
|
String trimmed = s.trim().toLowerCase();
|
||||||
|
if (!trimmed.isEmpty() && trimmed.equals(suffix)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("白名单".equals(mode)) {
|
||||||
|
return found; // 白名单模式:必须在列表中
|
||||||
|
} else if ("黑名单".equals(mode)) {
|
||||||
|
return !found; // 黑名单模式:不能在列表中
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取允许的邮箱后缀提示信息
|
||||||
|
* @return 如 "允许的后缀: @qq.com, @163.com" 或空字符串
|
||||||
|
*/
|
||||||
|
public String getAllowedSuffixHint() {
|
||||||
|
String mode = getEmailValidationMode();
|
||||||
|
if ("默认".equals(mode)) return "";
|
||||||
|
String suffixList = getEmailSuffixList();
|
||||||
|
if (suffixList == null || suffixList.trim().isEmpty()) return "";
|
||||||
|
String[] suffixes = suffixList.split("[,;\\n]+");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String s : suffixes) {
|
||||||
|
String t = s.trim();
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
if (sb.length() > 0) sb.append(", ");
|
||||||
|
sb.append("@").append(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sb.length() == 0) return "";
|
||||||
|
if ("白名单".equals(mode)) {
|
||||||
|
return "§b§l允许的后缀: " + sb.toString();
|
||||||
|
} else if ("黑名单".equals(mode)) {
|
||||||
|
return "§c§l以下后缀被禁止: " + sb.toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
public void saveSmtp() {
|
public void saveSmtp() {
|
||||||
File file = new File(dataFolder, "SMTP设置.txt");
|
File file = new File(dataFolder, "SMTP设置.txt");
|
||||||
List<String> L = new ArrayList<>();
|
List<String> L = new ArrayList<>();
|
||||||
L.add("# ===== SMTP 配置 =====");
|
// 记录已写入的key,最后追加未写入的未知key
|
||||||
for (Map.Entry<String, String> e
|
java.util.Set<String> writtenKeys = new java.util.LinkedHashSet<>();
|
||||||
: smtpSettings.entrySet()) {
|
|
||||||
L.add(e.getKey() + "=" + e.getValue());
|
L.add("# ===== 邮件配置 =====");
|
||||||
|
L.add("# --- SMTP 信息 ---");
|
||||||
|
// 保存SMTP相关配置(按固定顺序)
|
||||||
|
String[] smtpKeys = {"smtp地址", "smtp端口", "smtp账号", "smtp密码", "发件人名称", "smtp加密"};
|
||||||
|
for (String key : smtpKeys) {
|
||||||
|
if (smtpSettings.containsKey(key)) {
|
||||||
|
L.add(key + "=" + smtpSettings.get(key));
|
||||||
|
writtenKeys.add(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
L.add("");
|
||||||
|
L.add("# --- 邮箱验证模式 ---");
|
||||||
|
L.add("# 模式: 默认 / 白名单 / 黑名单");
|
||||||
|
L.add("# 默认: 不校验邮箱后缀");
|
||||||
|
L.add("# 白名单: 只允许列表中的后缀");
|
||||||
|
L.add("# 黑名单: 禁止列表中的后缀");
|
||||||
|
// 保存邮箱验证模式
|
||||||
|
if (smtpSettings.containsKey("邮箱验证模式")) {
|
||||||
|
L.add("邮箱验证模式=" + smtpSettings.get("邮箱验证模式"));
|
||||||
|
} else {
|
||||||
|
L.add("邮箱验证模式=默认");
|
||||||
|
}
|
||||||
|
writtenKeys.add("邮箱验证模式");
|
||||||
|
// 保存邮箱后缀列表(支持多行格式:每行一个后缀)
|
||||||
|
L.add("# 邮箱后缀列表(每行一个,不含@)");
|
||||||
|
String suffixVal = smtpSettings.getOrDefault("邮箱后缀列表", "");
|
||||||
|
if (suffixVal != null && !suffixVal.trim().isEmpty()) {
|
||||||
|
String[] parts = suffixVal.split("[,\\n]+");
|
||||||
|
boolean firstSuffix = true;
|
||||||
|
for (String s : parts) {
|
||||||
|
String trimmed = s.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
if (firstSuffix) {
|
||||||
|
L.add("邮箱后缀列表=" + trimmed);
|
||||||
|
firstSuffix = false;
|
||||||
|
} else {
|
||||||
|
L.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstSuffix) {
|
||||||
|
L.add("邮箱后缀列表=");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
L.add("邮箱后缀列表=");
|
||||||
|
}
|
||||||
|
writtenKeys.add("邮箱后缀列表");
|
||||||
|
|
||||||
|
// ★ 追加所有未写入的未知key(防止丢失用户自定义配置项)
|
||||||
|
java.util.Set<String> allKeys = new java.util.LinkedHashSet<>(smtpSettings.keySet());
|
||||||
|
allKeys.removeAll(writtenKeys);
|
||||||
|
if (!allKeys.isEmpty()) {
|
||||||
|
L.add("");
|
||||||
|
L.add("# --- 其他配置 ---");
|
||||||
|
for (String key : allKeys) {
|
||||||
|
L.add(key + "=" + smtpSettings.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeLines(file, L);
|
writeLines(file, L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createDefaultSmtp(File f) {
|
private void createDefaultSmtp(File f) {
|
||||||
List<String> L = new ArrayList<>();
|
List<String> L = new ArrayList<>();
|
||||||
L.add("# ===== SMTP 配置 =====");
|
L.add("# ===== 邮件配置 =====");
|
||||||
|
L.add("# --- SMTP 信息 ---");
|
||||||
L.add("smtp地址=smtp.example.com");
|
L.add("smtp地址=smtp.example.com");
|
||||||
L.add("smtp端口=465");
|
L.add("smtp端口=465");
|
||||||
L.add("smtp账号=");
|
L.add("smtp账号=");
|
||||||
L.add("smtp密码=");
|
L.add("smtp密码=");
|
||||||
L.add("发件人名称=Sdf1_login");
|
L.add("发件人名称=Sdf1_login");
|
||||||
L.add("smtp加密=true");
|
L.add("smtp加密=true");
|
||||||
|
L.add("");
|
||||||
|
L.add("# --- 邮箱验证模式 ---");
|
||||||
|
L.add("# 模式: 默认 / 白名单 / 黑名单");
|
||||||
|
L.add("# 默认: 不校验邮箱后缀");
|
||||||
|
L.add("# 白名单: 只允许列表中的后缀");
|
||||||
|
L.add("# 黑名单: 禁止列表中的后缀");
|
||||||
|
L.add("邮箱验证模式=默认");
|
||||||
|
L.add("# 邮箱后缀列表(每行一个,不含@)");
|
||||||
|
L.add("# 示例: qq.com");
|
||||||
|
L.add("# 示例: gmail.com");
|
||||||
|
L.add("邮箱后缀列表=");
|
||||||
writeLines(f, L);
|
writeLines(f, L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +399,8 @@ public class ConfigManager {
|
|||||||
{"区域防护_最大领地数", String.valueOf(areaProtectMaxLands)},
|
{"区域防护_最大领地数", String.valueOf(areaProtectMaxLands)},
|
||||||
{"传送_请求有效秒", String.valueOf(tpRequestValidSeconds)},
|
{"传送_请求有效秒", String.valueOf(tpRequestValidSeconds)},
|
||||||
{"传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds)},
|
{"传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds)},
|
||||||
|
{"密码错误封禁阈值", String.valueOf(passwordFailThreshold)},
|
||||||
|
{"密码错误封禁时长秒", String.valueOf(passwordBanDuration)},
|
||||||
};
|
};
|
||||||
for (String[] nk : newKeys) {
|
for (String[] nk : newKeys) {
|
||||||
if (!m.containsKey(nk[0])) { hasNewKey = true; }
|
if (!m.containsKey(nk[0])) { hasNewKey = true; }
|
||||||
@@ -186,7 +419,7 @@ public class ConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bw.flush(); bw.close();
|
bw.flush(); bw.close();
|
||||||
System.out.println("[Config] 已补充缺失的配置项");
|
Bukkit.getLogger().info("[Config] 已补充缺失的配置项");
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +441,10 @@ public class ConfigManager {
|
|||||||
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
|
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
|
||||||
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
|
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
|
||||||
|
|
||||||
|
// 密码错误封禁配置
|
||||||
|
passwordFailThreshold = parseInt(m.getOrDefault("密码错误封禁阈值", "5"), 5);
|
||||||
|
passwordBanDuration = parseInt(m.getOrDefault("密码错误封禁时长秒", "300"), 300);
|
||||||
|
|
||||||
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
|
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
|
||||||
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
|
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
|
||||||
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
|
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
|
||||||
@@ -235,7 +472,16 @@ public class ConfigManager {
|
|||||||
areaProtectMaxLands = parseInt(m.getOrDefault("区域防护_最大领地数", "3"), 3);
|
areaProtectMaxLands = parseInt(m.getOrDefault("区域防护_最大领地数", "3"), 3);
|
||||||
|
|
||||||
tpRequestValidSeconds = parseIntFromString(m.getOrDefault("传送_请求有效秒", "90"));
|
tpRequestValidSeconds = parseIntFromString(m.getOrDefault("传送_请求有效秒", "90"));
|
||||||
|
// 安全下限:防止配置被误设为极小值(如把"3"当"3分钟")导致传送请求秒过期。
|
||||||
|
// 若发现配置过小,强制下限并告警,便于定位插件设置.txt 中的异常值。
|
||||||
|
if (tpRequestValidSeconds < 30) {
|
||||||
|
Bukkit.getLogger().warning("[Sdf1配置] 警告: 传送_请求有效秒=" + tpRequestValidSeconds
|
||||||
|
+ " 过小,已强制下限为 30 秒(请检查插件设置.txt 是否误设为极小值)");
|
||||||
|
tpRequestValidSeconds = 30;
|
||||||
|
}
|
||||||
tpSendIntervalSeconds = parseIntFromString(m.getOrDefault("传送_发送间隔秒", "10"));
|
tpSendIntervalSeconds = parseIntFromString(m.getOrDefault("传送_发送间隔秒", "10"));
|
||||||
|
Bukkit.getLogger().info("[Sdf1配置] 传送请求有效时间 = " + tpRequestValidSeconds + " 秒;发送间隔 = "
|
||||||
|
+ tpSendIntervalSeconds + " 秒");
|
||||||
|
|
||||||
// rewardChannel 已标准化为 bonds/economy,默认bonds
|
// rewardChannel 已标准化为 bonds/economy,默认bonds
|
||||||
}
|
}
|
||||||
@@ -306,6 +552,8 @@ public class ConfigManager {
|
|||||||
managed.put("奖励发放方式", "债券");
|
managed.put("奖励发放方式", "债券");
|
||||||
managed.put("传送_请求有效秒", String.valueOf(tpRequestValidSeconds));
|
managed.put("传送_请求有效秒", String.valueOf(tpRequestValidSeconds));
|
||||||
managed.put("传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds));
|
managed.put("传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds));
|
||||||
|
managed.put("密码错误封禁阈值", String.valueOf(passwordFailThreshold));
|
||||||
|
managed.put("密码错误封禁时长秒", String.valueOf(passwordBanDuration));
|
||||||
|
|
||||||
// ★ 保留非ConfigManager管理的配置行(如web通信-*等)
|
// ★ 保留非ConfigManager管理的配置行(如web通信-*等)
|
||||||
// ★ 同时保留连续的注释块(如 "# ===== Web通信配置 =====")
|
// ★ 同时保留连续的注释块(如 "# ===== Web通信配置 =====")
|
||||||
@@ -396,6 +644,9 @@ public class ConfigManager {
|
|||||||
L.add("区域防护_最大领地数=3");
|
L.add("区域防护_最大领地数=3");
|
||||||
L.add("传送_请求有效秒=90");
|
L.add("传送_请求有效秒=90");
|
||||||
L.add("传送_发送间隔秒=10");
|
L.add("传送_发送间隔秒=10");
|
||||||
|
// 密码错误封禁
|
||||||
|
L.add("密码错误封禁阈值=5");
|
||||||
|
L.add("密码错误封禁时长秒=300");
|
||||||
// Web通信配置(默认禁用,需手动开启)
|
// Web通信配置(默认禁用,需手动开启)
|
||||||
L.add("");
|
L.add("");
|
||||||
L.add("# ===== Web通信配置 =====");
|
L.add("# ===== Web通信配置 =====");
|
||||||
@@ -463,11 +714,36 @@ public class ConfigManager {
|
|||||||
private void loadMap(File file, Map<String, String> map) {
|
private void loadMap(File file, Map<String, String> map) {
|
||||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
String line;
|
String line;
|
||||||
|
boolean firstLine = true;
|
||||||
|
String lastKey = null; // 跟踪上一个key,用于续行
|
||||||
while ((line = r.readLine()) != null) {
|
while ((line = r.readLine()) != null) {
|
||||||
|
// 处理UTF-8 BOM头
|
||||||
|
if (firstLine) {
|
||||||
|
firstLine = false;
|
||||||
|
if (line.startsWith("\uFEFF")) {
|
||||||
|
line = line.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
if (line.isEmpty() || line.startsWith("#")) {
|
||||||
|
lastKey = null; // 空行/注释行中断续行
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int eq = line.indexOf('=');
|
int eq = line.indexOf('=');
|
||||||
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
|
if (eq > 0) {
|
||||||
|
// key=value 标准行
|
||||||
|
String key = line.substring(0, eq).trim();
|
||||||
|
String val = line.substring(eq + 1).trim();
|
||||||
|
map.put(key, val);
|
||||||
|
lastKey = key; // 记录key,后续裸行可续行
|
||||||
|
} else if (lastKey != null && !map.getOrDefault(lastKey, "").isEmpty()) {
|
||||||
|
// 裸行 + 上一个key有值 → 续行,用\n连接
|
||||||
|
String existing = map.get(lastKey);
|
||||||
|
map.put(lastKey, existing + "\n" + line);
|
||||||
|
} else if (lastKey != null) {
|
||||||
|
// 裸行 + 上一个key值为空 → 直接赋值
|
||||||
|
map.put(lastKey, line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
@@ -480,13 +756,19 @@ public class ConfigManager {
|
|||||||
w.flush(); // 强制刷盘
|
w.flush(); // 强制刷盘
|
||||||
w.close(); // 确保关闭
|
w.close(); // 确保关闭
|
||||||
// ★ 原子替换:将.tmp文件重命名为目标文件
|
// ★ 原子替换:将.tmp文件重命名为目标文件
|
||||||
// 不能反过来(file→tmp),否则Linux上file.renameTo(tmp)会成功覆盖.tmp→丢失新内容+原文件被删
|
|
||||||
if (!tmp.renameTo(file)) {
|
if (!tmp.renameTo(file)) {
|
||||||
// 替换失败,删除临时文件
|
// renameTo在Windows上可能失败(文件锁定等),fallback到copy+delete
|
||||||
tmp.delete();
|
try {
|
||||||
|
java.nio.file.Files.copy(tmp.toPath(), file.toPath(),
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
tmp.delete();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warning("[Sdf1_login] 配置文件保存失败: " + e.getMessage());
|
||||||
|
tmp.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ignored) {
|
} catch (IOException e) {
|
||||||
// 写入失败时删除临时文件,保留原文件不变
|
logger.warning("[Sdf1_login] 配置文件写入失败: " + e.getMessage());
|
||||||
tmp.delete();
|
tmp.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,25 @@ public class DatabaseManager {
|
|||||||
// UI偏好:1=CLI(默认), 0=GUI
|
// UI偏好:1=CLI(默认), 0=GUI
|
||||||
safeAdd(st, "ui_mode",
|
safeAdd(st, "ui_mode",
|
||||||
"INTEGER DEFAULT 1");
|
"INTEGER DEFAULT 1");
|
||||||
|
// ===== 风控 / 封禁相关列 =====
|
||||||
|
// IP归属(省+市+区),用于异地登录检测
|
||||||
|
safeAdd(st, "last_login_location",
|
||||||
|
"TEXT DEFAULT ''");
|
||||||
|
// 账号冻结(玩家主动冻结防被盗):0=正常 1=已冻结
|
||||||
|
safeAdd(st, "frozen",
|
||||||
|
"INTEGER DEFAULT 0");
|
||||||
|
// 冻结校验 token(邮件链接防猜测)
|
||||||
|
safeAdd(st, "freeze_token",
|
||||||
|
"TEXT DEFAULT ''");
|
||||||
|
// 禁言到期时间戳(毫秒),0=未禁言
|
||||||
|
safeAdd(st, "muted_until",
|
||||||
|
"INTEGER DEFAULT 0");
|
||||||
|
// 禁言理由
|
||||||
|
safeAdd(st, "mute_reason",
|
||||||
|
"TEXT DEFAULT ''");
|
||||||
|
// ★ 正版OAuth登录IP(用于异地登录风控)
|
||||||
|
safeAdd(st, "last_oauth_ip",
|
||||||
|
"TEXT DEFAULT ''");
|
||||||
|
|
||||||
// ===== 背包备份表:安全迁移 =====
|
// ===== 背包备份表:安全迁移 =====
|
||||||
// 检查旧表是否存在(用 inventory_data 列判断)
|
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||||
@@ -161,6 +180,19 @@ public class DatabaseManager {
|
|||||||
+ ")");
|
+ ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PVP 背包备份表(独立表:避免与登录背包系统的 inventory_backups 互相覆盖导致原始背包丢失)
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "pvp_inventory_backup ("
|
||||||
|
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
+ "player_name TEXT NOT NULL,"
|
||||||
|
+ "contents_b64 TEXT DEFAULT '',"
|
||||||
|
+ "armor_b64 TEXT DEFAULT '',"
|
||||||
|
+ "extra_b64 TEXT DEFAULT '',"
|
||||||
|
+ "level INTEGER DEFAULT 0,"
|
||||||
|
+ "experience REAL DEFAULT 0,"
|
||||||
|
+ "save_time INTEGER DEFAULT 0"
|
||||||
|
+ ")");
|
||||||
|
|
||||||
// 安全报警表
|
// 安全报警表
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
+ "security_alerts ("
|
+ "security_alerts ("
|
||||||
@@ -275,13 +307,60 @@ public class DatabaseManager {
|
|||||||
safeAdd(st, "menu_snowball",
|
safeAdd(st, "menu_snowball",
|
||||||
"INTEGER DEFAULT 1");
|
"INTEGER DEFAULT 1");
|
||||||
|
|
||||||
// 传送请求表
|
// 领地方块记录表:记录领地内谁破坏/放置了什么方块(7天滚动)
|
||||||
st.execute("CREATE TABLE IF NOT EXISTS teleport_requests ("
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
+ "sender TEXT NOT NULL,"
|
+ "land_block_log ("
|
||||||
+ "receiver TEXT NOT NULL,"
|
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
+ "type TEXT NOT NULL DEFAULT 'tpa',"
|
+ "land_name TEXT,"
|
||||||
+ "timestamp INTEGER NOT NULL DEFAULT 0"
|
+ "land_id INTEGER,"
|
||||||
|
+ "world TEXT,"
|
||||||
|
+ "x INTEGER,"
|
||||||
|
+ "y INTEGER,"
|
||||||
|
+ "z INTEGER,"
|
||||||
|
+ "player_name TEXT,"
|
||||||
|
+ "action TEXT,"
|
||||||
|
+ "material TEXT,"
|
||||||
|
+ "detail TEXT,"
|
||||||
|
+ "time INTEGER"
|
||||||
+ ")");
|
+ ")");
|
||||||
|
|
||||||
|
// ★ 新增detail列(已有数据库兼容)
|
||||||
|
try {
|
||||||
|
st.executeUpdate("ALTER TABLE land_block_log ADD COLUMN detail TEXT");
|
||||||
|
} catch (SQLException ignored) {}
|
||||||
|
|
||||||
|
// 领地容器记录表:记录领地内容器的打开/拿取(7天滚动)
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "land_container_log ("
|
||||||
|
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
+ "land_name TEXT,"
|
||||||
|
+ "land_id INTEGER,"
|
||||||
|
+ "world TEXT,"
|
||||||
|
+ "x INTEGER,"
|
||||||
|
+ "y INTEGER,"
|
||||||
|
+ "z INTEGER,"
|
||||||
|
+ "player_name TEXT,"
|
||||||
|
+ "action TEXT,"
|
||||||
|
+ "container_type TEXT,"
|
||||||
|
+ "detail TEXT,"
|
||||||
|
+ "amount INTEGER,"
|
||||||
|
+ "time INTEGER"
|
||||||
|
+ ")");
|
||||||
|
|
||||||
|
// 背包自动滚动备份表(15分钟定时):每位玩家保留最近 3 份
|
||||||
|
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ "inv_backup_auto ("
|
||||||
|
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
+ "player_name TEXT NOT NULL,"
|
||||||
|
+ "contents_b64 TEXT DEFAULT '',"
|
||||||
|
+ "armor_b64 TEXT DEFAULT '',"
|
||||||
|
+ "extra_b64 TEXT DEFAULT '',"
|
||||||
|
+ "level INTEGER DEFAULT 0,"
|
||||||
|
+ "experience REAL DEFAULT 0,"
|
||||||
|
+ "save_time INTEGER DEFAULT 0"
|
||||||
|
+ ")");
|
||||||
|
|
||||||
|
// 注:传送请求不再落库,仅驻留内存(配置项才落库),故不再建 teleport_requests 表
|
||||||
st.close();
|
st.close();
|
||||||
logger.info("[Sdf1_login] 数据库初始化完成");
|
logger.info("[Sdf1_login] 数据库初始化完成");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -677,6 +756,36 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 读取当前仍有效的禁言列表(muted_until > now) */
|
||||||
|
public List<Object[]> getActiveMutes() {
|
||||||
|
List<Object[]> list =
|
||||||
|
new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT player_name, muted_until, "
|
||||||
|
+ "mute_reason FROM users "
|
||||||
|
+ "WHERE muted_until > ?");
|
||||||
|
ps.setLong(1,
|
||||||
|
System.currentTimeMillis());
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Object[] row = new Object[3];
|
||||||
|
row[0] = rs.getString(
|
||||||
|
"player_name");
|
||||||
|
row[1] = rs.getLong(
|
||||||
|
"muted_until");
|
||||||
|
row[2] = rs.getString(
|
||||||
|
"mute_reason");
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public Object getField(String name,
|
public Object getField(String name,
|
||||||
String field) {
|
String field) {
|
||||||
try {
|
try {
|
||||||
@@ -861,19 +970,17 @@ public class DatabaseManager {
|
|||||||
|
|
||||||
// ==================== 背包备份 ====================
|
// ==================== 背包备份 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存背包主备份 —— 追加写入,每位玩家仅保留最近 3 份(更早的自动清理)。
|
||||||
|
* ★ 2026-07-09 改版:原为「DELETE 全部再 INSERT 单份」,会导致 PVP 竞技场/测试场
|
||||||
|
* 掉线时用当前(竞技场装备)背包覆盖玩家真实背包。改为滚动保留 3 份后,
|
||||||
|
* 即便发生误覆盖,玩家仍可从「最近 3 份」里挑出真实背包还原。
|
||||||
|
*/
|
||||||
public void saveInventoryBackup(String name,
|
public void saveInventoryBackup(String name,
|
||||||
String contents, String armor,
|
String contents, String armor,
|
||||||
String extra, int level,
|
String extra, int level,
|
||||||
double experience) {
|
double experience) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement del =
|
|
||||||
db.prepareStatement(
|
|
||||||
"DELETE FROM "
|
|
||||||
+ "inventory_backups "
|
|
||||||
+ "WHERE player_name=?");
|
|
||||||
del.setString(1, name);
|
|
||||||
del.executeUpdate();
|
|
||||||
del.close();
|
|
||||||
PreparedStatement ps =
|
PreparedStatement ps =
|
||||||
db.prepareStatement(
|
db.prepareStatement(
|
||||||
"INSERT INTO "
|
"INSERT INTO "
|
||||||
@@ -897,6 +1004,43 @@ public class DatabaseManager {
|
|||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
|
||||||
|
// 仅保留最近 3 份,删除该玩家更早的备份
|
||||||
|
// ★ 2026-07-10:改为先查后删,避免某些 SQLite 版本/驱动把带 LIMIT 的子查询优化掉,
|
||||||
|
// 导致只保留最新一份的问题。
|
||||||
|
try {
|
||||||
|
PreparedStatement sel =
|
||||||
|
db.prepareStatement(
|
||||||
|
"SELECT id FROM inventory_backups "
|
||||||
|
+ "WHERE player_name=? "
|
||||||
|
+ "ORDER BY save_time DESC LIMIT 3");
|
||||||
|
sel.setString(1, name);
|
||||||
|
ResultSet rs = sel.executeQuery();
|
||||||
|
java.util.List<Integer> keep = new java.util.ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
keep.add(rs.getInt("id"));
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
sel.close();
|
||||||
|
|
||||||
|
if (keep.size() == 3) {
|
||||||
|
// 保留 3 份,删除其余
|
||||||
|
StringBuilder in = new StringBuilder();
|
||||||
|
for (int i = 0; i < keep.size(); i++) {
|
||||||
|
if (i > 0) in.append(",");
|
||||||
|
in.append(keep.get(i));
|
||||||
|
}
|
||||||
|
PreparedStatement del = db.prepareStatement(
|
||||||
|
"DELETE FROM inventory_backups "
|
||||||
|
+ "WHERE player_name=? AND id NOT IN ("
|
||||||
|
+ in.toString() + ")");
|
||||||
|
del.setString(1, name);
|
||||||
|
del.executeUpdate();
|
||||||
|
del.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -959,6 +1103,116 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== PVP 背包备份(独立表,避免与登录背包系统互相覆盖)====================
|
||||||
|
// 登录背包系统使用 inventory_backups 表;PVP 备份若共用同一表/同一玩家名键,
|
||||||
|
// 会在换装、断线重连等时机互相覆盖,导致玩家"原始背包丢失"。故 PVP 必须独立存储。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 PVP 背包备份(非覆盖)。
|
||||||
|
* ★ 关键保护:只要该玩家已存在 PVP 背包备份(即尚未真正退场),绝不再次写入,
|
||||||
|
* 确保入场前的原始背包永远不被后续状态(换装/断线重连)覆盖。
|
||||||
|
*/
|
||||||
|
public void savePvpInventoryBackup(String name,
|
||||||
|
String contents, String armor,
|
||||||
|
String extra, int level,
|
||||||
|
double experience) {
|
||||||
|
try {
|
||||||
|
if (hasPvpInventoryBackup(name)) {
|
||||||
|
return; // 已有备份,严禁覆盖
|
||||||
|
}
|
||||||
|
PreparedStatement ps =
|
||||||
|
db.prepareStatement(
|
||||||
|
"INSERT INTO "
|
||||||
|
+ "pvp_inventory_backup"
|
||||||
|
+ " (player_name, "
|
||||||
|
+ "contents_b64, "
|
||||||
|
+ "armor_b64, "
|
||||||
|
+ "extra_b64, "
|
||||||
|
+ "level, "
|
||||||
|
+ "experience, "
|
||||||
|
+ "save_time) "
|
||||||
|
+ "VALUES "
|
||||||
|
+ "(?,?,?,?,?,?,?)");
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.setString(2, contents);
|
||||||
|
ps.setString(3, armor);
|
||||||
|
ps.setString(4, extra);
|
||||||
|
ps.setInt(5, level);
|
||||||
|
ps.setDouble(6, experience);
|
||||||
|
ps.setLong(7,
|
||||||
|
System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getPvpInventoryBackup(String playerName) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
db.prepareStatement(
|
||||||
|
"SELECT contents_b64, "
|
||||||
|
+ "armor_b64, "
|
||||||
|
+ "extra_b64, "
|
||||||
|
+ "level, "
|
||||||
|
+ "experience "
|
||||||
|
+ "FROM "
|
||||||
|
+ "pvp_inventory_backup "
|
||||||
|
+ "WHERE player_name=? "
|
||||||
|
+ "ORDER BY save_time DESC "
|
||||||
|
+ "LIMIT 1");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
String[] result = new String[5];
|
||||||
|
result[0] = rs.getString("contents_b64");
|
||||||
|
result[1] = rs.getString("armor_b64");
|
||||||
|
result[2] = rs.getString("extra_b64");
|
||||||
|
result[3] = String.valueOf(rs.getInt("level"));
|
||||||
|
result[4] = String.valueOf(rs.getDouble("experience"));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePvpInventoryBackup(String name) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"DELETE FROM pvp_inventory_backup "
|
||||||
|
+ "WHERE player_name=?");
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPvpInventoryBackup(String name) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT 1 FROM pvp_inventory_backup "
|
||||||
|
+ "WHERE player_name=? LIMIT 1");
|
||||||
|
ps.setString(1, name);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
boolean has = rs.next();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return has;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasInventoryBackup(
|
public boolean hasInventoryBackup(
|
||||||
String name) {
|
String name) {
|
||||||
try {
|
try {
|
||||||
@@ -1316,7 +1570,7 @@ public class DatabaseManager {
|
|||||||
PreparedStatement ps =
|
PreparedStatement ps =
|
||||||
db.prepareStatement(
|
db.prepareStatement(
|
||||||
"SELECT id, save_time "
|
"SELECT id, save_time "
|
||||||
+ "FROM inventory_backups "
|
+ "FROM inv_backup_auto "
|
||||||
+ "WHERE player_name=? "
|
+ "WHERE player_name=? "
|
||||||
+ "ORDER BY save_time DESC "
|
+ "ORDER BY save_time DESC "
|
||||||
+ "LIMIT ?");
|
+ "LIMIT ?");
|
||||||
@@ -1341,6 +1595,62 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家登录备份列表(inventory_backups)
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> getLoginBackups(
|
||||||
|
String name, int limit) {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT id, save_time FROM inventory_backups "
|
||||||
|
+ "WHERE player_name=? ORDER BY save_time DESC LIMIT ?");
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.setInt(2, limit);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("id", rs.getInt("id"));
|
||||||
|
row.put("save_time", rs.getLong("save_time"));
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定ID的登录备份数据
|
||||||
|
*/
|
||||||
|
public String[] getLoginBackupById(int backupId) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT contents_b64, armor_b64, extra_b64, level, experience "
|
||||||
|
+ "FROM inventory_backups WHERE id=?");
|
||||||
|
ps.setInt(1, backupId);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
String[] result = new String[5];
|
||||||
|
result[0] = rs.getString("contents_b64");
|
||||||
|
result[1] = rs.getString("armor_b64");
|
||||||
|
result[2] = rs.getString("extra_b64");
|
||||||
|
result[3] = String.valueOf(rs.getInt("level"));
|
||||||
|
result[4] = String.valueOf(rs.getDouble("experience"));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定ID的备份数据
|
* 获取指定ID的备份数据
|
||||||
*/
|
*/
|
||||||
@@ -1354,7 +1664,7 @@ public class DatabaseManager {
|
|||||||
+ "extra_b64, "
|
+ "extra_b64, "
|
||||||
+ "level, "
|
+ "level, "
|
||||||
+ "experience "
|
+ "experience "
|
||||||
+ "FROM inventory_backups "
|
+ "FROM inv_backup_auto "
|
||||||
+ "WHERE id=?");
|
+ "WHERE id=?");
|
||||||
ps.setInt(1, backupId);
|
ps.setInt(1, backupId);
|
||||||
ResultSet rs = ps.executeQuery();
|
ResultSet rs = ps.executeQuery();
|
||||||
@@ -1384,6 +1694,44 @@ public class DatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家最新背包备份
|
||||||
|
*/
|
||||||
|
public String[] getInventoryBackup(String playerName) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps =
|
||||||
|
db.prepareStatement(
|
||||||
|
"SELECT contents_b64, "
|
||||||
|
+ "armor_b64, "
|
||||||
|
+ "extra_b64, "
|
||||||
|
+ "level, "
|
||||||
|
+ "experience "
|
||||||
|
+ "FROM inventory_backups "
|
||||||
|
+ "WHERE player_name=? "
|
||||||
|
+ "ORDER BY save_time DESC "
|
||||||
|
+ "LIMIT 1");
|
||||||
|
ps.setString(1, playerName);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
String[] result = new String[5];
|
||||||
|
result[0] = rs.getString("contents_b64");
|
||||||
|
result[1] = rs.getString("armor_b64");
|
||||||
|
result[2] = rs.getString("extra_b64");
|
||||||
|
result[3] = String.valueOf(rs.getInt("level"));
|
||||||
|
result[4] = String.valueOf(rs.getDouble("experience"));
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==================== 工单操作 ====================
|
// ==================== 工单操作 ====================
|
||||||
|
|
||||||
public int createTicket(String type,
|
public int createTicket(String type,
|
||||||
@@ -1878,6 +2226,251 @@ public class DatabaseManager {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 领地方块/容器记录 ====================
|
||||||
|
|
||||||
|
/** 记录领地方块破坏/放置 */
|
||||||
|
public void recordLandBlock(String landName, int landId,
|
||||||
|
String world, int x, int y, int z,
|
||||||
|
String playerName, String action,
|
||||||
|
String material) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"INSERT INTO land_block_log"
|
||||||
|
+ " (land_name, land_id, world, x, y, z,"
|
||||||
|
+ " player_name, action, material, time)"
|
||||||
|
+ " VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||||||
|
ps.setString(1, landName);
|
||||||
|
ps.setInt(2, landId);
|
||||||
|
ps.setString(3, world);
|
||||||
|
ps.setInt(4, x);
|
||||||
|
ps.setInt(5, y);
|
||||||
|
ps.setInt(6, z);
|
||||||
|
ps.setString(7, playerName);
|
||||||
|
ps.setString(8, action);
|
||||||
|
ps.setString(9, material);
|
||||||
|
ps.setLong(10, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ★ 记录领地方块(含detail,用于告示牌内容等) */
|
||||||
|
public void recordLandBlockWithDetail(String landName, int landId,
|
||||||
|
String world, int x, int y, int z,
|
||||||
|
String playerName, String action,
|
||||||
|
String material, String detail) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"INSERT INTO land_block_log"
|
||||||
|
+ " (land_name, land_id, world, x, y, z,"
|
||||||
|
+ " player_name, action, material, detail, time)"
|
||||||
|
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
|
ps.setString(1, landName);
|
||||||
|
ps.setInt(2, landId);
|
||||||
|
ps.setString(3, world);
|
||||||
|
ps.setInt(4, x);
|
||||||
|
ps.setInt(5, y);
|
||||||
|
ps.setInt(6, z);
|
||||||
|
ps.setString(7, playerName);
|
||||||
|
ps.setString(8, action);
|
||||||
|
ps.setString(9, material);
|
||||||
|
ps.setString(10, detail);
|
||||||
|
ps.setLong(11, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录领地容器打开/拿取 */
|
||||||
|
public void recordLandContainer(String landName, int landId,
|
||||||
|
String world, int x, int y, int z,
|
||||||
|
String playerName, String action,
|
||||||
|
String containerType, String detail,
|
||||||
|
int amount) {
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"INSERT INTO land_container_log"
|
||||||
|
+ " (land_name, land_id, world, x, y, z,"
|
||||||
|
+ " player_name, action, container_type,"
|
||||||
|
+ " detail, amount, time)"
|
||||||
|
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
|
ps.setString(1, landName);
|
||||||
|
ps.setInt(2, landId);
|
||||||
|
ps.setString(3, world);
|
||||||
|
ps.setInt(4, x);
|
||||||
|
ps.setInt(5, y);
|
||||||
|
ps.setInt(6, z);
|
||||||
|
ps.setString(7, playerName);
|
||||||
|
ps.setString(8, action);
|
||||||
|
ps.setString(9, containerType);
|
||||||
|
ps.setString(10, detail);
|
||||||
|
ps.setInt(11, amount);
|
||||||
|
ps.setLong(12, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询某坐标最近的方块记录(按时间倒序) */
|
||||||
|
public List<Map<String, Object>> getLandBlockLogAt(
|
||||||
|
String world, int x, int y, int z, int limit) {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT time, player_name, action, material, detail"
|
||||||
|
+ " FROM land_block_log"
|
||||||
|
+ " WHERE world=? AND x=? AND y=? AND z=?"
|
||||||
|
+ " ORDER BY time DESC LIMIT ?");
|
||||||
|
ps.setString(1, world);
|
||||||
|
ps.setInt(2, x);
|
||||||
|
ps.setInt(3, y);
|
||||||
|
ps.setInt(4, z);
|
||||||
|
ps.setInt(5, limit > 0 ? limit : 20);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, Object> row =
|
||||||
|
new LinkedHashMap<>();
|
||||||
|
row.put("time", rs.getLong("time"));
|
||||||
|
row.put("player_name",
|
||||||
|
rs.getString("player_name"));
|
||||||
|
row.put("action", rs.getString("action"));
|
||||||
|
row.put("material", rs.getString("material"));
|
||||||
|
row.put("detail", rs.getString("detail"));
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询某坐标最近的容器记录(按时间倒序) */
|
||||||
|
public List<Map<String, Object>> getLandContainerLogAt(
|
||||||
|
String world, int x, int y, int z, int limit) {
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PreparedStatement ps = db.prepareStatement(
|
||||||
|
"SELECT time, player_name, action, container_type,"
|
||||||
|
+ " detail, amount"
|
||||||
|
+ " FROM land_container_log"
|
||||||
|
+ " WHERE world=? AND x=? AND y=? AND z=?"
|
||||||
|
+ " ORDER BY time DESC LIMIT ?");
|
||||||
|
ps.setString(1, world);
|
||||||
|
ps.setInt(2, x);
|
||||||
|
ps.setInt(3, y);
|
||||||
|
ps.setInt(4, z);
|
||||||
|
ps.setInt(5, limit > 0 ? limit : 20);
|
||||||
|
ResultSet rs = ps.executeQuery();
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, Object> row =
|
||||||
|
new LinkedHashMap<>();
|
||||||
|
row.put("time", rs.getLong("time"));
|
||||||
|
row.put("player_name",
|
||||||
|
rs.getString("player_name"));
|
||||||
|
row.put("action", rs.getString("action"));
|
||||||
|
row.put("container_type",
|
||||||
|
rs.getString("container_type"));
|
||||||
|
row.put("detail", rs.getString("detail"));
|
||||||
|
row.put("amount", rs.getInt("amount"));
|
||||||
|
list.add(row);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理超过 cutoff(毫秒时间戳)的领地记录,返回删除行数 */
|
||||||
|
public int pruneLandRecordsOlderThan(long cutoff) {
|
||||||
|
int deleted = 0;
|
||||||
|
try {
|
||||||
|
PreparedStatement ps1 = db.prepareStatement(
|
||||||
|
"DELETE FROM land_block_log WHERE time < ?");
|
||||||
|
ps1.setLong(1, cutoff);
|
||||||
|
deleted += ps1.executeUpdate();
|
||||||
|
ps1.close();
|
||||||
|
|
||||||
|
PreparedStatement ps2 = db.prepareStatement(
|
||||||
|
"DELETE FROM land_container_log WHERE time < ?");
|
||||||
|
ps2.setLong(1, cutoff);
|
||||||
|
deleted += ps2.executeUpdate();
|
||||||
|
ps2.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 背包自动滚动备份(15分钟定时)====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存背包自动备份 —— 追加写入,每位玩家仅保留最近 3 份。
|
||||||
|
* 跳过 PVP 竞技场/测试场的判断由调用方(InventoryAutoBackup)负责。
|
||||||
|
*/
|
||||||
|
public void saveAutoBackup(String name,
|
||||||
|
String contents, String armor,
|
||||||
|
String extra, int level,
|
||||||
|
double experience) {
|
||||||
|
try {
|
||||||
|
try (PreparedStatement ps = db.prepareStatement(
|
||||||
|
"INSERT INTO inv_backup_auto "
|
||||||
|
+ "(player_name, contents_b64, armor_b64, "
|
||||||
|
+ "extra_b64, level, experience, save_time) "
|
||||||
|
+ "VALUES (?,?,?,?,?,?,?)")) {
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.setString(2, contents);
|
||||||
|
ps.setString(3, armor);
|
||||||
|
ps.setString(4, extra);
|
||||||
|
ps.setInt(5, level);
|
||||||
|
ps.setDouble(6, experience);
|
||||||
|
ps.setLong(7, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留最近 3 份 —— 先查最新 3 个 ID,再删其余
|
||||||
|
List<Integer> keep = new ArrayList<>();
|
||||||
|
try (PreparedStatement sel = db.prepareStatement(
|
||||||
|
"SELECT id FROM inv_backup_auto "
|
||||||
|
+ "WHERE player_name=? "
|
||||||
|
+ "ORDER BY save_time DESC LIMIT 3")) {
|
||||||
|
sel.setString(1, name);
|
||||||
|
try (ResultSet rs = sel.executeQuery()) {
|
||||||
|
while (rs.next()) keep.add(rs.getInt("id"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keep.size() == 3) {
|
||||||
|
StringBuilder in = new StringBuilder();
|
||||||
|
for (int i = 0; i < keep.size(); i++) {
|
||||||
|
if (i > 0) in.append(",");
|
||||||
|
in.append(keep.get(i));
|
||||||
|
}
|
||||||
|
try (PreparedStatement del = db.prepareStatement(
|
||||||
|
"DELETE FROM inv_backup_auto "
|
||||||
|
+ "WHERE player_name=? AND id NOT IN ("
|
||||||
|
+ in + ")")) {
|
||||||
|
del.setString(1, name);
|
||||||
|
int removed = del.executeUpdate();
|
||||||
|
if (removed > 0) {
|
||||||
|
logger.info("[自动备份] " + name + " 滚动清理 " + removed + " 份旧备份");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warning("[自动备份] 保存/清理失败 (" + name + "): " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Connection getDb() {
|
public Connection getDb() {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一时长解析器(供 /tempban 与 /chat mute 共用)
|
||||||
|
*
|
||||||
|
* parseToExpireMs(String) 返回「绝对到期时间戳(毫秒)」,调用方自行校验最小/最大区间。
|
||||||
|
*
|
||||||
|
* 支持的写法:
|
||||||
|
* 1) 纯数字(Unix 秒时间戳): 253402271999
|
||||||
|
* 2) 中文日期(绝对): 2026年7月9日
|
||||||
|
* 3) ISO 日期(绝对): 2025-07-09
|
||||||
|
* 4) 相对时长(中文/英文单位): 1秒 / 1s / 1年 / 1年2月3天 / 2天12小时
|
||||||
|
*
|
||||||
|
* 解析规则优先级:纯数字 → 时间戳;含「年/月/日」且为完整日期格式 → 绝对日期;
|
||||||
|
* 否则按「数字+单位」逐段累加为相对时长。
|
||||||
|
*/
|
||||||
|
public class DurationParser {
|
||||||
|
|
||||||
|
private static final Map<String, Long> UNIT_MS = new HashMap<>();
|
||||||
|
static {
|
||||||
|
// 秒
|
||||||
|
UNIT_MS.put("秒", 1000L);
|
||||||
|
UNIT_MS.put("秒钟", 1000L);
|
||||||
|
UNIT_MS.put("s", 1000L);
|
||||||
|
UNIT_MS.put("sec", 1000L);
|
||||||
|
// 分
|
||||||
|
UNIT_MS.put("分", 60000L);
|
||||||
|
UNIT_MS.put("分钟", 60000L);
|
||||||
|
UNIT_MS.put("mins", 60000L);
|
||||||
|
UNIT_MS.put("min", 60000L);
|
||||||
|
UNIT_MS.put("m", 60000L);
|
||||||
|
// 时
|
||||||
|
UNIT_MS.put("时", 3600000L);
|
||||||
|
UNIT_MS.put("小时", 3600000L);
|
||||||
|
UNIT_MS.put("钟头", 3600000L);
|
||||||
|
UNIT_MS.put("h", 3600000L);
|
||||||
|
UNIT_MS.put("hr", 3600000L);
|
||||||
|
// 天
|
||||||
|
UNIT_MS.put("天", 86400000L);
|
||||||
|
UNIT_MS.put("日", 86400000L);
|
||||||
|
UNIT_MS.put("d", 86400000L);
|
||||||
|
// 周
|
||||||
|
UNIT_MS.put("周", 604800000L);
|
||||||
|
UNIT_MS.put("星期", 604800000L);
|
||||||
|
UNIT_MS.put("礼拜", 604800000L);
|
||||||
|
UNIT_MS.put("w", 604800000L);
|
||||||
|
// 月(按30天近似)
|
||||||
|
UNIT_MS.put("个月", 2592000000L);
|
||||||
|
UNIT_MS.put("月", 2592000000L);
|
||||||
|
UNIT_MS.put("mo", 2592000000L);
|
||||||
|
// 年(按365天近似)
|
||||||
|
UNIT_MS.put("年", 31536000000L);
|
||||||
|
UNIT_MS.put("y", 31536000000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析为绝对到期毫秒;无法解析抛出 IllegalArgumentException */
|
||||||
|
public static long parseToExpireMs(String input)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
if (input == null || input.trim().isEmpty())
|
||||||
|
throw new IllegalArgumentException("时长为空");
|
||||||
|
String s = input.trim().replace(" ", "");
|
||||||
|
|
||||||
|
// 1) 纯数字 → Unix 秒时间戳
|
||||||
|
if (s.matches("\\d+")) {
|
||||||
|
long ts = Long.parseLong(s);
|
||||||
|
return ts * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 中文日期 YYYY年M月D日
|
||||||
|
Matcher mc = Pattern.compile(
|
||||||
|
"(\\d{4})年(\\d{1,2})月(\\d{1,2})日").matcher(s);
|
||||||
|
if (mc.find()) {
|
||||||
|
return buildDateMillis(Integer.parseInt(mc.group(1)),
|
||||||
|
Integer.parseInt(mc.group(2)),
|
||||||
|
Integer.parseInt(mc.group(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) ISO 日期 YYYY-MM-DD / YYYY/MM/DD
|
||||||
|
Matcher mi = Pattern.compile(
|
||||||
|
"(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})").matcher(s);
|
||||||
|
if (mi.find()) {
|
||||||
|
return buildDateMillis(Integer.parseInt(mi.group(1)),
|
||||||
|
Integer.parseInt(mi.group(2)),
|
||||||
|
Integer.parseInt(mi.group(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 相对时长(可多段组合)
|
||||||
|
long rel = parseRelative(s);
|
||||||
|
if (rel >= 0) {
|
||||||
|
return System.currentTimeMillis() + rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("无法解析时长: " + input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseRelative(String s) {
|
||||||
|
// 按「数字 + 单位」逐段匹配并累加
|
||||||
|
StringBuilder units = new StringBuilder();
|
||||||
|
for (String u : UNIT_MS.keySet()) {
|
||||||
|
if (units.length() > 0) units.append("|");
|
||||||
|
// 转义,避免正则特殊字符
|
||||||
|
units.append(Pattern.quote(u));
|
||||||
|
}
|
||||||
|
Pattern p = Pattern.compile(
|
||||||
|
"([0-9零一二两三四五六七八九十百千万]+)(" + units + ")");
|
||||||
|
Matcher m = p.matcher(s);
|
||||||
|
long total = 0;
|
||||||
|
int found = 0;
|
||||||
|
while (m.find()) {
|
||||||
|
found++;
|
||||||
|
long num = parseNumber(m.group(1));
|
||||||
|
Long u = UNIT_MS.get(m.group(2));
|
||||||
|
if (u == null) continue;
|
||||||
|
total += num * u;
|
||||||
|
}
|
||||||
|
return found > 0 ? total : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long buildDateMillis(int y, int mo, int d) {
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
c.set(y, mo - 1, d, 0, 0, 0);
|
||||||
|
c.set(Calendar.MILLISECOND, 0);
|
||||||
|
return c.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析数字(阿拉伯数字优先,其次中文数字 零-九/十百千万/两/壹贰叁…) */
|
||||||
|
private static long parseNumber(String s) {
|
||||||
|
if (s.matches("\\d+")) return Long.parseLong(s);
|
||||||
|
|
||||||
|
Map<Character, Long> cn = new HashMap<>();
|
||||||
|
cn.put('零', 0L); cn.put('〇', 0L);
|
||||||
|
cn.put('一', 1L); cn.put('壹', 1L);
|
||||||
|
cn.put('二', 2L); cn.put('贰', 2L); cn.put('两', 2L);
|
||||||
|
cn.put('三', 3L); cn.put('叁', 3L);
|
||||||
|
cn.put('四', 4L); cn.put('肆', 4L);
|
||||||
|
cn.put('五', 5L); cn.put('伍', 5L);
|
||||||
|
cn.put('六', 6L); cn.put('陆', 6L);
|
||||||
|
cn.put('七', 7L); cn.put('柒', 7L);
|
||||||
|
cn.put('八', 8L); cn.put('捌', 8L);
|
||||||
|
cn.put('九', 9L); cn.put('玖', 9L);
|
||||||
|
|
||||||
|
long section = 0, num = 0;
|
||||||
|
boolean hasNum = false;
|
||||||
|
for (char ch : s.toCharArray()) {
|
||||||
|
if (cn.containsKey(ch)) {
|
||||||
|
num = cn.get(ch);
|
||||||
|
hasNum = true;
|
||||||
|
} else if (ch == '十') {
|
||||||
|
if (!hasNum) num = 1;
|
||||||
|
section += num * 10; hasNum = false; num = 0;
|
||||||
|
} else if (ch == '百') {
|
||||||
|
if (!hasNum) num = 1;
|
||||||
|
section += num * 100; hasNum = false; num = 0;
|
||||||
|
} else if (ch == '千') {
|
||||||
|
if (!hasNum) num = 1;
|
||||||
|
section += num * 1000; hasNum = false; num = 0;
|
||||||
|
} else if (ch == '万') {
|
||||||
|
if (!hasNum) num = 1;
|
||||||
|
section = (section + num) * 10000; hasNum = false; num = 0;
|
||||||
|
} else {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return section + (hasNum ? num : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化绝对到期时间戳为「yyyy年M月d日 HH:mm」 */
|
||||||
|
public static String formatExpire(long ms) {
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
c.setTimeInMillis(ms);
|
||||||
|
return String.format("%d年%d月%d日 %02d:%02d",
|
||||||
|
c.get(Calendar.YEAR),
|
||||||
|
c.get(Calendar.MONTH) + 1,
|
||||||
|
c.get(Calendar.DAY_OF_MONTH),
|
||||||
|
c.get(Calendar.HOUR_OF_DAY),
|
||||||
|
c.get(Calendar.MINUTE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -285,9 +285,19 @@ public class HomeManager implements Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取全局家数量限制
|
* 获取全局家数量限制
|
||||||
|
* 读取area_config中的max_home_per_player配置(管理员可在Web后台调整)
|
||||||
|
* 解析失败或值<=0时回退到默认5
|
||||||
*/
|
*/
|
||||||
private int getGlobalHomeLimit() {
|
private int getGlobalHomeLimit() {
|
||||||
// 默认5个家,可从配置文件读取
|
try {
|
||||||
|
if (plugin.areaProtection != null) {
|
||||||
|
int limit = plugin.areaProtection.getGlobalMaxHomePerPlayer();
|
||||||
|
if (limit > 0) {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
// 默认5个家
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背包滚动自动备份。
|
||||||
|
* <p>
|
||||||
|
* ★ 功能:
|
||||||
|
* 1. 由 Main 的 15 分钟定时任务驱动,遍历所有在线玩家。
|
||||||
|
* 2. 在 PVP 竞技场 / 练习场(测试场)的玩家不备份 —— 避免竞技场装备覆盖真实背包。
|
||||||
|
* 3. 序列化 主背包 / 护甲 / 副手(extra) / 等级 / 经验,写入 inv_backup_auto 表。
|
||||||
|
* 4. 每玩家保留最近 3 份(滚动覆盖),由 DatabaseManager.saveAutoBackup 负责清理。
|
||||||
|
* <p>
|
||||||
|
* 与登录时的 saveInventoryBackup 共用同一套存储语义(level=玩家等级,
|
||||||
|
* experience=当前等级进度 p.getExp()),便于后续统一还原。
|
||||||
|
*/
|
||||||
|
public class InventoryAutoBackup {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
/** 备份周期:15 分钟(单位 tick) */
|
||||||
|
public static final long INTERVAL_TICKS = 15L * 60L * 20L;
|
||||||
|
|
||||||
|
public InventoryAutoBackup(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 对所有在线玩家执行一次滚动备份 */
|
||||||
|
public void runBackup() {
|
||||||
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
|
try {
|
||||||
|
backupPlayer(p);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[自动备份] 玩家 " + p.getName()
|
||||||
|
+ " 备份失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void backupPlayer(Player p) {
|
||||||
|
String name = p.getName();
|
||||||
|
|
||||||
|
// ★ 跳过 PVP 竞技场:竞技场内不备份,避免 PVP 装备覆盖真实背包
|
||||||
|
if (plugin.getPVPArenaManager().isInPVPArena(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ★ 跳过练习场 / 测试世界
|
||||||
|
if (plugin.getPVPTestManager().isInTestWorld(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack[] contents = p.getInventory().getContents();
|
||||||
|
ItemStack[] armor = p.getInventory().getArmorContents();
|
||||||
|
ItemStack[] extra = p.getInventory().getExtraContents();
|
||||||
|
|
||||||
|
String contentsB64, armorB64, extraB64;
|
||||||
|
try {
|
||||||
|
contentsB64 = InventorySerializer.toBase64(contents);
|
||||||
|
armorB64 = InventorySerializer.toBase64(armor);
|
||||||
|
extraB64 = InventorySerializer.toBase64(extra);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[自动备份] 序列化失败 (" + name + "): "
|
||||||
|
+ e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int level = p.getLevel();
|
||||||
|
double experience = p.getExp(); // 当前等级进度 0.0~1.0
|
||||||
|
plugin.getDb().saveAutoBackup(
|
||||||
|
name, contentsB64, armorB64, extraB64,
|
||||||
|
level, experience);
|
||||||
|
plugin.getLogger().info("[自动备份] " + name + " 背包已备份");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import Sdf1_login.AreaProtection.AreaConfig;
|
||||||
|
|
||||||
|
import org.bukkit.*;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领地方块/容器记录系统 —— 回声碎片(echo_shard)工具。
|
||||||
|
* <p>
|
||||||
|
* ★ 功能:
|
||||||
|
* 1. 领地内方块破坏/放置 → 自动写入 land_block_log
|
||||||
|
* 2. 领地内容器打开/拿取 → 自动写入 land_container_log
|
||||||
|
* 3. 玩家手持回声碎片右键/左键方块或容器 → 聊天栏打印该坐标的历史记录
|
||||||
|
* 4. /landrec give [玩家] → 发放回声碎片(由插件/管理员发放)
|
||||||
|
* <p>
|
||||||
|
* 记录保留 7 天滚动(由 Main 的每日清理定时器调用 prune)。
|
||||||
|
*/
|
||||||
|
public class LandRecordManager implements Listener {
|
||||||
|
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
/** 回声碎片物品的 NBT 标识 key */
|
||||||
|
public static final String ECHO_SHARD_KEY = "sdf1_echo_shard";
|
||||||
|
/** 回声碎片物品材质(1.19+ 存在) */
|
||||||
|
public static final Material ECHO_SHARD_MATERIAL = Material.ECHO_SHARD;
|
||||||
|
|
||||||
|
/** 记录查询默认条数 */
|
||||||
|
private static final int QUERY_LIMIT = 20;
|
||||||
|
/** 记录保留天数 */
|
||||||
|
public static final int RETENTION_DAYS = 7;
|
||||||
|
|
||||||
|
private final SimpleDateFormat sdf =
|
||||||
|
new SimpleDateFormat("MM-dd HH:mm");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 容器打开时的快照:用于关闭时计算"拿了什么"。
|
||||||
|
* key = 玩家名(MC 同一时刻只能打开一个容器 GUI,故按玩家唯一即可)。
|
||||||
|
*/
|
||||||
|
private final Map<String, ContainerSnapshot> openSnapshots =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
static class ContainerSnapshot {
|
||||||
|
String landName;
|
||||||
|
int landId;
|
||||||
|
String world;
|
||||||
|
int x, y, z;
|
||||||
|
String containerType;
|
||||||
|
Map<String, Integer> before = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LandRecordManager(Main plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 回声碎片物品 ====================
|
||||||
|
|
||||||
|
/** 创建一枚回声碎片(带 NBT 标识,便于精准识别) */
|
||||||
|
public ItemStack createEchoShard() {
|
||||||
|
ItemStack item = new ItemStack(ECHO_SHARD_MATERIAL);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setDisplayName("§b§l回声碎片");
|
||||||
|
meta.setLore(Arrays.asList(
|
||||||
|
"§7右键/左键 方块或容器",
|
||||||
|
"§7查看领地内的破坏/拿取记录",
|
||||||
|
"§8(领地监控工具)"));
|
||||||
|
NamespacedKey key =
|
||||||
|
new NamespacedKey(plugin, ECHO_SHARD_KEY);
|
||||||
|
meta.getPersistentDataContainer().set(
|
||||||
|
key, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断物品是否为回声碎片(材质 + NBT 双重校验) */
|
||||||
|
public boolean isEchoShard(ItemStack item) {
|
||||||
|
if (item == null || item.getType() != ECHO_SHARD_MATERIAL)
|
||||||
|
return false;
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta == null) return false;
|
||||||
|
NamespacedKey key =
|
||||||
|
new NamespacedKey(plugin, ECHO_SHARD_KEY);
|
||||||
|
return meta.getPersistentDataContainer().has(
|
||||||
|
key, PersistentDataType.BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直接发放一枚回声碎片给玩家(GUI 菜单调用) */
|
||||||
|
public void giveEchoShard(Player p) {
|
||||||
|
ItemStack shard = createEchoShard();
|
||||||
|
HashMap<Integer, ItemStack> left =
|
||||||
|
p.getInventory().addItem(shard);
|
||||||
|
if (!left.isEmpty()) {
|
||||||
|
p.sendMessage("§c§l[回声碎片] §f你的背包已满,无法发放");
|
||||||
|
} else {
|
||||||
|
p.sendMessage("§a§l[回声碎片] §f你获得了§b回声碎片§f,右键/左键方块或容器查看领地记录");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 记录写入(由 AreaProtection 钩子调用)====================
|
||||||
|
|
||||||
|
/** 记录方块破坏 */
|
||||||
|
public void recordBlockBreak(AreaConfig ac, Player p,
|
||||||
|
Block block) {
|
||||||
|
recordBlock(ac, p, block, "break");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录方块放置 */
|
||||||
|
public void recordBlockPlace(AreaConfig ac, Player p,
|
||||||
|
Block block) {
|
||||||
|
recordBlock(ac, p, block, "place");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录红石元器件状态变化(压力板、按钮、拉杆等) */
|
||||||
|
public void recordRedstoneStateChange(AreaConfig ac, Player p,
|
||||||
|
Block block) {
|
||||||
|
recordBlock(ac, p, block, "state_change");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ★ 记录告示牌放置/编辑(含内容) */
|
||||||
|
public void recordSignEdit(AreaConfig ac, Player p, Block block,
|
||||||
|
String[] lines, boolean isEdit) {
|
||||||
|
try {
|
||||||
|
int landId = plugin.areaProtection.getLandIdFromDb(ac.name);
|
||||||
|
// 拼接4行内容,空行跳过
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line != null && !line.isEmpty()) {
|
||||||
|
if (sb.length() > 0) sb.append(" | ");
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String detail = sb.toString();
|
||||||
|
String action = isEdit ? "sign_edit" : "sign_place";
|
||||||
|
plugin.getDb().recordLandBlockWithDetail(
|
||||||
|
ac.name, landId,
|
||||||
|
block.getWorld().getName(),
|
||||||
|
block.getX(), block.getY(), block.getZ(),
|
||||||
|
p.getName(), action,
|
||||||
|
block.getType().name(), detail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordBlock(AreaConfig ac, Player p, Block block,
|
||||||
|
String action) {
|
||||||
|
try {
|
||||||
|
int landId = plugin.areaProtection.getLandIdFromDb(
|
||||||
|
ac.name);
|
||||||
|
plugin.getDb().recordLandBlock(
|
||||||
|
ac.name, landId,
|
||||||
|
block.getWorld().getName(),
|
||||||
|
block.getX(), block.getY(), block.getZ(),
|
||||||
|
p.getName(), action,
|
||||||
|
block.getType().name());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录容器打开,并保存打开瞬间容器快照(用于关闭时计算拿取差量)。
|
||||||
|
* 仅在容器确实被允许打开时调用(钩子位于 AreaProtection
|
||||||
|
* 权限/取消检查之后)。
|
||||||
|
*/
|
||||||
|
public void recordContainerOpen(AreaConfig ac, Player p,
|
||||||
|
Location loc, String containerType,
|
||||||
|
Inventory inv) {
|
||||||
|
try {
|
||||||
|
int landId = plugin.areaProtection.getLandIdFromDb(
|
||||||
|
ac.name);
|
||||||
|
plugin.getDb().recordLandContainer(
|
||||||
|
ac.name, landId,
|
||||||
|
loc.getWorld().getName(),
|
||||||
|
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(),
|
||||||
|
p.getName(), "open", containerType, "", 0);
|
||||||
|
|
||||||
|
ContainerSnapshot snap = new ContainerSnapshot();
|
||||||
|
snap.landName = ac.name;
|
||||||
|
snap.landId = landId;
|
||||||
|
snap.world = loc.getWorld().getName();
|
||||||
|
snap.x = loc.getBlockX();
|
||||||
|
snap.y = loc.getBlockY();
|
||||||
|
snap.z = loc.getBlockZ();
|
||||||
|
snap.containerType = containerType;
|
||||||
|
snap.before = scanInventory(inv);
|
||||||
|
openSnapshots.put(p.getName(), snap);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 容器关闭:计算拿取差量 ====================
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onContainerClose(InventoryCloseEvent e) {
|
||||||
|
if (!(e.getPlayer() instanceof Player)) return;
|
||||||
|
Player p = (Player) e.getPlayer();
|
||||||
|
ContainerSnapshot snap = openSnapshots.remove(p.getName());
|
||||||
|
if (snap == null) return;
|
||||||
|
|
||||||
|
Map<String, Integer> after = scanInventory(e.getInventory());
|
||||||
|
|
||||||
|
// 1. 拿取:before > after
|
||||||
|
for (Map.Entry<String, Integer> en : snap.before.entrySet()) {
|
||||||
|
int taken = en.getValue() - after.getOrDefault(en.getKey(), 0);
|
||||||
|
if (taken > 0) {
|
||||||
|
recordContainerChange(snap, p.getName(), "take",
|
||||||
|
en.getKey(), taken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 存入:after > before
|
||||||
|
for (Map.Entry<String, Integer> en : after.entrySet()) {
|
||||||
|
int put = en.getValue() - snap.before.getOrDefault(en.getKey(), 0);
|
||||||
|
if (put > 0) {
|
||||||
|
recordContainerChange(snap, p.getName(), "put",
|
||||||
|
en.getKey(), put);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordContainerChange(ContainerSnapshot snap,
|
||||||
|
String playerName, String action,
|
||||||
|
String detail, int amount) {
|
||||||
|
try {
|
||||||
|
plugin.getDb().recordLandContainer(
|
||||||
|
snap.landName, snap.landId,
|
||||||
|
snap.world, snap.x, snap.y, snap.z,
|
||||||
|
playerName, action, snap.containerType,
|
||||||
|
detail, amount);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫描容器库存,按"类型名"聚合数量 */
|
||||||
|
private Map<String, Integer> scanInventory(Inventory inv) {
|
||||||
|
Map<String, Integer> map = new HashMap<>();
|
||||||
|
for (ItemStack it : inv.getContents()) {
|
||||||
|
if (it == null || it.getType() == Material.AIR)
|
||||||
|
continue;
|
||||||
|
map.merge(it.getType().name(),
|
||||||
|
it.getAmount(), Integer::sum);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 回声碎片查询(右键/左键)====================
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onShardInteract(PlayerInteractEvent e) {
|
||||||
|
ItemStack item = e.getItem();
|
||||||
|
if (item == null || !isEchoShard(item)) return;
|
||||||
|
Action a = e.getAction();
|
||||||
|
if (a != Action.RIGHT_CLICK_BLOCK && a != Action.LEFT_CLICK_BLOCK
|
||||||
|
&& a != Action.RIGHT_CLICK_AIR
|
||||||
|
&& a != Action.LEFT_CLICK_AIR) return;
|
||||||
|
|
||||||
|
Player p = e.getPlayer();
|
||||||
|
// 阻止误触方块/打开容器
|
||||||
|
e.setCancelled(true);
|
||||||
|
|
||||||
|
Block target = e.getClickedBlock();
|
||||||
|
if (target == null) {
|
||||||
|
// 视线选中的方块(非 deprecated 的 getTargetBlockExact)
|
||||||
|
Block sight = p.getTargetBlockExact(6);
|
||||||
|
if (sight != null && sight.getType() != Material.AIR)
|
||||||
|
target = sight;
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
p.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryAndPrint(p, target.getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queryAndPrint(Player p, Location loc) {
|
||||||
|
String world = loc.getWorld().getName();
|
||||||
|
int x = loc.getBlockX(), y = loc.getBlockY(),
|
||||||
|
z = loc.getBlockZ();
|
||||||
|
|
||||||
|
List<Map<String, Object>> blocks =
|
||||||
|
plugin.getDb().getLandBlockLogAt(
|
||||||
|
world, x, y, z, QUERY_LIMIT);
|
||||||
|
List<Map<String, Object>> containers =
|
||||||
|
plugin.getDb().getLandContainerLogAt(
|
||||||
|
world, x, y, z, QUERY_LIMIT);
|
||||||
|
|
||||||
|
if (blocks.isEmpty() && containers.isEmpty()) {
|
||||||
|
p.sendMessage("§6§l[回声碎片] §f坐标 ("
|
||||||
|
+ x + "," + y + "," + z + ") 暂无领地记录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有需要翻译的材料名
|
||||||
|
Set<String> materials = new LinkedHashSet<>();
|
||||||
|
for (Map<String, Object> r : blocks) {
|
||||||
|
materials.add((String) r.get("material"));
|
||||||
|
}
|
||||||
|
for (Map<String, Object> r : containers) {
|
||||||
|
if ("take".equals(r.get("action")) || "put".equals(r.get("action"))) {
|
||||||
|
materials.add((String) r.get("detail"));
|
||||||
|
}
|
||||||
|
// 容器类型(如 CHEST)也要翻译
|
||||||
|
String ct = (String) r.get("container_type");
|
||||||
|
if (ct != null && !ct.isEmpty()) {
|
||||||
|
materials.add(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步批量翻译
|
||||||
|
MaterialTranslator.translateBatch(materials)
|
||||||
|
.thenAccept(translations -> {
|
||||||
|
// 回到主线程发送消息
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
printResults(p, x, y, z, blocks, containers, translations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printResults(Player p, int x, int y, int z,
|
||||||
|
List<Map<String, Object>> blocks,
|
||||||
|
List<Map<String, Object>> containers,
|
||||||
|
Map<String, String> translations) {
|
||||||
|
p.sendMessage("§6§l[回声碎片] §e坐标 ("
|
||||||
|
+ x + "," + y + "," + z + ") 的记录:");
|
||||||
|
|
||||||
|
if (!blocks.isEmpty()) {
|
||||||
|
p.sendMessage("§b— 方块记录 —");
|
||||||
|
for (Map<String, Object> r : blocks) {
|
||||||
|
String action = (String) r.get("action");
|
||||||
|
String act;
|
||||||
|
if ("break".equals(action)) {
|
||||||
|
act = "§c破坏";
|
||||||
|
} else if ("place".equals(action)) {
|
||||||
|
act = "§a放置";
|
||||||
|
} else if ("state_change".equals(action)) {
|
||||||
|
act = "§e状态变化";
|
||||||
|
} else if ("sign_place".equals(action)) {
|
||||||
|
act = "§a放置告示牌";
|
||||||
|
} else if ("sign_edit".equals(action)) {
|
||||||
|
act = "§b编辑告示牌";
|
||||||
|
} else {
|
||||||
|
act = "§7" + action;
|
||||||
|
}
|
||||||
|
String mat = (String) r.get("material");
|
||||||
|
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
|
||||||
|
StringBuilder msg = new StringBuilder();
|
||||||
|
msg.append(" §7").append(fmt((Long) r.get("time")))
|
||||||
|
.append(" §f").append(r.get("player_name"))
|
||||||
|
.append(" ").append(act)
|
||||||
|
.append(" §e").append(mat).append(" §a").append(zh);
|
||||||
|
// ★ 告示牌内容显示
|
||||||
|
String detail = (String) r.get("detail");
|
||||||
|
if (detail != null && !detail.isEmpty()
|
||||||
|
&& ("sign_place".equals(action) || "sign_edit".equals(action))) {
|
||||||
|
msg.append(" §7[§f").append(detail).append("§7]");
|
||||||
|
}
|
||||||
|
p.sendMessage(msg.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containers.isEmpty()) {
|
||||||
|
p.sendMessage("§b— 容器记录 —");
|
||||||
|
for (Map<String, Object> r : containers) {
|
||||||
|
String act = (String) r.get("action");
|
||||||
|
if ("open".equals(act)) {
|
||||||
|
String ct = (String) r.get("container_type");
|
||||||
|
String ctZh = translations.getOrDefault(ct, MaterialTranslator.toReadable(ct));
|
||||||
|
p.sendMessage(" §7" + fmt((Long) r.get("time"))
|
||||||
|
+ " §f" + r.get("player_name")
|
||||||
|
+ " §d打开§7[" + ct + "] §a" + ctZh);
|
||||||
|
} else if ("take".equals(act)) {
|
||||||
|
String mat = (String) r.get("detail");
|
||||||
|
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
|
||||||
|
p.sendMessage(" §7" + fmt((Long) r.get("time"))
|
||||||
|
+ " §f" + r.get("player_name")
|
||||||
|
+ " §c拿取 §e" + mat + " §a" + zh
|
||||||
|
+ " §7x" + r.get("amount"));
|
||||||
|
} else if ("put".equals(act)) {
|
||||||
|
String mat = (String) r.get("detail");
|
||||||
|
String zh = translations.getOrDefault(mat, MaterialTranslator.toReadable(mat));
|
||||||
|
p.sendMessage(" §7" + fmt((Long) r.get("time"))
|
||||||
|
+ " §f" + r.get("player_name")
|
||||||
|
+ " §a存入 §e" + mat + " §a" + zh
|
||||||
|
+ " §7x" + r.get("amount"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmt(long ms) {
|
||||||
|
return sdf.format(new Date(ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 命令:/landrec ====================
|
||||||
|
|
||||||
|
public boolean handleCommand(CommandSender sender,
|
||||||
|
String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
sendHelp(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String sub = args[0];
|
||||||
|
if (sub.equalsIgnoreCase("give")) {
|
||||||
|
// 发放权限:给自己(self)任意玩家可发;发给他人需 OP 或领地管理员
|
||||||
|
boolean isOp = sender.isOp();
|
||||||
|
boolean isAreaAdmin = (sender instanceof Player)
|
||||||
|
&& plugin.areaProtection.isAreaAdmin((Player) sender);
|
||||||
|
|
||||||
|
Player target;
|
||||||
|
if (args.length >= 2) {
|
||||||
|
if (!isOp && !isAreaAdmin) {
|
||||||
|
sender.sendMessage("§c§l[回声碎片] §f你无权向他人发放回声碎片");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
target = Bukkit.getPlayerExact(args[1]);
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage("§c§l[回声碎片] §f玩家 " + args[1] + " 不在线");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (sender instanceof Player) {
|
||||||
|
target = (Player) sender;
|
||||||
|
} else {
|
||||||
|
sender.sendMessage("§c§l[回声碎片] §f请指定要发放的玩家");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
giveEchoShard(target);
|
||||||
|
if (target != sender)
|
||||||
|
sender.sendMessage("§a§l[回声碎片] §f已发放回声碎片给 " + target.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
sendHelp(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandSender sender) {
|
||||||
|
sender.sendMessage("§6§l[回声碎片] §e指令:");
|
||||||
|
sender.sendMessage(" §7/landrec give §f- 给自己发放一枚回声碎片");
|
||||||
|
sender.sendMessage(" §7/landrec give <玩家> §f- 向他人发放(需 OP/领地管理员)");
|
||||||
|
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处记录");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,6 +154,16 @@ public class LoginManager {
|
|||||||
.msg("not_registered"));
|
.msg("not_registered"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 风控检查:封禁期内拒绝登录
|
||||||
|
if (plugin.getRiskControl().isBanned(name)) {
|
||||||
|
int remaining = plugin.getRiskControl()
|
||||||
|
.getBanRemainingSeconds(name);
|
||||||
|
p.sendMessage("§c§l[安全风控] §f账号因密码错误过多已被封禁,"
|
||||||
|
+ "剩余 §e" + remaining + " §f秒");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
p.sendMessage("§c用法: /login <密码>");
|
p.sendMessage("§c用法: /login <密码>");
|
||||||
return true;
|
return true;
|
||||||
@@ -176,6 +186,8 @@ public class LoginManager {
|
|||||||
plugin.getDb().checkPassword(
|
plugin.getDb().checkPassword(
|
||||||
name, hash);
|
name, hash);
|
||||||
if (matchMain) {
|
if (matchMain) {
|
||||||
|
// ★ 登录成功,重置风控
|
||||||
|
plugin.getRiskControl().onLoginSuccess(name);
|
||||||
plugin.getLoggedIn().add(name);
|
plugin.getLoggedIn().add(name);
|
||||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||||
if (plugin.webManager != null) {
|
if (plugin.webManager != null) {
|
||||||
@@ -225,6 +237,8 @@ public class LoginManager {
|
|||||||
.checkPasswordWithFallback(
|
.checkPasswordWithFallback(
|
||||||
name, hash);
|
name, hash);
|
||||||
if (pwdResult != null) {
|
if (pwdResult != null) {
|
||||||
|
// ★ 临时密码登录成功,也重置风控
|
||||||
|
plugin.getRiskControl().onLoginSuccess(name);
|
||||||
plugin.getLoggedIn().add(name);
|
plugin.getLoggedIn().add(name);
|
||||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||||
if (plugin.webManager != null) {
|
if (plugin.webManager != null) {
|
||||||
@@ -256,8 +270,19 @@ public class LoginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ★ 密码错误,记录风控
|
||||||
|
int remaining = plugin.getRiskControl()
|
||||||
|
.recordFailAttempt(p);
|
||||||
|
if (remaining < 0) {
|
||||||
|
// 已被封禁(triggerBan内部已踢出)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
p.sendMessage(plugin.getConfig2()
|
p.sendMessage(plugin.getConfig2()
|
||||||
.msg("login_failed"));
|
.msg("login_failed"));
|
||||||
|
if (remaining > 0) {
|
||||||
|
p.sendMessage("§c§l[风控] §f剩余 §e"
|
||||||
|
+ remaining + " §f次尝试机会");
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
plugin.getLogger().severe(
|
plugin.getLogger().severe(
|
||||||
"[Sdf1_login] handleLogin异常: "
|
"[Sdf1_login] handleLogin异常: "
|
||||||
|
|||||||
+1317
-257
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多源在线翻译工具 —— 将MC英文材料名翻译为中文。
|
||||||
|
* 优先使用国内接口(有道/简心),失败后降级到Google。
|
||||||
|
* 结果缓存避免重复请求。
|
||||||
|
*/
|
||||||
|
public class MaterialTranslator {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
private static final HttpClient http = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(3))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 翻译源优先级:国内接口优先
|
||||||
|
private static final List<TranslationSource> SOURCES = Arrays.asList(
|
||||||
|
TranslationSource.YOUDAO,
|
||||||
|
TranslationSource.JIANXIN,
|
||||||
|
TranslationSource.GOOGLE
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译源枚举
|
||||||
|
*/
|
||||||
|
private enum TranslationSource {
|
||||||
|
YOUDAO("有道翻译"),
|
||||||
|
JIANXIN("简心翻译"),
|
||||||
|
GOOGLE("Google翻译");
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
TranslationSource(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将材料名(如 GRASS_BLOCK)转为可读英文(Grass Block)
|
||||||
|
*/
|
||||||
|
public static String toReadable(String materialName) {
|
||||||
|
if (materialName == null || materialName.isEmpty()) return "";
|
||||||
|
return Arrays.stream(materialName.split("_"))
|
||||||
|
.map(w -> w.substring(0, 1).toUpperCase() + w.substring(1).toLowerCase())
|
||||||
|
.reduce((a, b) -> a + " " + b)
|
||||||
|
.orElse(materialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步翻译单个材料名(有缓存则直接返回)。
|
||||||
|
* 优先走 MC 官方 zh_cn.json 翻译,查不到才走机翻。
|
||||||
|
*/
|
||||||
|
public static String translate(String materialName) {
|
||||||
|
if (materialName == null || materialName.isEmpty()) return "";
|
||||||
|
String key = materialName.toUpperCase();
|
||||||
|
|
||||||
|
// 缓存命中
|
||||||
|
if (cache.containsKey(key)) {
|
||||||
|
return cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 官方翻译(基于原始 Material 名 GRASS_BLOCK → block.minecraft.grass_block)
|
||||||
|
String official = OfficialTranslations.lookUp(materialName);
|
||||||
|
if (official != null && !official.isEmpty()) {
|
||||||
|
cache.put(key, official);
|
||||||
|
return official;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 机翻兜底
|
||||||
|
String readable = toReadable(key);
|
||||||
|
String zh = translateWithFallback(readable);
|
||||||
|
if (zh != null && !zh.isEmpty()) {
|
||||||
|
cache.put(key, zh);
|
||||||
|
return zh;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readable; // fallback 可读英文
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量异步翻译,返回 future map
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<Map<String, String>> translateBatch(Collection<String> materials) {
|
||||||
|
Set<String> unique = new LinkedHashSet<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null && !m.isEmpty() && !cache.containsKey(m.toUpperCase())) {
|
||||||
|
unique.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unique.isEmpty()) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null) result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||||
|
for (String m : unique) {
|
||||||
|
futures.add(CompletableFuture.runAsync(() -> translate(m), executor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||||
|
.thenApply(v -> {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null) {
|
||||||
|
result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多源翻译,按优先级尝试,失败自动降级
|
||||||
|
*/
|
||||||
|
private static String translateWithFallback(String text) {
|
||||||
|
for (TranslationSource source : SOURCES) {
|
||||||
|
try {
|
||||||
|
String result = null;
|
||||||
|
switch (source) {
|
||||||
|
case YOUDAO:
|
||||||
|
result = translateViaYoudao(text);
|
||||||
|
break;
|
||||||
|
case JIANXIN:
|
||||||
|
result = translateViaJianxin(text);
|
||||||
|
break;
|
||||||
|
case GOOGLE:
|
||||||
|
result = translateViaGoogle(text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (result != null && !result.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Bukkit.getLogger().warning("[翻译] 机翻 " + source.name + " 异常: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 翻译源实现 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有道翻译API
|
||||||
|
* URL: https://v.api.aa1.cn/api/api-fanyi-yd/index.php
|
||||||
|
* 参数: msg=翻译内容, type=1(中→英) / 2(英→中)
|
||||||
|
*/
|
||||||
|
private static String translateViaYoudao(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
// type=2 表示英→中
|
||||||
|
String url = "https://v.api.aa1.cn/api/api-fanyi-yd/index.php?msg=" + encoded + "&type=2";
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 返回: {"type":"中英互译","desc":"中文翻译英文","text":"翻译结果"}
|
||||||
|
return parseJsonField(resp.body(), "text");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简心翻译API
|
||||||
|
* URL: https://api.qvqa.cn/api/fanyi
|
||||||
|
* 参数: text=翻译内容, source=en, target=zh
|
||||||
|
*/
|
||||||
|
private static String translateViaJianxin(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
String url = "https://api.qvqa.cn/api/fanyi?text=" + encoded + "&source=en&target=zh";
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 返回: {"code":200,"message":"OK","data":{"sourceText":"...","targetText":"翻译结果",...}}
|
||||||
|
return parseNestedJsonField(resp.body(), "data", "targetText");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Translate 免费接口
|
||||||
|
*/
|
||||||
|
private static String translateViaGoogle(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
String url = "https://clients5.google.com/translate_a/single"
|
||||||
|
+ "?client=gtx&sl=en&tl=zh-CN&dt=t&q=" + encoded;
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 响应格式: [["翻译结果","原文",...],...]
|
||||||
|
String body = resp.body();
|
||||||
|
int firstQuote = body.indexOf("[[\"");
|
||||||
|
if (firstQuote >= 0) {
|
||||||
|
int endQuote = body.indexOf("\"", firstQuote + 3);
|
||||||
|
if (endQuote > firstQuote) {
|
||||||
|
return body.substring(firstQuote + 3, endQuote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== JSON解析工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单JSON字段解析(单层)
|
||||||
|
* 从 {"key":"value"} 中提取 value
|
||||||
|
*/
|
||||||
|
private static String parseJsonField(String json, String field) {
|
||||||
|
try {
|
||||||
|
String pattern = "\"" + field + "\"";
|
||||||
|
int idx = json.indexOf(pattern);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
// 找到冒号后的引号
|
||||||
|
int colon = json.indexOf(':', idx + pattern.length());
|
||||||
|
if (colon < 0) return null;
|
||||||
|
|
||||||
|
int quoteStart = json.indexOf('"', colon + 1);
|
||||||
|
if (quoteStart < 0) return null;
|
||||||
|
|
||||||
|
int quoteEnd = json.indexOf('"', quoteStart + 1);
|
||||||
|
if (quoteEnd < 0) return null;
|
||||||
|
|
||||||
|
return json.substring(quoteStart + 1, quoteEnd);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 嵌套JSON字段解析
|
||||||
|
* 从 {"outer":{"inner":"value"}} 中提取 value
|
||||||
|
*/
|
||||||
|
private static String parseNestedJsonField(String json, String outerField, String innerField) {
|
||||||
|
try {
|
||||||
|
// 找到外层对象
|
||||||
|
String outerPattern = "\"" + outerField + "\"";
|
||||||
|
int outerIdx = json.indexOf(outerPattern);
|
||||||
|
if (outerIdx < 0) return null;
|
||||||
|
|
||||||
|
// 找到外层对象的开始大括号
|
||||||
|
int braceStart = json.indexOf('{', outerIdx);
|
||||||
|
if (braceStart < 0) return null;
|
||||||
|
|
||||||
|
// 从外层对象内部解析内层字段
|
||||||
|
int depth = 1;
|
||||||
|
int i = braceStart + 1;
|
||||||
|
while (i < json.length() && depth > 0) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') depth++;
|
||||||
|
else if (c == '}') depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
String innerJson = json.substring(braceStart, i);
|
||||||
|
return parseJsonField(innerJson, innerField);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存(可选)
|
||||||
|
*/
|
||||||
|
public static void clearCache() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前使用的翻译源信息(调试用)
|
||||||
|
*/
|
||||||
|
public static String getSourceInfo() {
|
||||||
|
StringBuilder sb = new StringBuilder("翻译源优先级: ");
|
||||||
|
for (int i = 0; i < SOURCES.size(); i++) {
|
||||||
|
TranslationSource src = SOURCES.get(i);
|
||||||
|
sb.append(i + 1).append(".").append(src.name);
|
||||||
|
if (i < SOURCES.size() - 1) sb.append(" > ");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MC 官方中文翻译加载器。<br>
|
||||||
|
* <br>
|
||||||
|
* 工作流程优先级:<br>
|
||||||
|
* 1. 本地缓存 (plugins/Sdf1_login/zh_cn.json) —— 同步加载,开机即用<br>
|
||||||
|
* 2. Mojang API —— 异步下载最新版,写缓存并刷入内存<br>
|
||||||
|
* 3. 全部失败 —— 返回 null,调用者走机翻兜底<br>
|
||||||
|
* <br>
|
||||||
|
* zh_cn.json 键示例:<br>
|
||||||
|
* {@code block.minecraft.grass_block → "草方块"}<br>
|
||||||
|
* {@code item.minecraft.diamond_sword → "钻石剑"}<br>
|
||||||
|
* <br>
|
||||||
|
* 查找策略:直接 key → block.minecraft.* → item.minecraft.*
|
||||||
|
*/
|
||||||
|
public class OfficialTranslations {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
|
||||||
|
private static final Gson gson = new Gson();
|
||||||
|
|
||||||
|
// ---------- 对外 API ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化(在 onEnable 调用)。<br>
|
||||||
|
* 纯同步:先读本地缓存 → 没有则从 Mojang API 下载并写缓存。<br>
|
||||||
|
* 缓存文件由部署脚本随 JAR 一起上传,确保服务器不直连 Mojang 也能加载。
|
||||||
|
*/
|
||||||
|
public static void init(Main plugin) {
|
||||||
|
File cacheFile = new File(plugin.getDataFolder(), "zh_cn.json");
|
||||||
|
|
||||||
|
// 1. 本地缓存 —— 同步加载
|
||||||
|
if (cacheFile.isFile()) {
|
||||||
|
try {
|
||||||
|
String content = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8);
|
||||||
|
loadFromJson(content);
|
||||||
|
plugin.getLogger().info("[官方翻译] 已加载本地缓存 zh_cn.json(" + map.size() + " 条)");
|
||||||
|
return; // 缓存命中,完毕
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[官方翻译] 本地缓存损坏: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 无缓存或损坏 → 同步从 Mojang API 下载
|
||||||
|
plugin.getLogger().info("[官方翻译] 本地缓存不存在/损坏,从 Mojang API 同步下载...");
|
||||||
|
try {
|
||||||
|
String version = getMinecraftVersion();
|
||||||
|
if (version == null) throw new RuntimeException("无法获取服务器版本号");
|
||||||
|
String json = downloadFromMojang(version);
|
||||||
|
if (json == null) throw new RuntimeException("下载返回空");
|
||||||
|
Files.writeString(cacheFile.toPath(), json, StandardCharsets.UTF_8);
|
||||||
|
loadFromJson(json);
|
||||||
|
plugin.getLogger().info("[官方翻译] Mojang 下载成功(" + map.size() + " 条),已缓存至 " + cacheFile.getName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[官方翻译] 同步下载失败: " + e.getMessage() + ",走机翻兜底");
|
||||||
|
plugin.getLogger().log(Level.FINE, "[官方翻译] 同步下载详情", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找官方翻译。<br>
|
||||||
|
* 传入原始 Material 名(如 {@code GRASS_BLOCK}),自动尝试多种 key 模式。
|
||||||
|
*
|
||||||
|
* @param materialName 原始 Material 名(大小写无关)
|
||||||
|
* @return 中文翻译,或 null(未命中)
|
||||||
|
*/
|
||||||
|
public static String lookUp(String materialName) {
|
||||||
|
if (materialName == null || materialName.isEmpty()) return null;
|
||||||
|
String lower = materialName.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
// 日志:当前 map 状态
|
||||||
|
Bukkit.getLogger().info("[翻译] officialLookUp: " + materialName
|
||||||
|
+ " | map已加载=" + isLoaded() + " 条目数=" + map.size());
|
||||||
|
|
||||||
|
// 1. 精确匹配(部分 key 可直接用)
|
||||||
|
String val = map.get(lower);
|
||||||
|
if (val != null) {
|
||||||
|
Bukkit.getLogger().info("[翻译] officialLookUp 命中(精确): " + lower + " → " + val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. block.minecraft.<name>
|
||||||
|
val = map.get("block.minecraft." + lower);
|
||||||
|
if (val != null) {
|
||||||
|
Bukkit.getLogger().info("[翻译] officialLookUp 命中(block): " + lower + " → " + val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. item.minecraft.<name>
|
||||||
|
val = map.get("item.minecraft." + lower);
|
||||||
|
if (val != null) {
|
||||||
|
Bukkit.getLogger().info("[翻译] officialLookUp 命中(item): " + lower + " → " + val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bukkit.getLogger().info("[翻译] officialLookUp 未命中: " + lower + ",将走机翻兜底");
|
||||||
|
return null; // 未命中 -> 调用者走机翻
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前加载的条目数(调试用) */
|
||||||
|
public static int size() {
|
||||||
|
return map.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否已完成至少一次加载(缓存或下载) */
|
||||||
|
public static boolean isLoaded() {
|
||||||
|
return !map.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 内部 ----------
|
||||||
|
|
||||||
|
/** 解析 zh_cn.json 字符串灌入内存 */
|
||||||
|
static void loadFromJson(String json) {
|
||||||
|
JsonObject obj = gson.fromJson(json, JsonObject.class);
|
||||||
|
if (obj == null) return;
|
||||||
|
map.clear();
|
||||||
|
for (Map.Entry<String, com.google.gson.JsonElement> entry : obj.entrySet()) {
|
||||||
|
map.put(entry.getKey(), entry.getValue().getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Mojang API 拉取 zh_cn.json(完整链:manifest → version → asset index → object) */
|
||||||
|
static String downloadFromMojang(String version) throws Exception {
|
||||||
|
HttpClient client = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Step 1: Version manifest
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("https://launchermeta.mojang.com/mc/game/version_manifest.json"))
|
||||||
|
.timeout(Duration.ofSeconds(10))
|
||||||
|
.header("User-Agent", "Sdf1Login/1.0")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() != 200) return null;
|
||||||
|
|
||||||
|
JsonObject manifest = gson.fromJson(resp.body(), JsonObject.class);
|
||||||
|
|
||||||
|
// 在 versions 数组中找目标版本
|
||||||
|
String versionUrl = null;
|
||||||
|
JsonArray versions = manifest.getAsJsonArray("versions");
|
||||||
|
if (versions != null) {
|
||||||
|
for (int i = 0; i < versions.size(); i++) {
|
||||||
|
JsonObject v = versions.get(i).getAsJsonObject();
|
||||||
|
if (version.equals(v.get("id").getAsString())) {
|
||||||
|
versionUrl = v.get("url").getAsString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (versionUrl == null) return null;
|
||||||
|
|
||||||
|
// Step 2: Version JSON (获取 assetIndex url)
|
||||||
|
req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(versionUrl))
|
||||||
|
.timeout(Duration.ofSeconds(10))
|
||||||
|
.header("User-Agent", "Sdf1Login/1.0")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() != 200) return null;
|
||||||
|
|
||||||
|
JsonObject versionJson = gson.fromJson(resp.body(), JsonObject.class);
|
||||||
|
JsonObject assetIndex = versionJson.getAsJsonObject("assetIndex");
|
||||||
|
if (assetIndex == null) return null;
|
||||||
|
String assetIndexUrl = assetIndex.get("url").getAsString();
|
||||||
|
|
||||||
|
// Step 3: Asset index (定位 zh_cn.json 的 hash)
|
||||||
|
req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(assetIndexUrl))
|
||||||
|
.timeout(Duration.ofSeconds(10))
|
||||||
|
.header("User-Agent", "Sdf1Login/1.0")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() != 200) return null;
|
||||||
|
|
||||||
|
JsonObject assets = gson.fromJson(resp.body(), JsonObject.class);
|
||||||
|
JsonObject objects = assets.getAsJsonObject("objects");
|
||||||
|
if (objects == null) return null;
|
||||||
|
|
||||||
|
JsonObject langEntry = objects.getAsJsonObject("minecraft/lang/zh_cn.json");
|
||||||
|
if (langEntry == null) return null;
|
||||||
|
String hash = langEntry.get("hash").getAsString();
|
||||||
|
if (hash == null || hash.length() < 2) return null;
|
||||||
|
|
||||||
|
// Step 4: 下载 zh_cn.json
|
||||||
|
String langUrl = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
|
||||||
|
req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(langUrl))
|
||||||
|
.timeout(Duration.ofSeconds(15))
|
||||||
|
.header("User-Agent", "Sdf1Login/1.0")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
return resp.body();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 Bukkit API 获取纯版本号,如 "1.21.4" */
|
||||||
|
private static String getMinecraftVersion() {
|
||||||
|
String ver = Bukkit.getBukkitVersion();
|
||||||
|
if (ver == null) return null;
|
||||||
|
int dash = ver.indexOf('-');
|
||||||
|
return dash > 0 ? ver.substring(0, dash) : ver;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,10 +77,21 @@ public class OrderManager implements Listener {
|
|||||||
public static class OrderItem {
|
public static class OrderItem {
|
||||||
public String name; public String mat;
|
public String name; public String mat;
|
||||||
public int originalPrice; public int finalPrice; public int qty;
|
public int originalPrice; public int finalPrice; public int qty;
|
||||||
|
public String nbt; // 物品NBT快照(Base64 of ItemStack.serializeAsBytes),退款时用于精确匹配含NBT商品;null/空=无NBT或旧数据
|
||||||
public OrderItem(String n,String m,int op,int fp,int q){
|
public OrderItem(String n,String m,int op,int fp,int q){
|
||||||
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;}
|
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;nbt=null;}
|
||||||
|
public OrderItem(String n,String m,int op,int fp,int q,String nbtB64){
|
||||||
|
name=n;mat=m;originalPrice=op;finalPrice=fp;qty=q;nbt=nbtB64;}
|
||||||
public int subtotal(){return finalPrice*qty;}
|
public int subtotal(){return finalPrice*qty;}
|
||||||
public int originalSubtotal(){return originalPrice*qty;}
|
public int originalSubtotal(){return originalPrice*qty;}
|
||||||
|
/** 还原下单时的真实物品(含NBT),用于退款时精确匹配;无快照或还原失败返回null */
|
||||||
|
public org.bukkit.inventory.ItemStack toTemplateStack() {
|
||||||
|
if (nbt == null || nbt.isEmpty()) return null;
|
||||||
|
try {
|
||||||
|
byte[] b = java.util.Base64.getDecoder().decode(nbt);
|
||||||
|
return org.bukkit.inventory.ItemStack.deserializeBytes(b);
|
||||||
|
} catch (Exception e) { return null; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public static class OrderRecord {
|
public static class OrderRecord {
|
||||||
public long orderId; public UUID uuid; public String player;
|
public long orderId; public UUID uuid; public String player;
|
||||||
@@ -384,13 +395,31 @@ public class OrderManager implements Listener {
|
|||||||
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
|
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
/** 精确按材质+ItemMeta(NBT)计数,用于含NBT商品(如附魔书)退款校验 */
|
||||||
|
private int countSimilarItems(Player p, org.bukkit.inventory.ItemStack tmpl) {
|
||||||
|
if (tmpl == null) return 0;
|
||||||
|
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.isSimilar(tmpl)) c += is.getAmount();
|
||||||
|
return c;
|
||||||
|
}
|
||||||
private int countShulkers(Player p, String matName) {
|
private int countShulkers(Player p, String matName) {
|
||||||
Material m = Material.matchMaterial(matName); if (m == null) return 0;
|
Material m = Material.matchMaterial(matName); if (m == null) return 0;
|
||||||
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
|
int c = 0; for (ItemStack is : p.getInventory().getContents()) if (is != null && is.getType() == m) c += is.getAmount();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
private void removeItems(Player p, String matName, int amt) {
|
/**
|
||||||
Material m = Material.matchMaterial(matName); if (m != null) p.getInventory().removeItem(new ItemStack(m, amt));
|
* 收走订单物品:优先按记录中的NBT快照精确匹配(材质+ItemMeta一致),
|
||||||
|
* 确保含NBT商品(附魔书等)既退钱又收回正确的实物,而非"仅退款"。
|
||||||
|
* 无快照(旧订单/普通物品)则按材质兜底。
|
||||||
|
*/
|
||||||
|
private void removeItems(Player p, OrderItem it) {
|
||||||
|
org.bukkit.inventory.ItemStack tmpl = it.toTemplateStack();
|
||||||
|
if (tmpl != null) {
|
||||||
|
tmpl.setAmount(it.qty);
|
||||||
|
p.getInventory().removeItem(tmpl); // removeItem 按 isSimilar 匹配,精确收走含NBT实物
|
||||||
|
} else {
|
||||||
|
Material m = Material.matchMaterial(it.mat);
|
||||||
|
if (m != null) p.getInventory().removeItem(new ItemStack(m, it.qty));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private boolean removeOneShulker(Player p,
|
private boolean removeOneShulker(Player p,
|
||||||
String materialName) {
|
String materialName) {
|
||||||
@@ -448,9 +477,10 @@ public class OrderManager implements Listener {
|
|||||||
}
|
}
|
||||||
return "§c对应的潜影盒已不在背包中";
|
return "§c对应的潜影盒已不在背包中";
|
||||||
} else {
|
} else {
|
||||||
// ★ 散装:检测物品数量
|
// ★ 散装:检测物品数量(含NBT商品按材质+ItemMeta精确校验,必须"有货且有正确NBT")
|
||||||
for (OrderItem it : r.items) {
|
for (OrderItem it : r.items) {
|
||||||
int have = countDirectItems(p, it.mat);
|
org.bukkit.inventory.ItemStack tmpl = it.toTemplateStack();
|
||||||
|
int have = (tmpl != null) ? countSimilarItems(p, tmpl) : countDirectItems(p, it.mat);
|
||||||
if (have < it.qty)
|
if (have < it.qty)
|
||||||
return "§c" + it.name + "不足(有"
|
return "§c" + it.name + "不足(有"
|
||||||
+ have + "/" + it.qty + ")";
|
+ have + "/" + it.qty + ")";
|
||||||
@@ -685,7 +715,7 @@ public class OrderManager implements Listener {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (OrderItem it : r.items)
|
for (OrderItem it : r.items)
|
||||||
removeItems(p, it.mat, it.qty);
|
removeItems(p, it);
|
||||||
}
|
}
|
||||||
// 退钱
|
// 退钱
|
||||||
plugin.getBonds().addBonds(
|
plugin.getBonds().addBonds(
|
||||||
@@ -821,13 +851,38 @@ public class OrderManager implements Listener {
|
|||||||
p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚");
|
p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (r.status == 0 || r.status == 1) {
|
if (r.status == 0) {
|
||||||
|
// ★ 管理员全额退款:同步收回物品(玩家在线时),避免"只退钱不退货"
|
||||||
|
Player target = Bukkit.getPlayer(r.uuid);
|
||||||
|
if (target != null && target.isOnline()) {
|
||||||
|
String pt = r.packType != null ? r.packType : "none";
|
||||||
|
boolean isShulker = pt.equals("custom")
|
||||||
|
|| pt.equals("default") || pt.equals("shulker");
|
||||||
|
if (isShulker) {
|
||||||
|
for (int i = 0; i < 36; i++) {
|
||||||
|
ItemStack is = target.getInventory().getItem(i);
|
||||||
|
if (is == null) continue;
|
||||||
|
if (!is.getType().name().contains("SHULKER")) continue;
|
||||||
|
ItemMeta im = is.getItemMeta();
|
||||||
|
boolean tagged = false;
|
||||||
|
if (im != null && im.getLore() != null) {
|
||||||
|
for (String line : im.getLore()) {
|
||||||
|
if (line.equals("§8#" + r.orderId)) { tagged = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagged) { target.getInventory().clear(i); break; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (OrderItem it : r.items) removeItems(target, it);
|
||||||
|
}
|
||||||
|
}
|
||||||
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
||||||
r.status = 2;
|
r.status = 2;
|
||||||
r.refundedAmount = r.totalPaid;
|
r.refundedAmount = r.totalPaid;
|
||||||
|
r.refundType = "auto_return";
|
||||||
saveOrders();
|
saveOrders();
|
||||||
p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚");
|
p.sendMessage("§a全额退款(含退货) §e" + r.player + " §6" + r.totalPaid + "枚");
|
||||||
notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚");
|
notifyPlayer(r.uuid, "§a[订单] 退款退货成功,§6" + r.totalPaid + "枚已退还");
|
||||||
openAdminOrders(p, tu, page);
|
openAdminOrders(p, tu, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -932,7 +987,7 @@ public class OrderManager implements Listener {
|
|||||||
Player target = Bukkit.getPlayer(r.uuid);
|
Player target = Bukkit.getPlayer(r.uuid);
|
||||||
if (target != null && target.isOnline()) {
|
if (target != null && target.isOnline()) {
|
||||||
for (OrderItem it : r.items)
|
for (OrderItem it : r.items)
|
||||||
removeItems(target, it.mat, it.qty);
|
removeItems(target, it);
|
||||||
}
|
}
|
||||||
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
||||||
r.status = 2;
|
r.status = 2;
|
||||||
@@ -1037,7 +1092,7 @@ public class OrderManager implements Listener {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (OrderItem it : r.items)
|
for (OrderItem it : r.items)
|
||||||
removeItems(p, it.mat, it.qty);
|
removeItems(p, it);
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
||||||
@@ -1280,7 +1335,7 @@ public class OrderManager implements Listener {
|
|||||||
case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break;
|
case "packFee": cur.packFee = Integer.parseInt(v); break; case "packType": cur.packType = v; break; case "packColor": cur.packColor = v; break;
|
||||||
case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break;
|
case "status": cur.status = Integer.parseInt(v); break; case "refundedAmount": cur.refundedAmount = Integer.parseInt(v); break;
|
||||||
case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break;
|
case "printTime": cur.printTime = Long.parseLong(v); break; case "refundType": cur.refundType = v; break;
|
||||||
case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) cur.items.add(new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4]))); break; }
|
case "item": String[] pp = v.split("\\|"); if (pp.length >= 5) { OrderItem oi = new OrderItem(pp[0], pp[1], Integer.parseInt(pp[2]), Integer.parseInt(pp[3]), Integer.parseInt(pp[4])); if (pp.length >= 6) oi.nbt = pp[5]; cur.items.add(oi); } break; }
|
||||||
} else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; }
|
} else if (line.isEmpty() && cur != null && cur.uuid != null) { orders.add(cur); cur = null; }
|
||||||
} if (cur != null && cur.uuid != null) orders.add(cur); br.close();
|
} if (cur != null && cur.uuid != null) orders.add(cur); br.close();
|
||||||
} catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); }
|
} catch (Exception e) { plugin.getLogger().warning("[Order]加载失败: " + e.getMessage()); }
|
||||||
@@ -1296,7 +1351,7 @@ public class OrderManager implements Listener {
|
|||||||
pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor));
|
pw.println("packType=" + nn(r.packType)); pw.println("packColor=" + nn(r.packColor));
|
||||||
pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount);
|
pw.println("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount);
|
||||||
pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType));
|
pw.println("printTime=" + r.printTime); pw.println("refundType=" + nn(r.refundType));
|
||||||
for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty);
|
for (OrderItem it : r.items) pw.println("item=" + it.name + "|" + it.mat + "|" + it.originalPrice + "|" + it.finalPrice + "|" + it.qty + "|" + (it.nbt == null ? "" : it.nbt));
|
||||||
} } pw.flush(); pw.close();
|
} } pw.flush(); pw.close();
|
||||||
} catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); }
|
} catch (Exception e) { plugin.getLogger().warning("[Order]保存失败: " + e.getMessage()); }
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -713,8 +713,10 @@ public class PVPManager implements Listener {
|
|||||||
instanceof Player)) return;
|
instanceof Player)) return;
|
||||||
Player victim =
|
Player victim =
|
||||||
(Player) event.getEntity();
|
(Player) event.getEntity();
|
||||||
|
org.bukkit.entity.Entity kc =
|
||||||
|
event.getDamageSource().getCausingEntity();
|
||||||
Player killer =
|
Player killer =
|
||||||
victim.getKiller();
|
(kc instanceof Player) ? (Player) kc : null;
|
||||||
if (killer == null) return;
|
if (killer == null) return;
|
||||||
if (killer.equals(victim)) return;
|
if (killer.equals(victim)) return;
|
||||||
|
|
||||||
@@ -1112,9 +1114,22 @@ public class PVPManager implements Listener {
|
|||||||
case "tempban":
|
case "tempban":
|
||||||
return handleTempBan(
|
return handleTempBan(
|
||||||
p, args);
|
p, args);
|
||||||
|
case "join":
|
||||||
|
case "arena":
|
||||||
|
plugin.getPVPArenaManager().joinArena(p);
|
||||||
|
return true;
|
||||||
|
case "leave":
|
||||||
|
case "exit":
|
||||||
|
plugin.leavePVP(p);
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return showStats(p,
|
showStats(p, p.getName());
|
||||||
p.getName());
|
p.sendMessage("§7——— PVP指令 ———");
|
||||||
|
p.sendMessage("§e/pvp join §7进入PVP竞技场");
|
||||||
|
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
|
||||||
|
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
|
||||||
|
p.sendMessage("§e/pvp list §7查看PVP区域列表");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1335,9 +1350,28 @@ public class PVPManager implements Listener {
|
|||||||
if (region == null) {
|
if (region == null) {
|
||||||
p.sendMessage("§6§l" + target
|
p.sendMessage("§6§l" + target
|
||||||
+ " 的PVP战绩:");
|
+ " 的PVP战绩:");
|
||||||
|
|
||||||
|
// ★ PVP世界战绩(从PvPArenaManager获取)
|
||||||
|
int pvpWorldKills = 0;
|
||||||
|
int pvpWorldDeaths = 0;
|
||||||
|
if (plugin.getPVPArenaManager() != null) {
|
||||||
|
Map<String, Integer> wk = plugin.getPVPArenaManager().getPVPWorldKills();
|
||||||
|
Map<String, Integer> wd = plugin.getPVPArenaManager().getPVPWorldDeaths();
|
||||||
|
pvpWorldKills = wk.getOrDefault(target, 0);
|
||||||
|
pvpWorldDeaths = wd.getOrDefault(target, 0);
|
||||||
|
}
|
||||||
|
if (pvpWorldKills > 0 || pvpWorldDeaths > 0) {
|
||||||
|
double pvpKd = pvpWorldDeaths > 0
|
||||||
|
? (double) pvpWorldKills / pvpWorldDeaths : pvpWorldKills;
|
||||||
|
p.sendMessage("§e- §fPVP世界 §a击杀:" + pvpWorldKills
|
||||||
|
+ " §c死亡:" + pvpWorldDeaths
|
||||||
|
+ " §eKD:" + String.format("%.2f", pvpKd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PVP区域战绩
|
||||||
List<Map<String, Object>> all =
|
List<Map<String, Object>> all =
|
||||||
getAllRegions();
|
getAllRegions();
|
||||||
boolean has = false;
|
boolean has = pvpWorldKills > 0 || pvpWorldDeaths > 0;
|
||||||
for (Map<String, Object> row :
|
for (Map<String, Object> row :
|
||||||
all) {
|
all) {
|
||||||
String rn = (String) row
|
String rn = (String) row
|
||||||
@@ -1355,6 +1389,21 @@ public class PVPManager implements Listener {
|
|||||||
has = true;
|
has = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ★ 总计
|
||||||
|
int totalKills = pvpWorldKills;
|
||||||
|
int totalDeaths = pvpWorldDeaths;
|
||||||
|
for (Map<String, Object> row : all) {
|
||||||
|
String rn = (String) row.get("name");
|
||||||
|
totalKills += getKills(target, rn);
|
||||||
|
totalDeaths += getDeaths(target, rn);
|
||||||
|
}
|
||||||
|
if (totalKills > 0 || totalDeaths > 0) {
|
||||||
|
double totalKd = totalDeaths > 0
|
||||||
|
? (double) totalKills / totalDeaths : totalKills;
|
||||||
|
p.sendMessage("§6§l- §f总计 §a击杀:" + totalKills
|
||||||
|
+ " §c死亡:" + totalDeaths
|
||||||
|
+ " §eKD:" + String.format("%.2f", totalKd));
|
||||||
|
}
|
||||||
if (!has)
|
if (!has)
|
||||||
p.sendMessage(
|
p.sendMessage(
|
||||||
"§7暂无记录");
|
"§7暂无记录");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -102,7 +102,14 @@ public class QuickBack implements Listener, CommandExecutor, TabCompleter {
|
|||||||
player.sendMessage("§c位置世界信息无效");
|
player.sendMessage("§c位置世界信息无效");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 禁止/back传送至PVP测试世界
|
||||||
|
String worldName = loc.getWorld().getName();
|
||||||
|
if (worldName != null && worldName.startsWith("pvp_test_")) {
|
||||||
|
player.sendMessage("§c§l[快速返回] §c无法返回PVP测试世界,请使用 /pvp test 进入测试场选装");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Location dest = loc.clone().add(0.5, 0, 0.5); // 对齐格子中心
|
Location dest = loc.clone().add(0.5, 0, 0.5); // 对齐格子中心
|
||||||
player.teleport(dest);
|
player.teleport(dest);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package Sdf1_login;
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import org.bukkit.BanList;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
* 风控管理器:密码错误阈值封禁 + 报警邮件
|
||||||
|
*
|
||||||
|
* 功能1:密码错误达到阈值 → 自动封禁账号N分钟 + 推送报警邮件
|
||||||
|
* 功能2:封禁期间拒绝所有登录尝试
|
||||||
*/
|
*/
|
||||||
public class RiskControlManager {
|
public class RiskControlManager {
|
||||||
|
|
||||||
@@ -17,232 +22,261 @@ public class RiskControlManager {
|
|||||||
private final Map<String, Integer> failAttempts =
|
private final Map<String, Integer> failAttempts =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// 每个IP -> 关联的可疑玩家列表
|
// 每个玩家名 -> 封禁到期时间戳(毫秒)
|
||||||
private final Map<String, Set<String>> ipSuspicious =
|
private final Map<String, Long> banUntil =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// 每个玩家 -> 上次验证时间(用于冷却)
|
|
||||||
private final Map<String, Long> verifyCooldown =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// 配置项
|
|
||||||
private static final int MAX_FAIL_ATTEMPTS = 5;
|
|
||||||
private static final int MAX_ACCOUNTS_PER_IP = 5;
|
|
||||||
private static final long COOLDOWN_MS = 60000L;
|
|
||||||
|
|
||||||
public RiskControlManager(Main plugin) {
|
public RiskControlManager(Main plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录一次失败的登录尝试
|
* 获取当前密码错误阈值(从配置读取)
|
||||||
*
|
|
||||||
* @param playerName 玩家名
|
|
||||||
* @return 剩余尝试次数
|
|
||||||
*/
|
*/
|
||||||
public int recordFailAttempt(String playerName) {
|
private int getMaxFailAttempts() {
|
||||||
int current = failAttempts
|
return plugin.getConfig2().passwordFailThreshold;
|
||||||
.getOrDefault(playerName, 0) + 1;
|
|
||||||
failAttempts.put(playerName, current);
|
|
||||||
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置指定玩家的失败计数(登录成功时调用)
|
* 获取封禁时长(毫秒)(从配置读取)
|
||||||
*/
|
*/
|
||||||
public void resetFailAttempts(String playerName) {
|
private long getBanDurationMs() {
|
||||||
|
return (long) plugin.getConfig2().passwordBanDuration * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 封禁检查 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否已被封禁
|
||||||
|
* @return true=已被封禁(应拒绝登录)
|
||||||
|
*/
|
||||||
|
public boolean isBanned(String playerName) {
|
||||||
|
Long until = banUntil.get(playerName);
|
||||||
|
if (until == null) return false;
|
||||||
|
if (System.currentTimeMillis() < until) {
|
||||||
|
return true; // 仍在封禁期内
|
||||||
|
}
|
||||||
|
// 封禁已过期,自动解除
|
||||||
|
banUntil.remove(playerName);
|
||||||
failAttempts.remove(playerName);
|
failAttempts.remove(playerName);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定玩家的失败次数
|
|
||||||
*/
|
|
||||||
public int getFailAttempts(String playerName) {
|
|
||||||
return failAttempts
|
|
||||||
.getOrDefault(playerName, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断玩家是否已被锁定(失败次数达到上限)
|
|
||||||
*/
|
|
||||||
public boolean isLocked(String playerName) {
|
|
||||||
return failAttempts
|
|
||||||
.getOrDefault(playerName, 0)
|
|
||||||
>= MAX_FAIL_ATTEMPTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查IP是否达到注册上限
|
|
||||||
* 直接查数据库,不依赖 DatabaseManager 的方法
|
|
||||||
*/
|
|
||||||
public boolean canRegisterFromIP(String ip) {
|
|
||||||
if (ip == null || ip.isEmpty()) return true;
|
|
||||||
try {
|
|
||||||
java.sql.Connection conn =
|
|
||||||
plugin.getDb().getDb();
|
|
||||||
if (conn == null) return true;
|
|
||||||
java.sql.PreparedStatement ps =
|
|
||||||
conn.prepareStatement(
|
|
||||||
"SELECT COUNT(*) FROM users "
|
|
||||||
+ "WHERE ip_address = ?");
|
|
||||||
ps.setString(1, ip);
|
|
||||||
java.sql.ResultSet rs = ps.executeQuery();
|
|
||||||
int count = 0;
|
|
||||||
if (rs.next()) count = rs.getInt(1);
|
|
||||||
rs.close();
|
|
||||||
ps.close();
|
|
||||||
return count < 5;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录IP关联的可疑行为
|
|
||||||
*/
|
|
||||||
public void recordSuspiciousIP(String ip,
|
|
||||||
String playerName) {
|
|
||||||
if (ip == null || ip.isEmpty()) return;
|
|
||||||
ipSuspicious
|
|
||||||
.computeIfAbsent(ip,
|
|
||||||
k -> ConcurrentHashMap.newKeySet())
|
|
||||||
.add(playerName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某IP关联的所有可疑玩家
|
|
||||||
*/
|
|
||||||
public Set<String> getSuspiciousAccounts(String ip) {
|
|
||||||
return ipSuspicious
|
|
||||||
.getOrDefault(ip, Collections.emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否在验证冷却期内
|
|
||||||
*/
|
|
||||||
public boolean isOnCooldown(String playerName) {
|
|
||||||
Long last = verifyCooldown.get(playerName);
|
|
||||||
if (last == null) return false;
|
|
||||||
return System.currentTimeMillis() - last
|
|
||||||
< COOLDOWN_MS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置验证冷却时间
|
|
||||||
*/
|
|
||||||
public void setCooldown(String playerName) {
|
|
||||||
verifyCooldown.put(playerName,
|
|
||||||
System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当检测到可疑登录时触发报警
|
|
||||||
* - 全服广播
|
|
||||||
* - 有邮箱则发邮件
|
|
||||||
* - 记录到数据库
|
|
||||||
*/
|
|
||||||
public void triggerAlert(Player player,
|
|
||||||
String suspiciousIP) {
|
|
||||||
String name = player.getName();
|
|
||||||
|
|
||||||
// 记录到数据库
|
|
||||||
plugin.getDb().recordSecurityAlert(
|
|
||||||
name, suspiciousIP);
|
|
||||||
|
|
||||||
// 记录IP可疑行为
|
|
||||||
recordSuspiciousIP(suspiciousIP, name);
|
|
||||||
|
|
||||||
// 全服广播
|
|
||||||
String serverMsg = "§c§l[安全警告] §e" + name
|
|
||||||
+ " §f的账号疑似遭到 §c" + suspiciousIP
|
|
||||||
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
|
|
||||||
Bukkit.broadcastMessage(serverMsg);
|
|
||||||
|
|
||||||
// 有邮箱则发邮件
|
|
||||||
String emailAddr = (String) plugin.getDb()
|
|
||||||
.getField(name, "email");
|
|
||||||
if (emailAddr != null
|
|
||||||
&& !emailAddr.isEmpty()) {
|
|
||||||
String subject =
|
|
||||||
"[安全警告] 您的账号疑似遭到盗号尝试";
|
|
||||||
String body = "尊敬的玩家 " + name
|
|
||||||
+ ":\n\n"
|
|
||||||
+ "您的账号在 "
|
|
||||||
+ new java.text.SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd HH:mm:ss")
|
|
||||||
.format(new java.util.Date())
|
|
||||||
+ " 疑似遭到 IP: " + suspiciousIP
|
|
||||||
+ " 的盗号尝试。\n\n"
|
|
||||||
+ "建议您:\n"
|
|
||||||
+ "1. 立即修改密码"
|
|
||||||
+ "(使用 /sdf1_login pw)\n"
|
|
||||||
+ "2. 如非本人操作,"
|
|
||||||
+ "请联系管理员处理\n\n"
|
|
||||||
+ "--Sdf1_login 安全系统";
|
|
||||||
plugin.getEmail().sendTempPassword(
|
|
||||||
emailAddr, name, body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理登录失败时的风控逻辑
|
|
||||||
*
|
|
||||||
* @param player 玩家
|
|
||||||
* @param suspiciousIP 可疑来源IP
|
|
||||||
* @return true=已被锁定(应踢出), false=仍可继续尝试
|
|
||||||
*/
|
|
||||||
public boolean handleLoginFailure(Player player,
|
|
||||||
String suspiciousIP) {
|
|
||||||
String name = player.getName();
|
|
||||||
int remaining = recordFailAttempt(name);
|
|
||||||
|
|
||||||
if (remaining <= 0) {
|
|
||||||
// 达到上限,触发报警
|
|
||||||
triggerAlert(player, suspiciousIP);
|
|
||||||
player.kickPlayer(
|
|
||||||
"§c§l[风控] §f登录失败次数过多,"
|
|
||||||
+ " 账号已被临时锁定, 请联系管理员");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未达上限,提示剩余次数
|
|
||||||
player.sendMessage(
|
|
||||||
"§c§l[风控] §f密码错误! "
|
|
||||||
+ "剩余" + remaining + "次机会");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家剩余封禁时间(秒),0=未封禁
|
||||||
|
*/
|
||||||
|
public int getBanRemainingSeconds(String playerName) {
|
||||||
|
Long until = banUntil.get(playerName);
|
||||||
|
if (until == null) return 0;
|
||||||
|
long remaining = until - System.currentTimeMillis();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
banUntil.remove(playerName);
|
||||||
|
failAttempts.remove(playerName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) (remaining / 1000) + 1; // 向上取整
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 失败记录 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录一次失败的登录尝试,判断是否触发封禁
|
||||||
|
*
|
||||||
|
* @param player 登录失败的玩家
|
||||||
|
* @return 剩余尝试次数(-1=已被封禁)
|
||||||
|
*/
|
||||||
|
public int recordFailAttempt(Player player) {
|
||||||
|
String name = player.getName();
|
||||||
|
int maxAttempts = getMaxFailAttempts();
|
||||||
|
|
||||||
|
// 已被封禁,直接返回
|
||||||
|
if (isBanned(name)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = failAttempts
|
||||||
|
.getOrDefault(name, 0) + 1;
|
||||||
|
failAttempts.put(name, current);
|
||||||
|
|
||||||
|
int remaining = Math.max(0, maxAttempts - current);
|
||||||
|
|
||||||
|
// 达到阈值 → 触发封禁
|
||||||
|
if (current >= maxAttempts) {
|
||||||
|
triggerBan(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发封禁:Bukkit真实封禁 + 记录时间 + 发邮件 + 踢出
|
||||||
|
* 双层封禁:Bukkit ban阻止连接加入,内存计时器到期自动解封
|
||||||
|
*/
|
||||||
|
private void triggerBan(Player player) {
|
||||||
|
String name = player.getName();
|
||||||
|
long durationMs = getBanDurationMs();
|
||||||
|
long until = System.currentTimeMillis() + durationMs;
|
||||||
|
banUntil.put(name, until);
|
||||||
|
|
||||||
|
String ip = plugin.getPlayerIP(player);
|
||||||
|
int banMinutes = (int) (durationMs / 60000);
|
||||||
|
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[风控] 玩家 " + name
|
||||||
|
+ " 密码错误达到阈值("
|
||||||
|
+ getMaxFailAttempts() + "次)"
|
||||||
|
+ ",封禁 " + banMinutes + " 分钟");
|
||||||
|
|
||||||
|
// ★ 真实封禁:调用Bukkit BanList API阻止玩家重新加入服务器
|
||||||
|
String banReason = "§c§l[安全风控] 密码连续错误"
|
||||||
|
+ getMaxFailAttempts() + "次,"
|
||||||
|
+ "账号临时封禁" + banMinutes + "分钟";
|
||||||
|
try {
|
||||||
|
// 使用 BanList API 设置带过期时间的封禁
|
||||||
|
Date expiry = new Date(System.currentTimeMillis() + durationMs);
|
||||||
|
Bukkit.getBanList(BanList.Type.NAME).addBan(
|
||||||
|
name, banReason, expiry, "[Sdf1_login] 风控");
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[风控] 已通过Bukkit BanList封禁玩家 "
|
||||||
|
+ name + " (" + banMinutes + "分钟,到期: " + expiry + ")");
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[风控] BanList封禁失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
// 踢出玩家(无论BanList是否成功,立即踢出)
|
||||||
|
player.kickPlayer(banReason);
|
||||||
|
|
||||||
|
// 异步发送报警邮件
|
||||||
|
sendBanAlertEmail(name, ip, banMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送封禁报警邮件到玩家绑定的邮箱
|
||||||
|
* 未绑定邮箱则跳过
|
||||||
|
*/
|
||||||
|
private void sendBanAlertEmail(String playerName,
|
||||||
|
String ip, int banMinutes) {
|
||||||
|
String emailAddr = (String) plugin.getDb()
|
||||||
|
.getField(playerName, "email");
|
||||||
|
if (emailAddr == null || emailAddr.isEmpty()) {
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[风控] 玩家 " + playerName
|
||||||
|
+ " 未绑定邮箱,跳过报警邮件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String subject =
|
||||||
|
"[安全警告] 您的账号已被临时封禁";
|
||||||
|
String body = "尊敬的玩家 " + playerName + ":\n\n"
|
||||||
|
+ "您的账号在 "
|
||||||
|
+ new java.text.SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd HH:mm:ss")
|
||||||
|
.format(new java.util.Date())
|
||||||
|
+ " 因密码连续错误 "
|
||||||
|
+ getMaxFailAttempts() + " 次"
|
||||||
|
+ ",已被自动封禁 " + banMinutes + " 分钟。\n\n"
|
||||||
|
+ "来源IP: " + (ip != null ? ip : "未知") + "\n"
|
||||||
|
+ "封禁到期: "
|
||||||
|
+ new java.text.SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd HH:mm:ss")
|
||||||
|
.format(new java.util.Date(
|
||||||
|
System.currentTimeMillis()
|
||||||
|
+ (long) banMinutes * 60000L))
|
||||||
|
+ "\n\n"
|
||||||
|
+ "如果这是您本人操作,请等待封禁到期后重试。\n"
|
||||||
|
+ "如果非本人操作,建议您立即修改密码"
|
||||||
|
+ "(使用 /sdf1_login pw)。\n\n"
|
||||||
|
+ "--Sdf1_login 安全系统";
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
plugin.getEmail().sendBody(
|
||||||
|
emailAddr, subject, body);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[风控] 封禁报警邮件已发送到 "
|
||||||
|
+ emailAddr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning(
|
||||||
|
"[风控] 报警邮件发送失败: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 成功重置 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录成功后重置风控状态
|
* 登录成功后重置风控状态
|
||||||
*/
|
*/
|
||||||
public void onLoginSuccess(String playerName) {
|
public void onLoginSuccess(String playerName) {
|
||||||
resetFailAttempts(playerName);
|
failAttempts.remove(playerName);
|
||||||
|
banUntil.remove(playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 清理 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期的冷却记录(可由定时任务调用)
|
* 清理过期的封禁记录(可由定时任务调用)
|
||||||
|
* 同步解除过期玩家的Bukkit封禁
|
||||||
*/
|
*/
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
verifyCooldown.entrySet().removeIf(
|
// 收集过期的玩家名
|
||||||
e -> now - e.getValue() > COOLDOWN_MS * 10);
|
List<String> expired = new ArrayList<>();
|
||||||
|
banUntil.entrySet().removeIf(e -> {
|
||||||
|
if (e.getValue() < now) {
|
||||||
|
expired.add(e.getKey());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
// 同步解除Bukkit封禁
|
||||||
|
for (String name : expired) {
|
||||||
|
try {
|
||||||
|
Bukkit.getBanList(BanList.Type.NAME)
|
||||||
|
.pardon(name);
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[风控] 封禁到期,已解除Bukkit封禁: "
|
||||||
|
+ name);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
failAttempts.remove(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有玩家的失败尝试记录(调试用)
|
* 获取所有封禁记录(调试/管理用)
|
||||||
|
*/
|
||||||
|
public Map<String, Long> getAllBans() {
|
||||||
|
return Collections.unmodifiableMap(banUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有失败尝试记录(调试用)
|
||||||
*/
|
*/
|
||||||
public Map<String, Integer> getAllFailAttempts() {
|
public Map<String, Integer> getAllFailAttempts() {
|
||||||
return Collections.unmodifiableMap(failAttempts);
|
return Collections.unmodifiableMap(failAttempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动解除某玩家的封禁(管理员用)
|
||||||
|
*/
|
||||||
|
public void unban(String playerName) {
|
||||||
|
banUntil.remove(playerName);
|
||||||
|
failAttempts.remove(playerName);
|
||||||
|
// 同步解除Bukkit封禁
|
||||||
|
try {
|
||||||
|
Bukkit.getBanList(BanList.Type.NAME)
|
||||||
|
.pardon(playerName);
|
||||||
|
plugin.getLogger().info("[风控] 已解除Bukkit封禁: "
|
||||||
|
+ playerName);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有风控数据(管理员调试用)
|
* 清除所有风控数据(管理员调试用)
|
||||||
*/
|
*/
|
||||||
public void clearAll() {
|
public void clearAll() {
|
||||||
failAttempts.clear();
|
failAttempts.clear();
|
||||||
ipSuspicious.clear();
|
banUntil.clear();
|
||||||
verifyCooldown.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ public class ShopItem {
|
|||||||
private final String id;
|
private final String id;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
private final Material material;
|
private final Material material;
|
||||||
private final int buyPrice;
|
private int buyPrice;
|
||||||
private final int sellPrice;
|
private int sellPrice;
|
||||||
private int stock;
|
private int stock;
|
||||||
private int hourlySales;
|
private int hourlySales;
|
||||||
private int totalSales;
|
private int totalSales;
|
||||||
@@ -42,6 +42,8 @@ public class ShopItem {
|
|||||||
public void setStock(int s) { this.stock = s; }
|
public void setStock(int s) { this.stock = s; }
|
||||||
public void setHourlySales(int s) { this.hourlySales = s; }
|
public void setHourlySales(int s) { this.hourlySales = s; }
|
||||||
public void setTotalSales(int s) { this.totalSales = s; }
|
public void setTotalSales(int s) { this.totalSales = s; }
|
||||||
|
public void setBuyPrice(int p) { this.buyPrice = p; }
|
||||||
|
public void setSellPrice(int p) { this.sellPrice = p; }
|
||||||
public void addSales(int n) {
|
public void addSales(int n) {
|
||||||
this.hourlySales += n;
|
this.hourlySales += n;
|
||||||
this.totalSales += n;
|
this.totalSales += n;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,8 @@ import java.sql.PreparedStatement;
|
|||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.RuntimeMXBean;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,10 +50,9 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
// 请求过期时间 (请求ID/发送者→ 创建时间戳),配置化
|
// 请求过期时间 (请求ID/发送者→ 创建时间戳),配置化
|
||||||
private final Map<String, Long> teleportRequestTimes = new ConcurrentHashMap<>();
|
private final Map<String, Long> teleportRequestTimes = new ConcurrentHashMap<>();
|
||||||
|
// 请求类型(tpa/tpahere/tpaall)仅驻留内存,不落库;落库既画蛇添足又引入墙钟依赖
|
||||||
|
private final Map<String, String> teleportRequestTypes = new ConcurrentHashMap<>();
|
||||||
private volatile long teleportCooldownMs = 30_000L; // 可配置的冷却时间
|
private volatile long teleportCooldownMs = 30_000L; // 可配置的冷却时间
|
||||||
|
|
||||||
// ★ 已处理/已拒绝的请求(不能再使用)
|
|
||||||
private final Set<String> processedRequests = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
// ★ 管理员配置状态 (玩家名 → 配置状态)
|
// ★ 管理员配置状态 (玩家名 → 配置状态)
|
||||||
// 状态值: "valid" = 等待输入有效时间, "interval" = 等待输入间隔时间
|
// 状态值: "valid" = 等待输入有效时间, "interval" = 等待输入间隔时间
|
||||||
@@ -116,13 +117,23 @@ public class TeleportManager implements Listener {
|
|||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传送计时统一使用 JVM 运行时长(自启动以来的毫秒数),单调时钟,
|
||||||
|
* 不受系统墙钟(NTP 校准 / 手动改时间 / 时区切换)影响。
|
||||||
|
* tpa 只关心“请求发起后 N 秒过期”,不需要真实日期。
|
||||||
|
*/
|
||||||
|
private static final RuntimeMXBean RUNTIME_BEAN = ManagementFactory.getRuntimeMXBean();
|
||||||
|
private long nowUptime() {
|
||||||
|
return RUNTIME_BEAN.getUptime();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查传送请求是否过期(由配置决定)
|
* 检查传送请求是否过期(由配置决定)
|
||||||
*/
|
*/
|
||||||
private boolean isTeleportRequestExpired(String key) {
|
private boolean isTeleportRequestExpired(String key) {
|
||||||
Long createTime = teleportRequestTimes.get(key);
|
Long createTime = teleportRequestTimes.get(key);
|
||||||
if (createTime == null) return true;
|
if (createTime == null) return true;
|
||||||
long elapsed = (System.currentTimeMillis() - createTime) / 1000;
|
long elapsed = (nowUptime() - createTime) / 1000;
|
||||||
return elapsed > getTpRequestValidSeconds();
|
return elapsed > getTpRequestValidSeconds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,13 +142,13 @@ public class TeleportManager implements Listener {
|
|||||||
*/
|
*/
|
||||||
private void cleanupExpiredRequests(String playerName) {
|
private void cleanupExpiredRequests(String playerName) {
|
||||||
int validSec = getTpRequestValidSeconds();
|
int validSec = getTpRequestValidSeconds();
|
||||||
long now = System.currentTimeMillis();
|
long now = nowUptime();
|
||||||
|
|
||||||
// 清理过期的incoming
|
// 清理过期的incoming(key 与存储/校验统一为 sender:receiver)
|
||||||
Set<String> incoming = incomingRequests.get(playerName);
|
Set<String> incoming = incomingRequests.get(playerName);
|
||||||
if (incoming != null) {
|
if (incoming != null) {
|
||||||
incoming.removeIf(sender -> {
|
incoming.removeIf(sender -> {
|
||||||
String key = playerName + ":" + sender;
|
String key = sender + ":" + playerName;
|
||||||
Long t = teleportRequestTimes.get(key);
|
Long t = teleportRequestTimes.get(key);
|
||||||
return t != null && (now - t) > (validSec * 1000L);
|
return t != null && (now - t) > (validSec * 1000L);
|
||||||
});
|
});
|
||||||
@@ -160,7 +171,7 @@ public class TeleportManager implements Listener {
|
|||||||
private void expireOldRequests() {
|
private void expireOldRequests() {
|
||||||
if (!running.get()) return;
|
if (!running.get()) return;
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = nowUptime();
|
||||||
int validSec = getTpRequestValidSeconds();
|
int validSec = getTpRequestValidSeconds();
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
|
||||||
@@ -199,7 +210,8 @@ public class TeleportManager implements Listener {
|
|||||||
Iterator<String> senderIter = senders.iterator();
|
Iterator<String> senderIter = senders.iterator();
|
||||||
while (senderIter.hasNext()) {
|
while (senderIter.hasNext()) {
|
||||||
String sender = senderIter.next();
|
String sender = senderIter.next();
|
||||||
String key = receiver + ":" + sender;
|
// ★ key 与存储/校验统一为 sender:receiver(原 receiver:sender 永远取不到时间→接收方请求永不清空)
|
||||||
|
String key = sender + ":" + receiver;
|
||||||
Long t = teleportRequestTimes.get(key);
|
Long t = teleportRequestTimes.get(key);
|
||||||
if (t != null && (now - t) > (validSec * 1000L)) {
|
if (t != null && (now - t) > (validSec * 1000L)) {
|
||||||
senderIter.remove();
|
senderIter.remove();
|
||||||
@@ -222,6 +234,7 @@ public class TeleportManager implements Listener {
|
|||||||
Map.Entry<String, Long> entry = timeIter.next();
|
Map.Entry<String, Long> entry = timeIter.next();
|
||||||
Long t = entry.getValue();
|
Long t = entry.getValue();
|
||||||
if ((now - t) > (validSec * 1000L + 5000L)) { // 额外5秒宽容
|
if ((now - t) > (validSec * 1000L + 5000L)) { // 额外5秒宽容
|
||||||
|
teleportRequestTypes.remove(entry.getKey());
|
||||||
timeIter.remove();
|
timeIter.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +246,6 @@ public class TeleportManager implements Listener {
|
|||||||
private boolean isValidRequest(String receiver, String sender) {
|
private boolean isValidRequest(String receiver, String sender) {
|
||||||
String key = sender + ":" + receiver;
|
String key = sender + ":" + receiver;
|
||||||
|
|
||||||
// ★ 检查是否已处理过
|
|
||||||
if (processedRequests.contains(key)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> senders = incomingRequests.get(receiver);
|
Set<String> senders = incomingRequests.get(receiver);
|
||||||
if (senders == null || !senders.contains(sender)) {
|
if (senders == null || !senders.contains(sender)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -245,14 +253,6 @@ public class TeleportManager implements Listener {
|
|||||||
return !isTeleportRequestExpired(key);
|
return !isTeleportRequestExpired(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 标记请求为已处理(防止二次接受/拒绝)
|
|
||||||
*/
|
|
||||||
private void markRequestProcessed(String sender, String receiver) {
|
|
||||||
String key = sender + ":" + receiver;
|
|
||||||
processedRequests.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 判断玩家类型 ====================
|
// ==================== 判断玩家类型 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -621,7 +621,7 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
// 检查冷却
|
// 检查冷却
|
||||||
String sender = player.getName();
|
String sender = player.getName();
|
||||||
long now = System.currentTimeMillis();
|
long now = nowUptime();
|
||||||
Long lastTime = teleportCooldown.get(sender);
|
Long lastTime = teleportCooldown.get(sender);
|
||||||
if (lastTime != null && now - lastTime < teleportCooldownMs) {
|
if (lastTime != null && now - lastTime < teleportCooldownMs) {
|
||||||
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
|
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
|
||||||
@@ -631,14 +631,12 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
teleportCooldown.put(sender, now);
|
teleportCooldown.put(sender, now);
|
||||||
|
|
||||||
// 存储到数据库
|
// 添加到内存(请求仅驻留内存,配置项才落库)
|
||||||
saveTeleportRequest(sender, target.getName(), "tpa");
|
|
||||||
|
|
||||||
// 添加到内存
|
|
||||||
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(target.getName());
|
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(target.getName());
|
||||||
incomingRequests.computeIfAbsent(target.getName(), k -> ConcurrentHashMap.newKeySet()).add(sender);
|
incomingRequests.computeIfAbsent(target.getName(), k -> ConcurrentHashMap.newKeySet()).add(sender);
|
||||||
// 记录请求创建时间
|
// 记录请求创建时间
|
||||||
teleportRequestTimes.put(sender + ":" + target.getName(), now);
|
teleportRequestTimes.put(sender + ":" + target.getName(), now);
|
||||||
|
teleportRequestTypes.put(sender + ":" + target.getName(), "tpa");
|
||||||
|
|
||||||
// 通知发送者
|
// 通知发送者
|
||||||
if (isBedrockPlayer(player)) {
|
if (isBedrockPlayer(player)) {
|
||||||
@@ -688,8 +686,6 @@ public class TeleportManager implements Listener {
|
|||||||
// 清理请求
|
// 清理请求
|
||||||
removeIncomingRequest(targetName, senderName);
|
removeIncomingRequest(targetName, senderName);
|
||||||
removeOutgoingRequest(senderName, targetName);
|
removeOutgoingRequest(senderName, targetName);
|
||||||
// ★ 标记自动接受的请求为已处理
|
|
||||||
markRequestProcessed(senderName, targetName);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,67 +736,9 @@ public class TeleportManager implements Listener {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 内存中没有 → DB fallback
|
// 内存中没有请求:tpa 请求仅驻留内存,不存在即视为无请求(不查库)
|
||||||
boolean foundInDb = false;
|
player.sendMessage("§c[传送] 没有找到来自 §f" + targetName + " §c的传送请求");
|
||||||
Long dbTimestamp = null;
|
return true;
|
||||||
try {
|
|
||||||
PreparedStatement psFallback = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"SELECT timestamp FROM teleport_requests WHERE sender=? AND receiver=? ORDER BY timestamp DESC LIMIT 1");
|
|
||||||
psFallback.setString(1, targetName);
|
|
||||||
psFallback.setString(2, player.getName());
|
|
||||||
ResultSet rsFallback = psFallback.executeQuery();
|
|
||||||
if (rsFallback.next()) {
|
|
||||||
foundInDb = true;
|
|
||||||
dbTimestamp = rsFallback.getLong("timestamp");
|
|
||||||
}
|
|
||||||
rsFallback.close();
|
|
||||||
psFallback.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
plugin.getLogger().warning("[传送] DB回退查询失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundInDb) {
|
|
||||||
player.sendMessage("§c[传送] 没有找到来自 §f" + targetName + " §c的传送请求");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 关键修复:在DB路径也检查processedRequests,防止重复接受/拒绝
|
|
||||||
String key = targetName + ":" + player.getName();
|
|
||||||
if (processedRequests.contains(key)) {
|
|
||||||
player.sendMessage("§c[传送] 请求 §f" + targetName + " §c的传送请求已被处理");
|
|
||||||
try {
|
|
||||||
PreparedStatement psClean = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"DELETE FROM teleport_requests WHERE sender=? AND receiver=?");
|
|
||||||
psClean.setString(1, targetName);
|
|
||||||
psClean.setString(2, player.getName());
|
|
||||||
psClean.executeUpdate();
|
|
||||||
psClean.close();
|
|
||||||
} catch (SQLException ex) { /* ignore */ }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查数据库中请求是否过期
|
|
||||||
int validSec = getTpRequestValidSeconds();
|
|
||||||
long age = (System.currentTimeMillis() - dbTimestamp) / 1000;
|
|
||||||
if (age > validSec) {
|
|
||||||
player.sendMessage("§c[传送] 请求 §f" + targetName + " §c的传送请求已过期(" + (int)age + " 秒前)");
|
|
||||||
try {
|
|
||||||
PreparedStatement psClean = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"DELETE FROM teleport_requests WHERE sender=? AND receiver=?");
|
|
||||||
psClean.setString(1, targetName);
|
|
||||||
psClean.setString(2, player.getName());
|
|
||||||
psClean.executeUpdate();
|
|
||||||
psClean.close();
|
|
||||||
} catch (SQLException ex) { /* ignore */ }
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复内存状态
|
|
||||||
incomingRequests.computeIfAbsent(player.getName(), k -> ConcurrentHashMap.newKeySet()).add(targetName);
|
|
||||||
outgoingRequests.computeIfAbsent(targetName, k -> ConcurrentHashMap.newKeySet()).add(player.getName());
|
|
||||||
teleportRequestTimes.put(player.getName() + ":" + targetName, dbTimestamp);
|
|
||||||
teleportRequestTimes.put(targetName + ":" + player.getName(), dbTimestamp);
|
|
||||||
senders = incomingRequests.get(player.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查目标玩家是否在线
|
// 3. 检查目标玩家是否在线
|
||||||
@@ -812,22 +750,8 @@ public class TeleportManager implements Listener {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 查询请求类型
|
// 4. 查询请求类型(仅内存,配置项才落库)
|
||||||
String requestType = "tpa";
|
String requestType = teleportRequestTypes.getOrDefault(targetName + ":" + player.getName(), "tpa");
|
||||||
try {
|
|
||||||
PreparedStatement psCheck = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"SELECT type FROM teleport_requests WHERE sender=? AND receiver=? ORDER BY timestamp DESC LIMIT 1");
|
|
||||||
psCheck.setString(1, targetName);
|
|
||||||
psCheck.setString(2, player.getName());
|
|
||||||
ResultSet rsCheck = psCheck.executeQuery();
|
|
||||||
if (rsCheck.next()) {
|
|
||||||
requestType = rsCheck.getString("type");
|
|
||||||
}
|
|
||||||
rsCheck.close();
|
|
||||||
psCheck.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
plugin.getLogger().warning("[传送] 查询请求类型失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 执行传送
|
// 5. 执行传送
|
||||||
if ("tpaall".equals(requestType) || "tpahere".equals(requestType)) {
|
if ("tpaall".equals(requestType) || "tpahere".equals(requestType)) {
|
||||||
@@ -840,9 +764,6 @@ public class TeleportManager implements Listener {
|
|||||||
removeIncomingRequest(player.getName(), targetName);
|
removeIncomingRequest(player.getName(), targetName);
|
||||||
removeOutgoingRequest(targetName, player.getName());
|
removeOutgoingRequest(targetName, player.getName());
|
||||||
|
|
||||||
// ★ 标记请求已处理(防止二次使用)
|
|
||||||
markRequestProcessed(targetName, player.getName());
|
|
||||||
|
|
||||||
player.sendMessage("§a[传送] 已传送到 §f" + targetName + " §a身边");
|
player.sendMessage("§a[传送] 已传送到 §f" + targetName + " §a身边");
|
||||||
actualSender.sendMessage("§a[传送] §f" + player.getName() + " §a已接受请求");
|
actualSender.sendMessage("§a[传送] §f" + player.getName() + " §a已接受请求");
|
||||||
teleportCooldown.remove(actualSender.getName());
|
teleportCooldown.remove(actualSender.getName());
|
||||||
@@ -895,49 +816,9 @@ public class TeleportManager implements Listener {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// DB fallback: 查数据库找请求
|
// 内存中没有请求:tpa 请求仅驻留内存,不存在即视为无请求(不查库)
|
||||||
try {
|
player.sendMessage("§c[传送] 没有找到来自 §f" + targetName + " §c的传送请求");
|
||||||
PreparedStatement psFallback = plugin.getDb().getDb().prepareStatement(
|
return true;
|
||||||
"SELECT timestamp FROM teleport_requests WHERE sender=? AND receiver=? ORDER BY timestamp DESC LIMIT 1");
|
|
||||||
psFallback.setString(1, targetName);
|
|
||||||
psFallback.setString(2, player.getName());
|
|
||||||
ResultSet rsFallback = psFallback.executeQuery();
|
|
||||||
if (rsFallback.next()) {
|
|
||||||
long dbTimestamp = rsFallback.getLong("timestamp");
|
|
||||||
long validSec = getTpRequestValidSeconds();
|
|
||||||
long age = (System.currentTimeMillis() - dbTimestamp) / 1000;
|
|
||||||
if (age > validSec) {
|
|
||||||
rsFallback.close();
|
|
||||||
psFallback.close();
|
|
||||||
player.sendMessage("§c[传送] 请求 §f" + targetName + " §c的传送请求已过期(" + (int)age + " 秒前)");
|
|
||||||
try {
|
|
||||||
PreparedStatement psClean = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"DELETE FROM teleport_requests WHERE sender=? AND receiver=?");
|
|
||||||
psClean.setString(1, targetName);
|
|
||||||
psClean.setString(2, player.getName());
|
|
||||||
psClean.executeUpdate();
|
|
||||||
psClean.close();
|
|
||||||
} catch (SQLException ex) { /* ignore */ }
|
|
||||||
rsFallback.close();
|
|
||||||
psFallback.close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
rsFallback.close();
|
|
||||||
psFallback.close();
|
|
||||||
incomingRequests.computeIfAbsent(player.getName(), k -> ConcurrentHashMap.newKeySet()).add(targetName);
|
|
||||||
outgoingRequests.computeIfAbsent(targetName, k -> ConcurrentHashMap.newKeySet()).add(player.getName());
|
|
||||||
} else {
|
|
||||||
rsFallback.close();
|
|
||||||
psFallback.close();
|
|
||||||
player.sendMessage("§c[传送] 没有找到来自 §f" + targetName + " §c的传送请求");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
plugin.getLogger().warning("[传送] DB回退查询失败: " + e.getMessage());
|
|
||||||
player.sendMessage("§c[传送] 没有找到来自 §f" + targetName + " §c的传送请求");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
senders = incomingRequests.get(player.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Player actualSender = Bukkit.getServer().getPlayer(targetName);
|
Player actualSender = Bukkit.getServer().getPlayer(targetName);
|
||||||
@@ -948,9 +829,6 @@ public class TeleportManager implements Listener {
|
|||||||
removeIncomingRequest(player.getName(), targetName);
|
removeIncomingRequest(player.getName(), targetName);
|
||||||
removeOutgoingRequest(targetName, player.getName());
|
removeOutgoingRequest(targetName, player.getName());
|
||||||
|
|
||||||
// ★ 标记请求已拒绝(防止二次使用)
|
|
||||||
markRequestProcessed(targetName, player.getName());
|
|
||||||
|
|
||||||
player.sendMessage("§6[传送] 已拒绝来自 §f" + targetName + " §c的传送请求");
|
player.sendMessage("§6[传送] 已拒绝来自 §f" + targetName + " §c的传送请求");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -994,7 +872,11 @@ public class TeleportManager implements Listener {
|
|||||||
String key = args[0].toLowerCase();
|
String key = args[0].toLowerCase();
|
||||||
String val = args[1];
|
String val = args[1];
|
||||||
int seconds = plugin.getConfigMgr().parseIntFromString(val);
|
int seconds = plugin.getConfigMgr().parseIntFromString(val);
|
||||||
|
if (seconds < 30) {
|
||||||
|
player.sendMessage("§c[传送管理] 有效时间过小,已强制为 30 秒下限(防止请求秒过期)");
|
||||||
|
seconds = 30;
|
||||||
|
}
|
||||||
|
|
||||||
if ("valid".equals(key)) {
|
if ("valid".equals(key)) {
|
||||||
plugin.getConfigMgr().tpRequestValidSeconds = seconds;
|
plugin.getConfigMgr().tpRequestValidSeconds = seconds;
|
||||||
plugin.getConfigMgr().saveSettings();
|
plugin.getConfigMgr().saveSettings();
|
||||||
@@ -1156,7 +1038,7 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
// 检查冷却
|
// 检查冷却
|
||||||
String sender = player.getName();
|
String sender = player.getName();
|
||||||
long now = System.currentTimeMillis();
|
long now = nowUptime();
|
||||||
Long lastTime = teleportCooldown.get(sender);
|
Long lastTime = teleportCooldown.get(sender);
|
||||||
if (lastTime != null && now - lastTime < teleportCooldownMs) {
|
if (lastTime != null && now - lastTime < teleportCooldownMs) {
|
||||||
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
|
long remaining = (teleportCooldownMs - (now - lastTime)) / 1000;
|
||||||
@@ -1166,13 +1048,11 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
teleportCooldown.put(sender, now);
|
teleportCooldown.put(sender, now);
|
||||||
|
|
||||||
// 存储到数据库
|
// 添加到内存(请求仅驻留内存,配置项才落库)
|
||||||
saveTeleportRequest(sender, target.getName(), "tpahere");
|
|
||||||
|
|
||||||
// 添加到内存
|
|
||||||
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(target.getName());
|
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(target.getName());
|
||||||
incomingRequests.computeIfAbsent(target.getName(), k -> ConcurrentHashMap.newKeySet()).add(sender);
|
incomingRequests.computeIfAbsent(target.getName(), k -> ConcurrentHashMap.newKeySet()).add(sender);
|
||||||
teleportRequestTimes.put(sender + ":" + target.getName(), now);
|
teleportRequestTimes.put(sender + ":" + target.getName(), now);
|
||||||
|
teleportRequestTypes.put(sender + ":" + target.getName(), "tpahere");
|
||||||
|
|
||||||
// 通知发送者
|
// 通知发送者
|
||||||
if (isBedrockPlayer(player)) {
|
if (isBedrockPlayer(player)) {
|
||||||
@@ -1203,7 +1083,7 @@ public class TeleportManager implements Listener {
|
|||||||
private boolean handleTPAll(Player player) {
|
private boolean handleTPAll(Player player) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
String sender = player.getName();
|
String sender = player.getName();
|
||||||
long now = System.currentTimeMillis();
|
long now = nowUptime();
|
||||||
|
|
||||||
// 检查冷却
|
// 检查冷却
|
||||||
Long lastTime = teleportCooldown.get(sender);
|
Long lastTime = teleportCooldown.get(sender);
|
||||||
@@ -1220,13 +1100,11 @@ public class TeleportManager implements Listener {
|
|||||||
String name = p.getName();
|
String name = p.getName();
|
||||||
if (name.equalsIgnoreCase(sender)) continue;
|
if (name.equalsIgnoreCase(sender)) continue;
|
||||||
|
|
||||||
// 存储到数据库
|
// 添加到内存(请求仅驻留内存,配置项才落库)
|
||||||
saveTeleportRequest(sender, name, "tpaall");
|
|
||||||
|
|
||||||
// 添加到内存
|
|
||||||
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(name);
|
outgoingRequests.computeIfAbsent(sender, k -> ConcurrentHashMap.newKeySet()).add(name);
|
||||||
incomingRequests.computeIfAbsent(name, k -> ConcurrentHashMap.newKeySet()).add(sender);
|
incomingRequests.computeIfAbsent(name, k -> ConcurrentHashMap.newKeySet()).add(sender);
|
||||||
teleportRequestTimes.put(sender + ":" + name, now);
|
teleportRequestTimes.put(sender + ":" + name, now);
|
||||||
|
teleportRequestTypes.put(sender + ":" + name, "tpaall");
|
||||||
|
|
||||||
// 可点击通知
|
// 可点击通知
|
||||||
sendClickableRequestNotice(p, sender);
|
sendClickableRequestNotice(p, sender);
|
||||||
@@ -1846,23 +1724,6 @@ public class TeleportManager implements Listener {
|
|||||||
|
|
||||||
// ==================== 数据库操作 ====================
|
// ==================== 数据库操作 ====================
|
||||||
|
|
||||||
private void saveTeleportRequest(String sender, String receiver, String type) {
|
|
||||||
try {
|
|
||||||
PreparedStatement ps = plugin.getDb().getDb().prepareStatement(
|
|
||||||
"INSERT INTO teleport_requests " +
|
|
||||||
"(sender, receiver, type, timestamp) VALUES (?, ?, ?, ?)"
|
|
||||||
);
|
|
||||||
ps.setString(1, sender);
|
|
||||||
ps.setString(2, receiver);
|
|
||||||
ps.setString(3, type);
|
|
||||||
ps.setLong(4, System.currentTimeMillis());
|
|
||||||
ps.executeUpdate();
|
|
||||||
ps.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
// 静默失败
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 内存管理 ====================
|
// ==================== 内存管理 ====================
|
||||||
|
|
||||||
private void removeIncomingRequest(String receiver, String sender) {
|
private void removeIncomingRequest(String receiver, String sender) {
|
||||||
@@ -1873,6 +1734,7 @@ public class TeleportManager implements Listener {
|
|||||||
incomingRequests.remove(receiver);
|
incomingRequests.remove(receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teleportRequestTypes.remove(sender + ":" + receiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeOutgoingRequest(String sender, String receiver) {
|
private void removeOutgoingRequest(String sender, String receiver) {
|
||||||
@@ -1883,6 +1745,7 @@ public class TeleportManager implements Listener {
|
|||||||
outgoingRequests.remove(sender);
|
outgoingRequests.remove(sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teleportRequestTypes.remove(sender + ":" + receiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 辅助方法 ====================
|
// ==================== 辅助方法 ====================
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package Sdf1_login;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家正版/离线身份验证管理器
|
||||||
|
*
|
||||||
|
* 核心逻辑:在离线模式(online-mode=false)服务器中,所有玩家UUID都是离线格式,
|
||||||
|
* 无法通过UUID区分正版/盗版。因此:
|
||||||
|
* - UUID非离线格式 → 服务器在线模式,正版玩家 → autoLogin
|
||||||
|
* - UUID离线格式 → 一律视为非正版,需手动登录或OAuth验证
|
||||||
|
* - 正版玩家通过 /mslogin OAuth验证后,由Main.addVerifiedPremiumPlayer()标记,
|
||||||
|
* 在onJoin检查点0直接autoLogin,不经过此验证器
|
||||||
|
*/
|
||||||
public class VerificationManager {
|
public class VerificationManager {
|
||||||
|
|
||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
@@ -29,6 +33,11 @@ public class VerificationManager {
|
|||||||
return Bukkit.getServer().getOnlineMode();
|
return Bukkit.getServer().getOnlineMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步验证玩家身份
|
||||||
|
* 注意:此方法仅用于判断UUID格式,不执行自动登录
|
||||||
|
* 自动登录由onJoin中的检查点0/1/2控制
|
||||||
|
*/
|
||||||
public void verifyPremiumAsync(Player player,
|
public void verifyPremiumAsync(Player player,
|
||||||
Consumer<Boolean> callback) {
|
Consumer<Boolean> callback) {
|
||||||
Thread t = new Thread(() -> doVerify(player, callback),
|
Thread t = new Thread(() -> doVerify(player, callback),
|
||||||
@@ -50,49 +59,35 @@ public class VerificationManager {
|
|||||||
"[Sdf1_login] 当前UUID: " + uuid);
|
"[Sdf1_login] 当前UUID: " + uuid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UUID offlineUuid = UUID.nameUUIDFromBytes(
|
java.util.UUID offlineUuid = java.util.UUID.nameUUIDFromBytes(
|
||||||
("OfflinePlayer:" + name)
|
("OfflinePlayer:" + name)
|
||||||
.getBytes(StandardCharsets.UTF_8));
|
.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
String offlineStr = offlineUuid.toString();
|
|
||||||
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] 离线UUID: " + offlineStr);
|
|
||||||
|
|
||||||
boolean isOfflineUuid =
|
boolean isOfflineUuid =
|
||||||
player.getUniqueId().equals(offlineUuid);
|
player.getUniqueId().equals(offlineUuid);
|
||||||
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
"[Sdf1_login] 离线UUID: " + offlineUuid);
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] UUID匹配离线格式: "
|
"[Sdf1_login] UUID匹配离线格式: "
|
||||||
+ isOfflineUuid);
|
+ isOfflineUuid);
|
||||||
|
|
||||||
if (!isOfflineUuid) {
|
if (!isOfflineUuid) {
|
||||||
|
// UUID不是离线格式 → 服务器在线模式的正版玩家
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] 结论: 正版玩家");
|
"[Sdf1_login] 结论: 正版玩家(非离线UUID)");
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] ====== 验证结束 ======");
|
"[Sdf1_login] ====== 验证结束 ======");
|
||||||
callback.accept(true);
|
callback.accept(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 离线UUID → 无法通过UUID区分正版/盗版,一律视为非正版
|
||||||
|
// 正版玩家需通过 /mslogin OAuth验证 或 /login 手动登录
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] UUID为离线格式,"
|
"[Sdf1_login] 结论: 离线UUID玩家,需手动登录");
|
||||||
+ "查询Mojang API...");
|
plugin.getLogger().info(
|
||||||
UUID mojangUuid = fetchMojangUuid(name);
|
"[Sdf1_login] (正版玩家请使用 /mslogin 或 /正版 命令验证)");
|
||||||
|
|
||||||
if (mojangUuid != null) {
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] Mojang返回UUID: "
|
|
||||||
+ mojangUuid);
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] 结论: 盗版客户端"
|
|
||||||
+ "使用了正版名字");
|
|
||||||
} else {
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] Mojang无此名字");
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] 结论: 纯离线玩家");
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.getLogger().info(
|
plugin.getLogger().info(
|
||||||
"[Sdf1_login] ====== 验证结束 ======");
|
"[Sdf1_login] ====== 验证结束 ======");
|
||||||
callback.accept(false);
|
callback.accept(false);
|
||||||
@@ -104,55 +99,4 @@ public class VerificationManager {
|
|||||||
callback.accept(false);
|
callback.accept(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private UUID fetchMojangUuid(String name) {
|
|
||||||
HttpURLConnection conn = null;
|
|
||||||
try {
|
|
||||||
URL url = new URL(
|
|
||||||
"https://api.mojang.com/users/profiles/minecraft/"
|
|
||||||
+ name);
|
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setConnectTimeout(5000);
|
|
||||||
conn.setReadTimeout(5000);
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] Mojang API响应: " + code);
|
|
||||||
|
|
||||||
if (code == 200) {
|
|
||||||
BufferedReader reader =
|
|
||||||
new BufferedReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
conn.getInputStream(),
|
|
||||||
StandardCharsets.UTF_8));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
sb.append(line);
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
|
|
||||||
String json = sb.toString();
|
|
||||||
String uuidStr = json.replaceAll(
|
|
||||||
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
|
|
||||||
"$1");
|
|
||||||
plugin.getLogger().info(
|
|
||||||
"[Sdf1_login] Mojang原始UUID: "
|
|
||||||
+ uuidStr);
|
|
||||||
|
|
||||||
String formatted = uuidStr.replaceFirst(
|
|
||||||
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
|
|
||||||
"$1-$2-$3-$4-$5");
|
|
||||||
return UUID.fromString(formatted);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.getLogger().warning(
|
|
||||||
"[Sdf1_login] Mojang查询异常: "
|
|
||||||
+ e.getMessage());
|
|
||||||
} finally {
|
|
||||||
if (conn != null) conn.disconnect();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
name: Sdf1_login
|
name: Sdf1_login
|
||||||
version: 2.6
|
version: 2.8
|
||||||
main: Sdf1_login.Main
|
main: Sdf1_login.Main
|
||||||
api-version: '1.21'
|
api-version: '1.21'
|
||||||
commands:
|
commands:
|
||||||
@@ -102,6 +102,9 @@ commands:
|
|||||||
protect:
|
protect:
|
||||||
description: 区域保护
|
description: 区域保护
|
||||||
usage: /protect
|
usage: /protect
|
||||||
|
landrec:
|
||||||
|
description: 领地记录系统(回声碎片发放)
|
||||||
|
usage: /landrec give [玩家]
|
||||||
传送:
|
传送:
|
||||||
description: 传送至领地
|
description: 传送至领地
|
||||||
usage: /传送 <领地名>
|
usage: /传送 <领地名>
|
||||||
@@ -114,6 +117,12 @@ commands:
|
|||||||
控制台:
|
控制台:
|
||||||
description: 生成Web端登录令牌(中文版)
|
description: 生成Web端登录令牌(中文版)
|
||||||
usage: /控制台
|
usage: /控制台
|
||||||
|
mslogin:
|
||||||
|
description: 正版登录(Microsoft OAuth)
|
||||||
|
usage: /mslogin
|
||||||
|
正版:
|
||||||
|
description: 正版登录(中文版)
|
||||||
|
usage: /正版
|
||||||
tpa:
|
tpa:
|
||||||
description: tp传送指令
|
description: tp传送指令
|
||||||
usage: /tpa
|
usage: /tpa
|
||||||
@@ -153,3 +162,19 @@ commands:
|
|||||||
back:
|
back:
|
||||||
description: 快速返回最近记录点
|
description: 快速返回最近记录点
|
||||||
usage: /back
|
usage: /back
|
||||||
|
|
||||||
|
tempban:
|
||||||
|
description: 临时封禁玩家
|
||||||
|
usage: /tempban <玩家名/IP> <时长> [理由]
|
||||||
|
tempbanip:
|
||||||
|
description: 临时封禁IP或玩家
|
||||||
|
usage: /tempbanip <IP/玩家名> <时长> [理由]
|
||||||
|
banip:
|
||||||
|
description: 永久封禁IP或玩家
|
||||||
|
usage: /banip <IP/玩家名> [永久] [理由]
|
||||||
|
whois:
|
||||||
|
description: 查询玩家信息
|
||||||
|
usage: /whois [玩家名]
|
||||||
|
who:
|
||||||
|
description: 查看在线玩家列表
|
||||||
|
usage: /who
|
||||||
|
|||||||
Reference in New Issue
Block a user