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

|
||||
|
||||
|
||||
## 4. 下载地址
|
||||
- 官方文档:[https://wiki.ypshidifu.cn](https://wiki.ypshidifu.cn) (推荐)
|
||||
- GitHub:[https://github.com/ypsdf1/sdf1_login](https://github.com/ypsdf1/sdf1_login)
|
||||
- Gitee:[https://gitee.com/nihaoshidifu/sdf1_login](https://gitee.com/nihaoshidifu/sdf1_login)
|
||||
- gitcode: [https://gitcode.com/ypsdf1/Sdf1_login](https://gitcode.com/ypsdf1/Sdf1_login)
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.MilkBowl</groupId>
|
||||
<artifactId>VaultAPI</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
@@ -55,10 +49,9 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<source>25</source>
|
||||
<target>25</target>
|
||||
<release>25</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,26 @@ public class BondManager {
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
|
||||
// ===== 交易监听器 =====
|
||||
public interface TransactionListener {
|
||||
void onTransaction(String playerName, String type, int amount, String targetPlayer);
|
||||
}
|
||||
private final List<TransactionListener> transactionListeners = new ArrayList<>();
|
||||
|
||||
public void addTransactionListener(TransactionListener listener) {
|
||||
transactionListeners.add(listener);
|
||||
}
|
||||
|
||||
private void fireTransactionEvent(String playerName, String type, int amount, String targetPlayer) {
|
||||
for (TransactionListener listener : transactionListeners) {
|
||||
try {
|
||||
listener.onTransaction(playerName, type, amount, targetPlayer);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 交易类型常量 =====
|
||||
public static final String TX_ADMIN_GIVE = "admin_give";
|
||||
public static final String TX_ADMIN_DEDUCT = "admin_deduct";
|
||||
@@ -361,6 +381,8 @@ public class BondManager {
|
||||
ps.setLong(9, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
// 触发交易监听器
|
||||
fireTransactionEvent(player, type, amount, targetPlayer);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -697,4 +719,89 @@ public class BondManager {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 获取玩家最近1小时出售总额 =====
|
||||
public int getTodaySellTotal(String player) {
|
||||
return getSellTotalInWindow(player, System.currentTimeMillis() - 3600000L);
|
||||
}
|
||||
|
||||
private int getSellTotalInWindow(String player, long since) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM bond_transaction "
|
||||
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, since);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int total = 0;
|
||||
if (rs.next()) {
|
||||
total = rs.getInt(1);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return total;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家最近1小时出售窗口中最早交易的时间戳。
|
||||
* 返回0表示窗口内无交易。
|
||||
*/
|
||||
public long getEarliestSellTimeInWindow(String player) {
|
||||
long since = System.currentTimeMillis() - 3600000L;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT MIN(time) FROM bond_transaction "
|
||||
+ "WHERE player_name=? AND type='shop_sell' AND time >= ?");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, since);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
long earliest = 0;
|
||||
if (rs.next()) {
|
||||
earliest = rs.getLong(1);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return earliest;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间之后的所有交易记录(用于Web同步)
|
||||
*/
|
||||
public List<Map<String, Object>> getTransactionsAfterTime(long afterTime) {
|
||||
List<Map<String, Object>> transactions = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT id, player_name, type, amount, target_player, operator, reason, balance_before, balance_after, time "
|
||||
+ "FROM bond_transaction WHERE time > ? ORDER BY time ASC");
|
||||
ps.setLong(1, afterTime);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> tx = new LinkedHashMap<>();
|
||||
tx.put("id", rs.getInt("id"));
|
||||
tx.put("player_name", rs.getString("player_name"));
|
||||
tx.put("type", rs.getString("type"));
|
||||
tx.put("amount", rs.getInt("amount"));
|
||||
tx.put("target_player", rs.getString("target_player"));
|
||||
tx.put("operator", rs.getString("operator"));
|
||||
tx.put("reason", rs.getString("reason"));
|
||||
tx.put("balance_before", rs.getInt("balance_before"));
|
||||
tx.put("balance_after", rs.getInt("balance_after"));
|
||||
tx.put("time", rs.getLong("time"));
|
||||
transactions.add(tx);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,64 +170,29 @@ public class CDKManager {
|
||||
if ("points".equals(input.type)) {
|
||||
handlePointsInput(p, msg.trim(),
|
||||
input.context);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("gift_target".equals(input.type)) {
|
||||
// 赠予用户组:输入目标玩家名
|
||||
String targetName = msg.trim();
|
||||
if (!targetName.isEmpty()) {
|
||||
GUIManager guiMgr = plugin.getGui();
|
||||
if (guiMgr != null) {
|
||||
guiMgr.handleGiftTargetInput(p, targetName);
|
||||
}
|
||||
} else {
|
||||
p.sendMessage("§c请输入有效的玩家名");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEconInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
net.milkbowl.vault.economy.Economy econ
|
||||
= null;
|
||||
try {
|
||||
var reg = plugin.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault
|
||||
.economy
|
||||
.Economy.class);
|
||||
if (reg != null)
|
||||
econ = reg.getProvider();
|
||||
} catch (Exception ignored) {}
|
||||
if (econ == null) {
|
||||
admin.sendMessage("§c经济系统不可用");
|
||||
return;
|
||||
}
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String numStr =
|
||||
msg.substring(1).trim();
|
||||
double amount =
|
||||
Double.parseDouble(numStr);
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
if (msg.startsWith("-")) {
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp == null) {
|
||||
admin.sendMessage("§c" + target
|
||||
+ " 不在线");
|
||||
return;
|
||||
}
|
||||
if (!econ.has(tp, amount)) {
|
||||
admin.sendMessage("§c余额不足");
|
||||
return;
|
||||
}
|
||||
econ.withdrawPlayer(tp, amount);
|
||||
admin.sendMessage("§a扣款成功");
|
||||
} else {
|
||||
econ.depositPlayer(target, amount);
|
||||
admin.sendMessage("§a到账成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
// Economy已移除,所有经济操作改用债券系统
|
||||
admin.sendMessage("§c[经济操作] 功能已移除,请使用债券系统");
|
||||
}
|
||||
|
||||
private void handleBondInput(Player admin,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,9 +90,8 @@ public class ChatInputManager {
|
||||
name, "password_salt");
|
||||
String hash =
|
||||
PasswordUtils.hash(msg, salt);
|
||||
boolean mainOk =
|
||||
db.checkPassword(name, hash);
|
||||
if (mainOk) {
|
||||
String result = db.checkPasswordWithFallback(name, hash);
|
||||
if ("main".equals(result)) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "main";
|
||||
@@ -100,10 +99,7 @@ public class ChatInputManager {
|
||||
"§e请输入新密码:");
|
||||
return;
|
||||
}
|
||||
boolean tempOk =
|
||||
db.checkPasswordOrTemp(
|
||||
name, hash);
|
||||
if (tempOk) {
|
||||
if ("temp".equals(result)) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "temp";
|
||||
@@ -164,10 +160,21 @@ public class ChatInputManager {
|
||||
reset(p);
|
||||
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)
|
||||
db.getField(p.getName(),
|
||||
"email");
|
||||
String emailAddr = msg.trim();
|
||||
// 已绑定邮箱:验证登录密码
|
||||
if (oldEmail != null
|
||||
&& !oldEmail.isEmpty()) {
|
||||
|
||||
@@ -149,18 +149,8 @@ public class CommissionManager {
|
||||
originalAmount * rate;
|
||||
if (commission <= 0) return;
|
||||
|
||||
// 通过 Vault 发放
|
||||
var rsp = main.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault
|
||||
.economy.Economy.class);
|
||||
if (rsp == null) return;
|
||||
net.milkbowl.vault.economy.Economy econ =
|
||||
rsp.getProvider();
|
||||
if (econ == null) return;
|
||||
|
||||
econ.depositPlayer(inviter, commission);
|
||||
// Economy已移除,佣金提成功能不可用
|
||||
// 邀请人可通过债券系统获取奖励
|
||||
|
||||
// 通知邀请人
|
||||
Player inviterP =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 死亡报告系统
|
||||
* - 记录玩家死亡坐标(一次性,新死亡覆盖)
|
||||
* - 死亡后发送包含坐标的消息
|
||||
*/
|
||||
public class DeathReport implements Listener {
|
||||
private final Main plugin;
|
||||
|
||||
// 内存缓存: player_name → DeathData
|
||||
private final ConcurrentHashMap<String, DeathData> deathReports = new ConcurrentHashMap<>();
|
||||
|
||||
public static class DeathData {
|
||||
public final String playerName;
|
||||
public String worldName;
|
||||
public final double x, y, z;
|
||||
public long deathTime;
|
||||
public final UUID playerId;
|
||||
|
||||
DeathData(String playerName, UUID playerId, Location loc) {
|
||||
this.playerName = playerName;
|
||||
this.playerId = playerId;
|
||||
this.worldName = loc.getWorld() != null ? loc.getWorld().getName() : "world";
|
||||
this.x = loc.getX();
|
||||
this.y = loc.getY();
|
||||
this.z = loc.getZ();
|
||||
this.deathTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
public DeathReport(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initTable();
|
||||
loadAllFromDB();
|
||||
}
|
||||
|
||||
// ==================== 事件监听 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Location deathLoc = player.getLocation();
|
||||
if (deathLoc == null) return;
|
||||
|
||||
DeathData data = new DeathData(player.getName(), player.getUniqueId(), deathLoc);
|
||||
deathReports.put(player.getName(), data);
|
||||
saveToDB(data);
|
||||
|
||||
// 发送死亡报告消息,带可点击的返回按钮
|
||||
String timeStr = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(data.deathTime));
|
||||
|
||||
Component header = Component.text("§c✦ 你已死亡 ✦ ").append(
|
||||
Component.text("世界: §f" + data.worldName + " §| §7坐标: §e" +
|
||||
String.format("%.1f, %.1f, %.1f", data.x, data.y, data.z) + " §7时间: §f" + timeStr)
|
||||
);
|
||||
|
||||
// "返回死亡地点" 按钮 - 通过 /back 命令
|
||||
Component backButton = Component.text("§e[点击返回死亡地点] ")
|
||||
.clickEvent(ClickEvent.runCommand("/back"))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§7传送到你死亡的位置")));
|
||||
|
||||
player.sendMessage(header);
|
||||
player.sendMessage(backButton);
|
||||
}
|
||||
|
||||
// ==================== 查询/获取 ====================
|
||||
|
||||
/**
|
||||
* 获取玩家的最新死亡坐标(如果有的话)
|
||||
*/
|
||||
public DeathData getDeathReport(String playerName) {
|
||||
return deathReports.get(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除玩家的死亡报告
|
||||
*/
|
||||
public void clearDeathReport(String playerName) {
|
||||
deathReports.remove(playerName);
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private void loadAllFromDB() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name, player_uuid, world, x, y, z, death_time FROM death_reports");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
DeathData data = new DeathData(
|
||||
rs.getString("player_name"),
|
||||
UUID.fromString(rs.getString("player_uuid")),
|
||||
new Location(null, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"))
|
||||
);
|
||||
data.worldName = rs.getString("world");
|
||||
data.deathTime = rs.getLong("death_time");
|
||||
deathReports.put(data.playerName, data);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 表可能不存在,忽略(会在第一次死亡时自动创建)
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToDB(DeathData data) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT OR REPLACE INTO death_reports (player_name, player_uuid, world, x, y, z, death_time) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
ps.setString(1, data.playerName);
|
||||
ps.setString(2, data.playerId.toString());
|
||||
ps.setString(3, data.worldName);
|
||||
ps.setDouble(4, data.x);
|
||||
ps.setDouble(5, data.y);
|
||||
ps.setDouble(6, data.z);
|
||||
ps.setLong(7, data.deathTime);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[死亡报告] 保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void persistAll() {
|
||||
for (DeathData data : deathReports.values()) {
|
||||
saveToDB(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
public void initTable() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
java.sql.Statement st = conn.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS death_reports ("
|
||||
+ "player_name TEXT PRIMARY KEY, "
|
||||
+ "player_uuid TEXT NOT NULL, "
|
||||
+ "world TEXT, "
|
||||
+ "x REAL, y REAL, z REAL, "
|
||||
+ "death_time LONG)");
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[死亡报告] 建表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ public class GUIManager implements Listener {
|
||||
"§6§l邀请数据";
|
||||
public static final String T_USER_MGMT =
|
||||
"§6§l用户管理";
|
||||
public static final String T_USER_GROUP =
|
||||
"§6§l用户组管理";
|
||||
public static final String T_TASK_CENTER =
|
||||
"§6§l任务中心";
|
||||
private final Map<UUID, Integer> userMgmtPages = new HashMap<>();
|
||||
@@ -193,18 +195,7 @@ public class GUIManager implements Listener {
|
||||
}
|
||||
// 在 openMain 方法中,Row3 区域加:
|
||||
|
||||
// ★ 余额查询 - 所有人都能看 ★
|
||||
double myBal = 0;
|
||||
try {
|
||||
var reg = plugin.getServer()
|
||||
.getServicesManager()
|
||||
.getRegistration(
|
||||
net.milkbowl.vault.economy
|
||||
.Economy.class);
|
||||
if (reg != null)
|
||||
myBal = reg.getProvider()
|
||||
.getBalance(p);
|
||||
} catch (Exception ignored) {}
|
||||
// 余额查询已移除Vault支持,仅显示债券余额
|
||||
int myBonds = plugin.getBonds().getBonds(p.getName());
|
||||
g.setItem(32, mkItem(Material.EMERALD,
|
||||
"§b§l我的钱包",
|
||||
@@ -217,6 +208,20 @@ public class GUIManager implements Listener {
|
||||
"§7给钱、扣钱"));
|
||||
}
|
||||
|
||||
// ★ 领地系统 - 39号位(读取用户偏好)
|
||||
int landUiMode = 1;
|
||||
try {
|
||||
landUiMode = plugin.getDb()
|
||||
.getUiMode(p.getName());
|
||||
} catch (Exception ignored) {}
|
||||
g.setItem(39, mkItem(Material.GRASS_BLOCK,
|
||||
"§a§l领地系统",
|
||||
"§7管理你的领地和权限",
|
||||
"",
|
||||
landUiMode == 0
|
||||
? "§e点击打开GUI"
|
||||
: "§e点击打开CLI"));
|
||||
|
||||
|
||||
|
||||
// ===== Row0(0-8): 自定义菜单项 =====
|
||||
@@ -327,28 +332,19 @@ public class GUIManager implements Listener {
|
||||
public void openBalanceOps(Player p) {
|
||||
Inventory g = Bukkit.createInventory(null, 54, "§d§l余额操作");
|
||||
fillBg(g);
|
||||
|
||||
net.milkbowl.vault.economy.Economy eco = null;
|
||||
try {
|
||||
var reg = plugin.getServer().getServicesManager()
|
||||
.getRegistration(net.milkbowl.vault.economy.Economy.class);
|
||||
if (reg != null) eco = reg.getProvider();
|
||||
} catch (Exception ignored) {}
|
||||
// Vault经济支持已移除,仅显示债券余额
|
||||
|
||||
int slot = 0;
|
||||
for (Player op : Bukkit.getOnlinePlayers()) {
|
||||
if (slot >= 45) break;
|
||||
double bal = 0;
|
||||
if (eco != null) bal = eco.getBalance(op);
|
||||
boolean isSelf = op.getName().equals(p.getName());
|
||||
int bondBal = plugin.getBonds().getBonds(op.getName());
|
||||
g.setItem(slot, mkItem(
|
||||
isSelf ? Material.EMERALD_BLOCK : Material.PLAYER_HEAD,
|
||||
"§e" + op.getName(),
|
||||
"§7金币: §a$" + String.format("%.2f", bal),
|
||||
"§7债券: §e" + bondBal + " §6枚",
|
||||
"",
|
||||
"§a左键经济(±) §6右键债券(±)"));
|
||||
"§a仅显示债券余额(Vault经济支持已移除)"));
|
||||
slot++;
|
||||
}
|
||||
|
||||
@@ -399,6 +395,8 @@ public class GUIManager implements Listener {
|
||||
|
||||
public void openSubMenu(
|
||||
Player p, String fileName) {
|
||||
// ★ 剥离MC颜色代码(§f、§7等),防止污染文件名
|
||||
fileName = fileName.replaceAll("\u00A7[0-9a-fk-or]", "").trim();
|
||||
String menuTitle =
|
||||
"§6§l" + fileName
|
||||
.replace(".txt", "");
|
||||
@@ -406,21 +404,153 @@ public class GUIManager implements Listener {
|
||||
null, 54, menuTitle);
|
||||
fillBg(g);
|
||||
|
||||
java.io.File f = new java.io.File(
|
||||
plugin.getDataFolder(), fileName);
|
||||
if (!f.exists()) {
|
||||
p.sendMessage("§c文件不存在: "
|
||||
java.io.File dir = plugin.getDataFolder();
|
||||
java.io.File f = null;
|
||||
String cleanName = fileName.trim();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] openSubMenu: searching '"
|
||||
+ cleanName + "' in '"
|
||||
+ dir.getAbsolutePath() + "'");
|
||||
|
||||
// ★ 第1步:listFiles遍历匹配
|
||||
java.io.File[] allTxt = dir.listFiles(
|
||||
(d, n) -> n.toLowerCase()
|
||||
.endsWith(".txt"));
|
||||
if (allTxt != null) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 目录.txt文件数: "
|
||||
+ allTxt.length);
|
||||
for (java.io.File ff : allTxt) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] > " + ff.getName()
|
||||
+ " size=" + ff.length());
|
||||
if (f == null && ff.getName()
|
||||
.equalsIgnoreCase(cleanName)) {
|
||||
f = ff;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] listFiles返回null!");
|
||||
}
|
||||
|
||||
// ★ 第2步:直接构造路径
|
||||
if (f == null) {
|
||||
f = new java.io.File(dir, cleanName);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 回退构造路径: "
|
||||
+ f.getAbsolutePath()
|
||||
+ " exists=" + f.exists()
|
||||
+ " size=" + f.length());
|
||||
}
|
||||
|
||||
// ★ 第3步:逐个尝试读取方法(不检查exists,直接试)
|
||||
List<String> lines = null;
|
||||
if (f != null) {
|
||||
// 方法1:NIO UTF-8
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] UTF-8读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] UTF-8失败: "
|
||||
+ t.getClass().getSimpleName()
|
||||
+ ": " + t.getMessage());
|
||||
}
|
||||
|
||||
// 方法2:NIO GBK
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
lines = java.nio.file.Files
|
||||
.readAllLines(f.toPath(),
|
||||
java.nio.charset
|
||||
.Charset
|
||||
.forName("GBK"));
|
||||
plugin.getLogger().info(
|
||||
"[Menu] GBK读取: "
|
||||
+ lines.size() + "行");
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] GBK失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 方法3:FileInputStream原始字节
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
try {
|
||||
java.io.FileInputStream fis =
|
||||
new java.io.FileInputStream(f);
|
||||
byte[] raw = fis.readAllBytes();
|
||||
fis.close();
|
||||
if (raw.length > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始字节: "
|
||||
+ raw.length + "B");
|
||||
// 尝试UTF-8解码
|
||||
String content = new String(raw,
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
java.util.List<String> rawLines =
|
||||
java.util.Arrays.asList(
|
||||
content.split("\\r?\\n"));
|
||||
lines = rawLines;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 原始读取: "
|
||||
+ lines.size() + "行");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 原始读取失败: "
|
||||
+ t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 打印前5行调试
|
||||
if (lines != null && !lines.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 最终行数: "
|
||||
+ lines.size());
|
||||
for (int i = 0;
|
||||
i < Math.min(5, lines.size());
|
||||
i++) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + (i + 1)
|
||||
+ ": [" + lines.get(i)
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
p.sendMessage("§c§l子菜单读取失败§r §7"
|
||||
+ fileName);
|
||||
p.sendMessage("§7文件: "
|
||||
+ (f != null
|
||||
? f.getAbsolutePath()
|
||||
+ " (" + f.length() + "B)"
|
||||
: "null"));
|
||||
p.sendMessage("§7存在: "
|
||||
+ (f != null ? f.exists() : "N/A"));
|
||||
if (allTxt != null) {
|
||||
p.sendMessage("§7目录.txt文件:");
|
||||
for (java.io.File ff : allTxt) {
|
||||
p.sendMessage("§7 "
|
||||
+ ff.getName()
|
||||
+ " (" + ff.length()
|
||||
+ "B)");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<String> lines =
|
||||
java.nio.file.Files
|
||||
.readAllLines(
|
||||
f.toPath(),
|
||||
java.nio.charset
|
||||
.StandardCharsets
|
||||
.UTF_8);
|
||||
List<String[]> items =
|
||||
new ArrayList<>();
|
||||
StringBuilder buf =
|
||||
@@ -484,11 +614,6 @@ public class GUIManager implements Listener {
|
||||
it.setItemMeta(im);
|
||||
g.setItem(i, it);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 子菜单读取失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
g.setItem(49, mkItem(
|
||||
Material.ARROW,
|
||||
@@ -609,78 +734,12 @@ public class GUIManager implements Listener {
|
||||
Collections.shuffle(candidates);
|
||||
|
||||
// 3. 主线程分批检测,每tick处理30个坐标
|
||||
final int[] idx = {0};
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
World w = p.getWorld();
|
||||
int end = Math.min(
|
||||
idx[0] + BIOME_BATCH,
|
||||
candidates.size());
|
||||
|
||||
for (int i = idx[0]; i < end; i++) {
|
||||
int x = candidates.get(i)[0];
|
||||
int z = candidates.get(i)[1];
|
||||
|
||||
// 跳过未加载区块
|
||||
if (!w.isChunkLoaded(x >> 4, z >> 4))
|
||||
continue;
|
||||
|
||||
// 获取地表高度
|
||||
int surfaceY =
|
||||
w.getHighestBlockYAt(x, z);
|
||||
|
||||
// 在地表Y取群系(避免地下误判)
|
||||
String key = w.getBlockAt(
|
||||
x, surfaceY, z)
|
||||
.getBiome()
|
||||
.getKey().toString();
|
||||
String cat =
|
||||
mapBiomeCategory(
|
||||
key, w.getName());
|
||||
|
||||
if (category.equals(cat)) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () -> {
|
||||
Location tp =
|
||||
new Location(
|
||||
w,
|
||||
x + 0.5,
|
||||
surfaceY + 1,
|
||||
z + 0.5);
|
||||
p.teleport(tp);
|
||||
p.sendMessage(
|
||||
"§a已传送到 "
|
||||
+ category
|
||||
+ " ("
|
||||
+ x + ","
|
||||
+ (surfaceY + 1)
|
||||
+ ","
|
||||
+ z + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
idx[0] = end;
|
||||
if (idx[0] >= candidates.size()) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () ->
|
||||
p.sendMessage(
|
||||
"§c附近未找到 "
|
||||
+ category
|
||||
+ ",请换个位置再试"));
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 1L);
|
||||
new BiomeScanTask(plugin, p, category, candidates).runTaskTimer(plugin, 0L, 1L);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String mapBiomeCategory(
|
||||
private static String mapBiomeCategory(
|
||||
String biome, String worldName) {
|
||||
if (biome == null) return "平原";
|
||||
String b = biome.toLowerCase();
|
||||
@@ -863,7 +922,7 @@ public class GUIManager implements Listener {
|
||||
// ===== 余额操作 =====
|
||||
if ("§d§l余额操作".equals(title)) {
|
||||
event.setCancelled(true);
|
||||
if (raw == 49) { openAdmin(p); return; }
|
||||
if (raw == 49) { plugin.openMyWallet(p); return; }
|
||||
if (raw < 0 || raw >= 45) return;
|
||||
ItemStack item = event.getView()
|
||||
.getTopInventory().getItem(raw);
|
||||
@@ -927,6 +986,13 @@ public class GUIManager implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 用户组管理 =====
|
||||
if (T_USER_GROUP.equals(title)) {
|
||||
event.setCancelled(true);
|
||||
handleUserGroupClick(p, raw, event.isLeftClick(), event.isRightClick(), event.isShiftClick());
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 打包确认 =====
|
||||
if ("§6§l选择包装方式".equals(title)) {
|
||||
event.setCancelled(true);
|
||||
@@ -2711,6 +2777,279 @@ public class GUIManager implements Listener {
|
||||
plugin.getConfig2().adminTag);
|
||||
}
|
||||
|
||||
// ==================== 用户组管理GUI ====================
|
||||
|
||||
/** 打开用户组管理主界面 */
|
||||
public void openUserGroup(Player p) {
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||
|
||||
Inventory g = Bukkit.createInventory(null, 54, T_USER_GROUP);
|
||||
|
||||
int bonds = plugin.getBonds().getBonds(p.getName());
|
||||
g.setItem(4, mkItem(Material.EMERALD, "§b§l我的债券: " + bonds + " 张", "§7用于购买/续费用户组"));
|
||||
|
||||
// Row1 (9-17): 可购买的用户组
|
||||
List<UserGroupManager.UserGroupConfig> allGroups = ugm.getAllGroups();
|
||||
int slot = 9;
|
||||
for (UserGroupManager.UserGroupConfig cfg : allGroups) {
|
||||
if (slot > 17) break;
|
||||
if (cfg.joinPrice <= 0) continue; // 不开放付费的组不显示
|
||||
|
||||
boolean isMember = ugm.isPlayerInGroup(p.getName(), cfg.name);
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
|
||||
if (isMember) {
|
||||
// 已加入 → 显示在 Row2
|
||||
continue;
|
||||
}
|
||||
|
||||
Material mat = isMember ? Material.LIME_DYE : Material.WHITE_DYE;
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + cfg.joinPrice + " 张债券");
|
||||
if (cfg.durationMinutes > 0) {
|
||||
long days = cfg.durationMinutes / 1440;
|
||||
long hours = (cfg.durationMinutes % 1440) / 60;
|
||||
String dur = days > 0 ? days + "天" : hours + "小时";
|
||||
lore.add("§7有效期: §e" + dur);
|
||||
} else {
|
||||
lore.add("§7有效期: §a永久");
|
||||
}
|
||||
if (cfg.renewPrice > 0) {
|
||||
lore.add("§7续费价格: §e" + cfg.renewPrice + " 张债券");
|
||||
}
|
||||
lore.add("");
|
||||
lore.add("§a左键: 购买给自己");
|
||||
lore.add("§b右键: 赠送给他人");
|
||||
|
||||
g.setItem(slot, mkItem(mat, cfg.displayColor + "§l" + displayName + " §7(购买)", lore.toArray(new String[0])));
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Row2 (18-26): 我的用户组
|
||||
slot = 18;
|
||||
List<Map<String, Object>> myGroups = ugm.getPlayerGroups(p.getName());
|
||||
for (Map<String, Object> grp : myGroups) {
|
||||
if (slot > 26) break;
|
||||
String grpName = String.valueOf(grp.get("group_name"));
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) continue;
|
||||
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
long expiry = ((Number) grp.getOrDefault("expiry_time", 0)).longValue();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
if (expiry > 0) {
|
||||
if (expiry <= now) {
|
||||
lore.add("§c已过期");
|
||||
} else {
|
||||
long remainMs = expiry - now;
|
||||
long remainMin = remainMs / 60000;
|
||||
long rDays = remainMin / 1440;
|
||||
long rHours = (remainMin % 1440) / 60;
|
||||
lore.add("§7剩余: §e" + (rDays > 0 ? rDays + "天" : rHours + "小时"));
|
||||
lore.add("§7到期: §7" + new java.text.SimpleDateFormat("MM-dd HH:mm").format(new java.util.Date(expiry)));
|
||||
}
|
||||
} else {
|
||||
lore.add("§7有效期: §a永久");
|
||||
}
|
||||
// 自动续费状态
|
||||
boolean autoRenew = getAutoRenew(p.getName(), grpName);
|
||||
lore.add("§7自动续费: " + (autoRenew ? "§a开启" : "§c关闭"));
|
||||
lore.add("");
|
||||
lore.add("§a左键: 续费");
|
||||
lore.add("§b右键: 赠送给他人");
|
||||
lore.add("§eShift+右键: " + (autoRenew ? "§c关闭" : "§a开启") + "自动续费");
|
||||
|
||||
g.setItem(slot, mkItem(Material.LIME_DYE, cfg.displayColor + "§l" + displayName, lore.toArray(new String[0])));
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Row4 (40): 赠送入口
|
||||
g.setItem(40, mkItem(Material.SPECTRAL_ARROW, "§e§l赠予用户组", "§7为其他玩家购买/续费用户组"));
|
||||
|
||||
// Row5 (49): 返回
|
||||
g.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
|
||||
fillBg(g);
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/** 处理用户组GUI点击 */
|
||||
public boolean handleUserGroupClick(Player p, int raw, boolean left, boolean right, boolean shift) {
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) return false;
|
||||
|
||||
if (raw == 49) { openMain(p); return true; } // 返回
|
||||
|
||||
// 赠送按钮
|
||||
if (raw == 40) {
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Row1: 可购买的组 (9-17)
|
||||
if (raw >= 9 && raw <= 17) {
|
||||
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||
String grpName = extractGroupNameFromItem(item);
|
||||
if (grpName == null) return false;
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) return false;
|
||||
|
||||
if (left && !right) {
|
||||
// 购买给自己
|
||||
p.closeInventory();
|
||||
p.sendMessage("§7正在购买 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||
String err = ugm.joinGroupByPrice(p.getName(), cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c购买失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l购买成功! §7你已加入用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||
}
|
||||
return true;
|
||||
} else if (right && !left) {
|
||||
// 赠送给他人
|
||||
p.closeInventory();
|
||||
// 记住赠送的组名
|
||||
pendingGiftGroup.put(p.getName(), grpName);
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Row2: 我的组 (18-26)
|
||||
if (raw >= 18 && raw <= 26) {
|
||||
ItemStack item = p.getOpenInventory().getTopInventory().getItem(raw);
|
||||
if (item == null || item.getType() == Material.GRAY_STAINED_GLASS_PANE) return false;
|
||||
String grpName = extractGroupNameFromItem(item);
|
||||
if (grpName == null) return false;
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) return false;
|
||||
|
||||
if (shift && right) {
|
||||
// Shift+右键: 切换自动续费
|
||||
boolean current = getAutoRenew(p.getName(), grpName);
|
||||
setAutoRenew(p.getName(), grpName, !current);
|
||||
p.sendMessage("§a自动续费已" + (!current ? "开启" : "关闭") + ": §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName));
|
||||
openUserGroup(p); // 刷新界面
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left && !right) {
|
||||
// 续费
|
||||
p.closeInventory();
|
||||
p.sendMessage("§7正在续费 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7...");
|
||||
String err = ugm.renewGroup(p.getName(), cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c续费失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l续费成功! §7用户组 §e" + (cfg.displayName.isEmpty() ? cfg.name : cfg.displayName) + " §7已续期");
|
||||
}
|
||||
return true;
|
||||
} else if (right && !left && !shift) {
|
||||
// 赠送给他人
|
||||
p.closeInventory();
|
||||
pendingGiftGroup.put(p.getName(), grpName);
|
||||
p.sendMessage("§e§l[赠予] §7请输入要赠予的玩家名:");
|
||||
plugin.getCDK().requestInput(p, "gift_target", grpName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 处理赠予玩家名输入 */
|
||||
public void handleGiftTargetInput(Player p, String targetName) {
|
||||
String grpName = pendingGiftGroup.remove(p.getName());
|
||||
if (grpName == null) return;
|
||||
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) { p.sendMessage("§c用户组系统未初始化"); return; }
|
||||
|
||||
UserGroupManager.UserGroupConfig cfg = ugm.getGroupConfig(grpName);
|
||||
if (cfg == null) { p.sendMessage("§c用户组不存在: " + grpName); return; }
|
||||
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
|
||||
// 检查目标玩家是否存在
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null && !dbMgr.userExists(targetName)) {
|
||||
p.sendMessage("§c玩家 §e" + targetName + " §c不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 使用新的赠送方法:送礼人(p)付费,收礼人(targetName)免费获得
|
||||
p.sendMessage("§7正在为 §e" + targetName + " §7购买用户组 §e" + displayName + " §7...");
|
||||
String err = ugm.giftGroupToPlayer(p.getName(), targetName, cfg.name);
|
||||
if (err != null) {
|
||||
p.sendMessage("§c赠予失败: " + err);
|
||||
} else {
|
||||
p.sendMessage("§a§l赠予成功!");
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 赠予流程:记录赠送的组名
|
||||
private final Map<String, String> pendingGiftGroup = new ConcurrentHashMap<>();
|
||||
|
||||
// ★ 自动续费偏好(per-player)
|
||||
private final Map<String, Boolean> autoRenewCache = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean getAutoRenew(String player, String group) {
|
||||
String key = player + "|" + group;
|
||||
if (autoRenewCache.containsKey(key)) return autoRenewCache.get(key);
|
||||
// 从数据库读取
|
||||
boolean val = false;
|
||||
try {
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null) {
|
||||
val = dbMgr.getPlayerAutoRenew(player, group);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
autoRenewCache.put(key, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private void setAutoRenew(String player, String group, boolean value) {
|
||||
String key = player + "|" + group;
|
||||
autoRenewCache.put(key, value);
|
||||
try {
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
if (dbMgr != null) {
|
||||
dbMgr.setPlayerAutoRenew(player, group, value);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/** 从GUI物品名称中提取用户组名(去掉格式化前缀和后缀) */
|
||||
private String extractGroupNameFromItem(ItemStack item) {
|
||||
if (item == null || !item.hasItemMeta()) return null;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null || meta.getDisplayName() == null) return null;
|
||||
String name = meta.getDisplayName();
|
||||
// 去掉所有§格式化代码
|
||||
name = name.replaceAll("§[0-9a-fk-orA-FK-OR]", "");
|
||||
// 去掉后缀 " (购买)"
|
||||
name = name.replaceAll("\\s*\\(购买\\)$", "").trim();
|
||||
// 匹配已知组名
|
||||
UserGroupManager ugm = plugin.getUserGroup();
|
||||
if (ugm == null) return null;
|
||||
for (UserGroupManager.UserGroupConfig cfg : ugm.getAllGroups()) {
|
||||
String displayName = cfg.displayName.isEmpty() ? cfg.name : cfg.displayName;
|
||||
if (name.equals(displayName) || name.equals(cfg.name)) {
|
||||
return cfg.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack glass = new ItemStack(
|
||||
Material.GRAY_STAINED_GLASS_PANE);
|
||||
@@ -2774,4 +3113,55 @@ public class GUIManager implements Listener {
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
/** 命名静态内部类替代匿名BukkitRunnable,修复Paper 26 ClassLoader兼容性 */
|
||||
private static class BiomeScanTask extends BukkitRunnable {
|
||||
private final Main plugin;
|
||||
private final Player p;
|
||||
private final String category;
|
||||
private final java.util.List<int[]> candidates;
|
||||
private int idx = 0;
|
||||
|
||||
BiomeScanTask(Main plugin, Player p, String category, java.util.List<int[]> candidates) {
|
||||
this.plugin = plugin;
|
||||
this.p = p;
|
||||
this.category = category;
|
||||
this.candidates = candidates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
World w = p.getWorld();
|
||||
int end = Math.min(idx + BIOME_BATCH, candidates.size());
|
||||
|
||||
for (int i = idx; i < end; i++) {
|
||||
int x = candidates.get(i)[0];
|
||||
int z = candidates.get(i)[1];
|
||||
|
||||
if (!w.isChunkLoaded(x >> 4, z >> 4)) continue;
|
||||
|
||||
int surfaceY = w.getHighestBlockYAt(x, z);
|
||||
String key = w.getBlockAt(x, surfaceY, z)
|
||||
.getBiome().getKey().toString();
|
||||
String cat = mapBiomeCategory(key, w.getName());
|
||||
|
||||
if (category.equals(cat)) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
Location tp = new Location(w, x + 0.5, surfaceY + 1, z + 0.5);
|
||||
p.teleport(tp);
|
||||
p.sendMessage("§a已传送到 " + category + " (" + x + "," + (surfaceY + 1) + "," + z + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
idx = end;
|
||||
if (idx >= candidates.size()) {
|
||||
this.cancel();
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
p.sendMessage("§c附近未找到 " + category + ",请换个位置再试"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Item;
|
||||
@@ -12,7 +11,6 @@ import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.sql.Date;
|
||||
@@ -287,11 +285,8 @@ public class GarbageManager {
|
||||
.decode(base64);
|
||||
String json = new String(bytes,
|
||||
StandardCharsets.UTF_8);
|
||||
Type type = new TypeToken<
|
||||
Map<String, Object>>() {}
|
||||
.getType();
|
||||
Map<String, Object> map =
|
||||
new Gson().fromJson(json, type);
|
||||
new Gson().fromJson(json, Map.class);
|
||||
map = fixNumberTypes(map);
|
||||
return ItemStack.deserialize(map);
|
||||
} catch (Exception e) {
|
||||
@@ -425,7 +420,7 @@ public class GarbageManager {
|
||||
"§7已是最后一页"));
|
||||
}
|
||||
|
||||
// 52 格:返回
|
||||
// 52 格:返回主菜单
|
||||
g.setItem(52, mk(Material.ARROW,
|
||||
"§7返回",
|
||||
"§7返回主菜单"));
|
||||
|
||||
@@ -194,28 +194,21 @@ public class GiftManager {
|
||||
giveItem(p, Material.WOODEN_AXE);
|
||||
giveItem(p, Material.WOODEN_SHOVEL);
|
||||
} else if (stage == 2) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
// 游戏币改为债券
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 500, "gift_stage2", "", "新手礼包", "阶段2奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
} else if (stage == 3) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 1500, "gift_stage3", "", "新手礼包", "阶段3奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
giveItem(p, Material.IRON_PICKAXE);
|
||||
giveItem(p, Material.IRON_AXE);
|
||||
@@ -225,16 +218,12 @@ public class GiftManager {
|
||||
giveItem(p, Material.IRON_LEGGINGS);
|
||||
giveItem(p, Material.IRON_BOOTS);
|
||||
} else if (stage == 4) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 2000);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 2000, "gift_stage4", "", "新手礼包", "阶段4奖励");
|
||||
int after = bm.getBonds(p.getName());
|
||||
p.sendMessage("§a§l[礼包] §f债券: §e" + before + "§f → §a" + after);
|
||||
}
|
||||
giveItem(p, Material.DIAMOND_HELMET);
|
||||
giveItem(p, Material.DIAMOND_CHESTPLATE);
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerBedEnterEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Home传送点管理器
|
||||
* 支持: sethome, delhome, home, homes
|
||||
* 集成用户组系统: home_limit (0=全局默认, >0=组限制, -1=无限)
|
||||
*/
|
||||
public class HomeManager implements Listener {
|
||||
private final Main plugin;
|
||||
|
||||
// 床位置缓存: player_name → Location (用于自动记录床位置)
|
||||
private final Map<String, Location> bedLocations = new ConcurrentHashMap<>();
|
||||
|
||||
public HomeManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDatabase();
|
||||
}
|
||||
|
||||
// ==================== 数据结构 ====================
|
||||
|
||||
public static class HomeData {
|
||||
public String name;
|
||||
public String world;
|
||||
public double x, y, z;
|
||||
public float yaw, pitch;
|
||||
public long createdAt;
|
||||
|
||||
public Location toLocation(World world) {
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 数据库初始化 ====================
|
||||
|
||||
private void initDatabase() {
|
||||
try {
|
||||
java.sql.Connection db = plugin.getDb().getConnection();
|
||||
if (db == null) {
|
||||
plugin.getLogger().warning("[Home] 无法获取数据库连接");
|
||||
return;
|
||||
}
|
||||
|
||||
java.sql.Statement st = db.createStatement();
|
||||
// homes表已在DatabaseManager中创建,此处确保索引
|
||||
st.execute("CREATE INDEX IF NOT EXISTS idx_homes_player ON homes(player_name)");
|
||||
st.close();
|
||||
|
||||
plugin.getLogger().info("[Home] 数据库初始化完成");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Home] 数据库初始化失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 命令处理 ====================
|
||||
|
||||
public boolean handleCommand(Player player, String label, String[] args) {
|
||||
String lowerLabel = label.toLowerCase();
|
||||
|
||||
switch (lowerLabel) {
|
||||
case "sethome":
|
||||
return handleSetHome(player, args);
|
||||
case "delhome":
|
||||
return handleDelHome(player, args);
|
||||
case "home":
|
||||
return handleHome(player, args);
|
||||
case "homes":
|
||||
return handleHomes(player, args);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /sethome <名称> - 设置家
|
||||
*/
|
||||
private boolean handleSetHome(Player player, String[] args) {
|
||||
if (args.length == 0) {
|
||||
player.sendMessage("§c用法: /sethome <名称>");
|
||||
return true;
|
||||
}
|
||||
|
||||
String homeName = args[0].toLowerCase();
|
||||
|
||||
// 校验名称格式
|
||||
if (!homeName.matches("^[a-zA-Z0-9_]{1,16}$")) {
|
||||
player.sendMessage("§c家名只能包含字母、数字、下划线,长度1-16");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查家数量限制
|
||||
int maxHomes = getMaxHomes(player);
|
||||
if (maxHomes != -1) {
|
||||
List<HomeData> homes = getHomes(player.getName());
|
||||
if (homes.size() >= maxHomes) {
|
||||
player.sendMessage("§c你已达到家数量上限 (" + maxHomes + "个)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在同名家
|
||||
if (homeExists(player.getName(), homeName)) {
|
||||
player.sendMessage("§c你已有一个名为 §e" + homeName + " §c的家");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 保存家
|
||||
Location loc = player.getLocation();
|
||||
saveHome(player.getName(), homeName, loc);
|
||||
|
||||
player.sendMessage("§a家 §e" + homeName + " §a已设置!");
|
||||
player.sendMessage("§7使用 §e/home " + homeName + " §7传送回家");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* /delhome <名称> - 删除家
|
||||
*/
|
||||
private boolean handleDelHome(Player player, String[] args) {
|
||||
if (args.length == 0) {
|
||||
player.sendMessage("§c用法: /delhome <名称>");
|
||||
return true;
|
||||
}
|
||||
|
||||
String homeName = args[0].toLowerCase();
|
||||
|
||||
if (!homeExists(player.getName(), homeName)) {
|
||||
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
|
||||
return true;
|
||||
}
|
||||
|
||||
deleteHome(player.getName(), homeName);
|
||||
player.sendMessage("§a家 §e" + homeName + " §a已删除!");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* /home [名称] - 传送到家
|
||||
*/
|
||||
private boolean handleHome(Player player, String[] args) {
|
||||
if (args.length == 0) {
|
||||
// 无参数: 传送到第一个家
|
||||
List<HomeData> homes = getHomes(player.getName());
|
||||
if (homes.isEmpty()) {
|
||||
player.sendMessage("§c你还没有设置任何家");
|
||||
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
|
||||
return true;
|
||||
}
|
||||
teleportToHome(player, homes.get(0));
|
||||
return true;
|
||||
}
|
||||
|
||||
String homeName = args[0].toLowerCase();
|
||||
HomeData home = getHome(player.getName(), homeName);
|
||||
|
||||
if (home == null) {
|
||||
player.sendMessage("§c你没有名为 §e" + homeName + " §c的家");
|
||||
return true;
|
||||
}
|
||||
|
||||
teleportToHome(player, home);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* /homes - 查看所有家
|
||||
*/
|
||||
private boolean handleHomes(Player player, String[] args) {
|
||||
List<HomeData> homes = getHomes(player.getName());
|
||||
|
||||
if (homes.isEmpty()) {
|
||||
player.sendMessage("§e你还没有设置任何家");
|
||||
player.sendMessage("§7使用 §e/sethome <名称> §7设置一个家");
|
||||
return true;
|
||||
}
|
||||
|
||||
int maxHomes = getMaxHomes(player);
|
||||
player.sendMessage("§6§l你的家 §7(" + homes.size() + (maxHomes == -1 ? "/" + "∞" : "/" + maxHomes) + ")");
|
||||
player.sendMessage("§7────────────────────");
|
||||
|
||||
for (HomeData home : homes) {
|
||||
String worldName = home.world != null ? home.world : "未知";
|
||||
player.sendMessage("§e • " + home.name + " §7[" + worldName + "]");
|
||||
}
|
||||
|
||||
player.sendMessage("§7────────────────────");
|
||||
player.sendMessage("§7使用 §e/home <名称> §7传送");
|
||||
player.sendMessage("§7使用 §e/sethome <名称> §7设置新家");
|
||||
player.sendMessage("§7使用 §e/delhome <名称> §7删除家");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 床事件监听 ====================
|
||||
|
||||
/**
|
||||
* 玩家上床时自动记录床位置
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerBedEnter(PlayerBedEnterEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Location bedLoc = event.getBed().getLocation();
|
||||
|
||||
bedLocations.put(player.getName(), bedLoc);
|
||||
|
||||
// 自动保存为"bed"家(如果不存在)
|
||||
if (!homeExists(player.getName(), "bed")) {
|
||||
saveHome(player.getName(), "bed", bedLoc);
|
||||
player.sendMessage("§a床位置已自动保存为 §e/home bed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家重生时更新床位置
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Location respawnLoc = event.getRespawnLocation();
|
||||
|
||||
// 如果重生点是床位置,更新bed家
|
||||
Location bedLoc = bedLocations.get(player.getName());
|
||||
if (bedLoc != null && bedLoc.getWorld() != null) {
|
||||
if (respawnLoc.getWorld() != null &&
|
||||
respawnLoc.getWorld().getName().equals(bedLoc.getWorld().getName()) &&
|
||||
respawnLoc.distance(bedLoc) < 5) {
|
||||
// 重生点就是床,更新bed家
|
||||
updateHome(player.getName(), "bed", respawnLoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 家数量限制 ====================
|
||||
|
||||
/**
|
||||
* 获取玩家最大家数量
|
||||
* @return -1=无限, 0=使用全局默认, >0=用户组独立限制
|
||||
*/
|
||||
private int getMaxHomes(Player player) {
|
||||
UserGroupManager userGroupMgr = plugin.getUserGroup();
|
||||
if (userGroupMgr == null) {
|
||||
return getGlobalHomeLimit();
|
||||
}
|
||||
|
||||
// 获取玩家最高优先级用户组
|
||||
UserGroupManager.UserGroupConfig highestGroup = userGroupMgr.getHighestGroup(player.getName());
|
||||
if (highestGroup == null) {
|
||||
return getGlobalHomeLimit();
|
||||
}
|
||||
|
||||
// 获取用户组的home_limit配置
|
||||
int groupLimit = userGroupMgr.getHomeLimit(highestGroup.name);
|
||||
|
||||
if (groupLimit == -1) {
|
||||
// 无限
|
||||
return -1;
|
||||
} else if (groupLimit == 0) {
|
||||
// 跟随全局
|
||||
return getGlobalHomeLimit();
|
||||
} else {
|
||||
// 独立限制
|
||||
return groupLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局家数量限制
|
||||
* 读取area_config中的max_home_per_player配置(管理员可在Web后台调整)
|
||||
* 解析失败或值<=0时回退到默认5
|
||||
*/
|
||||
private int getGlobalHomeLimit() {
|
||||
try {
|
||||
if (plugin.areaProtection != null) {
|
||||
int limit = plugin.areaProtection.getGlobalMaxHomePerPlayer();
|
||||
if (limit > 0) {
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
// 默认5个家
|
||||
return 5;
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private boolean homeExists(String playerName, String homeName) {
|
||||
try {
|
||||
java.sql.Connection db = plugin.getDb().getConnection();
|
||||
if (db == null) return false;
|
||||
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT COUNT(*) FROM homes WHERE player_name=? AND home_name=?");
|
||||
ps.setString(1, playerName);
|
||||
ps.setString(2, homeName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
boolean exists = rs.next() && rs.getInt(1) > 0;
|
||||
rs.close();
|
||||
ps.close();
|
||||
return exists;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<HomeData> getHomes(String playerName) {
|
||||
List<HomeData> homes = new ArrayList<>();
|
||||
try {
|
||||
java.sql.Connection db = plugin.getDb().getConnection();
|
||||
if (db == null) return homes;
|
||||
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM homes WHERE player_name=? ORDER BY created_at");
|
||||
ps.setString(1, playerName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
HomeData home = new HomeData();
|
||||
home.name = rs.getString("home_name");
|
||||
home.world = rs.getString("world");
|
||||
home.x = rs.getDouble("x");
|
||||
home.y = rs.getDouble("y");
|
||||
home.z = rs.getDouble("z");
|
||||
home.yaw = rs.getFloat("yaw");
|
||||
home.pitch = rs.getFloat("pitch");
|
||||
home.createdAt = rs.getLong("created_at");
|
||||
homes.add(home);
|
||||
}
|
||||
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[Home] 获取家列表失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return homes;
|
||||
}
|
||||
|
||||
private HomeData getHome(String playerName, String homeName) {
|
||||
List<HomeData> homes = getHomes(playerName);
|
||||
for (HomeData home : homes) {
|
||||
if (home.name.equalsIgnoreCase(homeName)) {
|
||||
return home;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void saveHome(String playerName, String homeName, Location loc) {
|
||||
try {
|
||||
java.sql.Connection db = plugin.getDb().getConnection();
|
||||
if (db == null) return;
|
||||
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO homes (player_name, home_name, world, x, y, z, yaw, pitch, created_at) "
|
||||
+ "VALUES (?,?,?,?,?,?,?,?,?)");
|
||||
ps.setString(1, playerName);
|
||||
ps.setString(2, homeName);
|
||||
ps.setString(3, loc.getWorld() != null ? loc.getWorld().getName() : "world");
|
||||
ps.setDouble(4, loc.getX());
|
||||
ps.setDouble(5, loc.getY());
|
||||
ps.setDouble(6, loc.getZ());
|
||||
ps.setFloat(7, loc.getYaw());
|
||||
ps.setFloat(8, loc.getPitch());
|
||||
ps.setLong(9, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[Home] 保存家失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHome(String playerName, String homeName, Location loc) {
|
||||
saveHome(playerName, homeName, loc); // INSERT OR REPLACE 自动更新
|
||||
}
|
||||
|
||||
private void deleteHome(String playerName, String homeName) {
|
||||
try {
|
||||
java.sql.Connection db = plugin.getDb().getConnection();
|
||||
if (db == null) return;
|
||||
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"DELETE FROM homes WHERE player_name=? AND home_name=?");
|
||||
ps.setString(1, playerName);
|
||||
ps.setString(2, homeName);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[Home] 删除家失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传送玩家到家
|
||||
*/
|
||||
private void teleportToHome(Player player, HomeData home) {
|
||||
World world = Bukkit.getWorld(home.world);
|
||||
if (world == null) {
|
||||
player.sendMessage("§c世界 §e" + home.world + " §c不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
Location loc = home.toLocation(world);
|
||||
|
||||
// 安全检查: 确保位置安全
|
||||
if (loc.getBlock().getType().isSolid()) {
|
||||
// 向上寻找安全位置
|
||||
for (int y = (int) loc.getY(); y < 256; y++) {
|
||||
Location safeLoc = new Location(world, loc.getX(), y + 1, loc.getZ());
|
||||
if (!safeLoc.getBlock().getType().isSolid()) {
|
||||
loc = safeLoc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.teleport(loc);
|
||||
player.sendMessage("§a已传送到家 §e" + home.name);
|
||||
}
|
||||
|
||||
// ==================== Tab补全 ====================
|
||||
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (!(sender instanceof Player)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
String cmdName = command.getName().toLowerCase();
|
||||
|
||||
if (cmdName.equals("home") || cmdName.equals("delhome")) {
|
||||
if (args.length == 1) {
|
||||
List<String> homes = new ArrayList<>();
|
||||
for (HomeData home : getHomes(player.getName())) {
|
||||
if (home.name.toLowerCase().startsWith(args[0].toLowerCase())) {
|
||||
homes.add(home.name);
|
||||
}
|
||||
}
|
||||
return homes;
|
||||
}
|
||||
} else if (cmdName.equals("sethome") || cmdName.equals("homes")) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ==================== 清理 ====================
|
||||
|
||||
public void shutdown() {
|
||||
bedLocations.clear();
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ public class IPGroupManager {
|
||||
return can;
|
||||
}
|
||||
|
||||
public int getMaxAccounts() {
|
||||
return maxAccounts;
|
||||
}
|
||||
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
|
||||
@@ -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,550 @@
|
||||
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 net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
|
||||
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;
|
||||
|
||||
/** 玩家最后一次查询的坐标缓存,翻页导航时复用,避免 getTargetBlockExact 不准 */
|
||||
private final Map<UUID, Location> lastQueryLocations = new ConcurrentHashMap<>();
|
||||
|
||||
/** 记录查询默认条数 */
|
||||
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;
|
||||
}
|
||||
Location loc = target.getLocation();
|
||||
lastQueryLocations.put(p.getUniqueId(), loc);
|
||||
queryAndPrint(p, loc);
|
||||
}
|
||||
|
||||
private void queryAndPrint(Player p, Location loc) {
|
||||
queryAndPrint(p, loc, 0);
|
||||
}
|
||||
|
||||
private void queryAndPrint(Player p, Location loc, int page) {
|
||||
String world = loc.getWorld().getName();
|
||||
int x = loc.getBlockX(), y = loc.getBlockY(),
|
||||
z = loc.getBlockZ();
|
||||
|
||||
// 按天分页:page=0今日,1昨日,2前日...
|
||||
List<Map<String, Object>> blocks =
|
||||
plugin.getDb().getLandBlockLogAt(
|
||||
world, x, y, z, QUERY_LIMIT, page);
|
||||
// 按天分页:page=0今日,1昨日,2前日...
|
||||
List<Map<String, Object>> containers =
|
||||
plugin.getDb().getLandContainerLogAt(
|
||||
world, x, y, z, QUERY_LIMIT, page);
|
||||
|
||||
if (blocks.isEmpty() && containers.isEmpty()) {
|
||||
if (page > 0) {
|
||||
p.sendMessage("§6§l[回声碎片] §e坐标 ("
|
||||
+ x + "," + y + "," + z + ") 第" + (page + 1) + "页无记录");
|
||||
} else {
|
||||
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"));
|
||||
}
|
||||
String ct = (String) r.get("container_type");
|
||||
if (ct != null && !ct.isEmpty()) {
|
||||
materials.add(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查下一页是否有数据(决定是否显示"下一页"链接)
|
||||
boolean hasNext = false;
|
||||
if (!plugin.getDb().getLandBlockLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
|
||||
if (!plugin.getDb().getLandContainerLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
|
||||
|
||||
final int currentPage = page;
|
||||
final boolean hasNextPage = hasNext;
|
||||
// 异步批量翻译
|
||||
MaterialTranslator.translateBatch(materials)
|
||||
.thenAccept(translations -> {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
printResults(p, x, y, z, blocks, containers, translations, currentPage, hasNextPage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
printResults(p, x, y, z, blocks, containers, translations, 0, false);
|
||||
}
|
||||
|
||||
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,
|
||||
int page, boolean hasNext) {
|
||||
String pageLabel = page == 0 ? "今日" : (page == 1 ? "昨日" : (page + 1) + "日前");
|
||||
p.sendMessage("§6§l[回声碎片] §e坐标 ("
|
||||
+ x + "," + y + "," + z + ") 的记录§7(" + pageLabel + "):");
|
||||
|
||||
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— 容器记录 §7(" + pageLabel + ")—");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 可点击翻页导航
|
||||
Component nav = Component.text(" §7[ ");
|
||||
if (page > 0) {
|
||||
nav = nav.append(Component.text("§e← 上一页")
|
||||
.clickEvent(ClickEvent.runCommand("/landrec log " + page))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看前一日记录"))));
|
||||
nav = nav.append(Component.text(" §7| "));
|
||||
}
|
||||
nav = nav.append(Component.text("§f第" + (page + 1) + "页"));
|
||||
if (hasNext) {
|
||||
nav = nav.append(Component.text(" §7| ")
|
||||
.append(Component.text("§e下一页 →")
|
||||
.clickEvent(ClickEvent.runCommand("/landrec log " + (page + 2)))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看后一日记录")))));
|
||||
}
|
||||
nav = nav.append(Component.text(" §7]"));
|
||||
p.sendMessage(nav);
|
||||
}
|
||||
|
||||
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")) {
|
||||
// 发放权限...
|
||||
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;
|
||||
}
|
||||
if (sub.equalsIgnoreCase("log")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f此命令仅限玩家使用");
|
||||
return true;
|
||||
}
|
||||
Player p = (Player) sender;
|
||||
int page = 0; // 默认今日
|
||||
boolean isNav = false; // true=从翻页链接跳转,复用 lastQueryLocations
|
||||
if (args.length >= 2) {
|
||||
try {
|
||||
page = Math.max(0, Integer.parseInt(args[1]) - 1);
|
||||
isNav = true;
|
||||
} catch (NumberFormatException ignored) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f页码格式错误");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 翻页时复用上次查询坐标,避免 getTargetBlockExact 玩家未对准
|
||||
Location navLoc = isNav ? lastQueryLocations.get(p.getUniqueId()) : null;
|
||||
if (navLoc != null) {
|
||||
queryAndPrint(p, navLoc, page);
|
||||
return true;
|
||||
}
|
||||
// 首次查询或缓存过期,取准心方块
|
||||
Block target = p.getTargetBlockExact(6);
|
||||
if (target == null || target.getType() == Material.AIR) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
|
||||
return true;
|
||||
}
|
||||
Location loc = target.getLocation();
|
||||
if (!isNav) {
|
||||
lastQueryLocations.put(p.getUniqueId(), loc);
|
||||
}
|
||||
queryAndPrint(p, loc, page);
|
||||
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/landrec log §f- 查看当前方块今日记录");
|
||||
sender.sendMessage(" §7/landrec log <页码> §f- 查看指定日期的记录(1=今日 2=昨日 ...)");
|
||||
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处今日记录");
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ public class LoginManager {
|
||||
plugin.getDb().createUser(
|
||||
name, hash, salt);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
@@ -100,6 +104,17 @@ public class LoginManager {
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// ★ 注册后立即同步用户数据到PHP
|
||||
if (plugin.webManager != null) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try { plugin.webManager.syncUserRegistrations(); }
|
||||
catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 注册后同步用户异常: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,6 +154,16 @@ public class LoginManager {
|
||||
.msg("not_registered"));
|
||||
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) {
|
||||
p.sendMessage("§c用法: /login <密码>");
|
||||
return true;
|
||||
@@ -161,7 +186,13 @@ public class LoginManager {
|
||||
plugin.getDb().checkPassword(
|
||||
name, hash);
|
||||
if (matchMain) {
|
||||
// ★ 登录成功,重置风控
|
||||
plugin.getRiskControl().onLoginSuccess(name);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
plugin.getDb().setLoggedIn(
|
||||
name, true);
|
||||
plugin.getDb().setLoggedIn(
|
||||
@@ -172,6 +203,16 @@ public class LoginManager {
|
||||
plugin.getDb().setField(name,
|
||||
"last_online_check",
|
||||
System.currentTimeMillis());
|
||||
// ★ 立即同步在线状态到PHP(异步,避免阻塞主线程)
|
||||
if (plugin.webManager != null) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
plugin.webManager.syncOnlinePlayers();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web通信] 异步syncOnlinePlayers异常: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
// 登录成功关闭飞行
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
@@ -191,12 +232,18 @@ public class LoginManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean matchTemp =
|
||||
String pwdResult =
|
||||
plugin.getDb()
|
||||
.checkPasswordOrTemp(
|
||||
.checkPasswordWithFallback(
|
||||
name, hash);
|
||||
if (matchTemp) {
|
||||
if (pwdResult != null) {
|
||||
// ★ 临时密码登录成功,也重置风控
|
||||
plugin.getRiskControl().onLoginSuccess(name);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
@@ -223,8 +270,19 @@ public class LoginManager {
|
||||
}
|
||||
|
||||
|
||||
// ★ 密码错误,记录风控
|
||||
int remaining = plugin.getRiskControl()
|
||||
.recordFailAttempt(p);
|
||||
if (remaining < 0) {
|
||||
// 已被封禁(triggerBan内部已踢出)
|
||||
return true;
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("login_failed"));
|
||||
if (remaining > 0) {
|
||||
p.sendMessage("§c§l[风控] §f剩余 §e"
|
||||
+ remaining + " §f次尝试机会");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] handleLogin异常: "
|
||||
|
||||
+7933
-6065
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();
|
||||
}
|
||||
}
|
||||
@@ -83,18 +83,29 @@ public class MenuManager implements Listener {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ===== 读取菜单.txt =====
|
||||
// ===== 读取菜单.txt(主菜单唯一来源) =====
|
||||
public void loadMenu() {
|
||||
items.clear();
|
||||
|
||||
// 优先菜单.txt,备选menu.txt
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) createDefault(f);
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) {
|
||||
f = alt;
|
||||
} else {
|
||||
createDefault(f);
|
||||
}
|
||||
}
|
||||
parseFile(f);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 加载 "
|
||||
+ items.size()
|
||||
+ " 个菜单项");
|
||||
"[Menu] 加载 " + items.size()
|
||||
+ " 个菜单项 (源: "
|
||||
+ f.getName() + ")");
|
||||
}
|
||||
|
||||
private void parseFile(File f) {
|
||||
@@ -372,9 +383,16 @@ public class MenuManager implements Listener {
|
||||
|
||||
// ===== 保存 =====
|
||||
public void saveMenu() {
|
||||
// 保存到加载源文件
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
if (!f.exists()) {
|
||||
File alt = new File(
|
||||
plugin.getDataFolder(),
|
||||
"menu.txt");
|
||||
if (alt.exists()) f = alt;
|
||||
}
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
|
||||
@@ -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 String name; public String mat;
|
||||
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){
|
||||
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 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 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();
|
||||
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) {
|
||||
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();
|
||||
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,
|
||||
String materialName) {
|
||||
@@ -448,9 +477,10 @@ public class OrderManager implements Listener {
|
||||
}
|
||||
return "§c对应的潜影盒已不在背包中";
|
||||
} else {
|
||||
// ★ 散装:检测物品数量
|
||||
// ★ 散装:检测物品数量(含NBT商品按材质+ItemMeta精确校验,必须"有货且有正确NBT")
|
||||
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)
|
||||
return "§c" + it.name + "不足(有"
|
||||
+ have + "/" + it.qty + ")";
|
||||
@@ -685,7 +715,7 @@ public class OrderManager implements Listener {
|
||||
}
|
||||
} else {
|
||||
for (OrderItem it : r.items)
|
||||
removeItems(p, it.mat, it.qty);
|
||||
removeItems(p, it);
|
||||
}
|
||||
// 退钱
|
||||
plugin.getBonds().addBonds(
|
||||
@@ -821,13 +851,38 @@ public class OrderManager implements Listener {
|
||||
p.sendMessage("§e输入退款百分比(1-100): #" + r.orderId + " 实付" + r.totalPaid + "枚");
|
||||
}
|
||||
} 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);
|
||||
r.status = 2;
|
||||
r.refundedAmount = r.totalPaid;
|
||||
r.refundType = "auto_return";
|
||||
saveOrders();
|
||||
p.sendMessage("§a全额退款 §e" + r.player + " §6" + r.totalPaid + "枚");
|
||||
notifyPlayer(r.uuid, "§a退款 §6" + r.totalPaid + "枚");
|
||||
p.sendMessage("§a全额退款(含退货) §e" + r.player + " §6" + r.totalPaid + "枚");
|
||||
notifyPlayer(r.uuid, "§a[订单] 退款退货成功,§6" + r.totalPaid + "枚已退还");
|
||||
openAdminOrders(p, tu, page);
|
||||
}
|
||||
}
|
||||
@@ -932,7 +987,7 @@ public class OrderManager implements Listener {
|
||||
Player target = Bukkit.getPlayer(r.uuid);
|
||||
if (target != null && target.isOnline()) {
|
||||
for (OrderItem it : r.items)
|
||||
removeItems(target, it.mat, it.qty);
|
||||
removeItems(target, it);
|
||||
}
|
||||
plugin.getBonds().addBonds(r.player, r.totalPaid);
|
||||
r.status = 2;
|
||||
@@ -1037,7 +1092,7 @@ public class OrderManager implements Listener {
|
||||
}
|
||||
} else {
|
||||
for (OrderItem it : r.items)
|
||||
removeItems(p, it.mat, it.qty);
|
||||
removeItems(p, it);
|
||||
}
|
||||
|
||||
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 "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 "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; }
|
||||
} if (cur != null && cur.uuid != null) orders.add(cur); br.close();
|
||||
} 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("status=" + r.status); pw.println("refundedAmount=" + r.refundedAmount);
|
||||
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();
|
||||
} 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;
|
||||
Player victim =
|
||||
(Player) event.getEntity();
|
||||
org.bukkit.entity.Entity kc =
|
||||
event.getDamageSource().getCausingEntity();
|
||||
Player killer =
|
||||
victim.getKiller();
|
||||
(kc instanceof Player) ? (Player) kc : null;
|
||||
if (killer == null) return;
|
||||
if (killer.equals(victim)) return;
|
||||
|
||||
@@ -1074,8 +1076,15 @@ public class PVPManager implements Listener {
|
||||
public boolean onCommand(Player p,
|
||||
String[] args) {
|
||||
if (args.length == 0) {
|
||||
return showStats(p,
|
||||
p.getName());
|
||||
showStats(p, p.getName());
|
||||
// ★ 同时展示竞技场总战绩
|
||||
showArenaTotalStats(p, p.getName());
|
||||
return true;
|
||||
}
|
||||
// ★ 数字参数 → 查询指定会话编号的竞技场战绩
|
||||
if (args.length == 1 && args[0].matches("\\d+")) {
|
||||
showArenaSessionStats(p, p.getName(), Integer.parseInt(args[0]));
|
||||
return true;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "create":
|
||||
@@ -1112,9 +1121,31 @@ public class PVPManager implements Listener {
|
||||
case "tempban":
|
||||
return handleTempBan(
|
||||
p, args);
|
||||
case "join":
|
||||
case "arena":
|
||||
plugin.getPVPArenaManager().joinArena(p);
|
||||
return true;
|
||||
case "equip":
|
||||
case "adjust":
|
||||
if (!plugin.getPVPArenaManager().isInPVPArena(p.getName())) {
|
||||
p.sendMessage("§c你不在PVP竞技场中");
|
||||
return true;
|
||||
}
|
||||
plugin.getPVPArenaManager().openEquipmentSelection(p);
|
||||
return true;
|
||||
case "leave":
|
||||
case "exit":
|
||||
plugin.leavePVP(p);
|
||||
return true;
|
||||
default:
|
||||
return showStats(p,
|
||||
p.getName());
|
||||
showStats(p, p.getName());
|
||||
p.sendMessage("§7——— PVP指令 ———");
|
||||
p.sendMessage("§e/pvp join §7进入PVP竞技场");
|
||||
p.sendMessage("§e/pvp equip §7调整装备(食物/盾牌/弓弩)");
|
||||
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
|
||||
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
|
||||
p.sendMessage("§e/pvp list §7查看PVP区域列表");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1335,9 +1366,28 @@ public class PVPManager implements Listener {
|
||||
if (region == null) {
|
||||
p.sendMessage("§6§l" + target
|
||||
+ " 的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 =
|
||||
getAllRegions();
|
||||
boolean has = false;
|
||||
boolean has = pvpWorldKills > 0 || pvpWorldDeaths > 0;
|
||||
for (Map<String, Object> row :
|
||||
all) {
|
||||
String rn = (String) row
|
||||
@@ -1355,6 +1405,21 @@ public class PVPManager implements Listener {
|
||||
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)
|
||||
p.sendMessage(
|
||||
"§7暂无记录");
|
||||
@@ -1377,4 +1442,43 @@ public class PVPManager implements Listener {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ★ 展示PVP竞技场总战绩
|
||||
private void showArenaTotalStats(Player p, String target) {
|
||||
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
|
||||
String uuid = off.getUniqueId().toString();
|
||||
Map<String, Object> stats = plugin.getPVPArenaManager().getTotalArenaStats(uuid);
|
||||
int kills = (int) stats.get("kills");
|
||||
int deaths = (int) stats.get("deaths");
|
||||
if (kills == 0 && deaths == 0) return;
|
||||
double kd = deaths > 0 ? (double) kills / deaths : kills;
|
||||
p.sendMessage("§6§l[PVP竞技场] " + target);
|
||||
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", kd));
|
||||
}
|
||||
|
||||
// ★ 展示指定会话编号的竞技场战绩
|
||||
private void showArenaSessionStats(Player p, String target, int sessionNumber) {
|
||||
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
|
||||
String uuid = off.getUniqueId().toString();
|
||||
// 先查会话信息(含3天有效期检查)
|
||||
Map<String, Object> session = plugin.getPVPArenaManager().getArenaSessionByNumber(sessionNumber);
|
||||
if (session == null) {
|
||||
p.sendMessage("§c未找到编号 " + sessionNumber + " 的竞技场记录,或已超出3天查询范围");
|
||||
return;
|
||||
}
|
||||
long createdAt = (Long) session.get("created_at");
|
||||
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
|
||||
if (System.currentTimeMillis() - createdAt > threeDaysMs) {
|
||||
p.sendMessage("§c该记录已超出3天查询范围,不再支持查询");
|
||||
return;
|
||||
}
|
||||
int dbSessionId = (int) session.get("id");
|
||||
Map<String, Object> stats = plugin.getPVPArenaManager().getArenaSessionStats(uuid, dbSessionId);
|
||||
int kills = (int) stats.get("kills");
|
||||
int deaths = (int) stats.get("deaths");
|
||||
String worldName = (String) session.get("world_name");
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(createdAt));
|
||||
p.sendMessage("§6§l[PVP竞技场#" + sessionNumber + "] " + target + " §7(" + dateStr + " " + worldName + ")");
|
||||
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", deaths > 0 ? (double) kills / deaths : kills));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -259,20 +259,21 @@ public class PointsManager {
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm != null) {
|
||||
int before = bm.getBonds(p.getName());
|
||||
bm.addBonds(p.getName(), 1000, "points_exchange", "", "积分商店", "积分兑换债券");
|
||||
int after = bm.getBonds(p.getName());
|
||||
plugin.getDb().deductPoints(p.getName(), 100);
|
||||
p.sendMessage(plugin.getConfig2().msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
p.sendMessage("§a[积分] §f债券: §e" + before + " → §a" + after);
|
||||
} else {
|
||||
p.sendMessage("§c债券系统不可用");
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
p.sendMessage(plugin.getConfig2().msg("points_insufficient")
|
||||
.replace("{points}", String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
|
||||
@@ -1213,15 +1213,6 @@ public class QuestTracker {
|
||||
}
|
||||
|
||||
Main main = (Main) plugin;
|
||||
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
|
||||
boolean useEconomy = "economy".equalsIgnoreCase(
|
||||
main.getConfigMgr().rewardChannel);
|
||||
|
||||
// Vault 经济引用(仅 useEconomy 时使用)
|
||||
net.milkbowl.vault.economy.Economy econ = null;
|
||||
if (useEconomy) {
|
||||
econ = main.getEconomy();
|
||||
}
|
||||
|
||||
int sentCount = 0;
|
||||
|
||||
@@ -1247,53 +1238,17 @@ public class QuestTracker {
|
||||
// 尝试识别经济金额
|
||||
double amt = extractEconomyAmount(part);
|
||||
if (amt > 0) {
|
||||
// ★ 根据配置分发
|
||||
if (useEconomy && econ != null) {
|
||||
// —— Vault 经济 ——
|
||||
double before =
|
||||
econ.getBalance(player);
|
||||
econ.depositPlayer(player, amt);
|
||||
double after =
|
||||
econ.getBalance(player);
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(余额: §e"
|
||||
+ String.format("%.2f", before)
|
||||
+ " §7→ §a"
|
||||
+ String.format("%.2f", after)
|
||||
+ "§7)");
|
||||
if (plugin instanceof Main) {
|
||||
((Main) plugin).getCommission()
|
||||
.payCommission(
|
||||
player.getName(), amt);
|
||||
}
|
||||
// Economy已移除,全部转为债券发放
|
||||
// —— 债券 ——
|
||||
int bondAmt = (int) Math.round(amt);
|
||||
BondManager bonds = main.getBonds();
|
||||
if (bonds != null) {
|
||||
int before = bonds.getBonds(player.getName());
|
||||
bonds.addBonds(player.getName(), bondAmt, "quest_reward", qf.displayName, "任务系统", "任务奖励:" + qf.displayName);
|
||||
int after = bonds.getBonds(player.getName());
|
||||
player.sendMessage("§a§l[任务奖励] §f" + part + " §7(债券: §e" + before + " §7→ §a" + after + "§7)");
|
||||
} else {
|
||||
// —— 债券(默认) ——
|
||||
int bondAmt =
|
||||
(int) Math.round(amt);
|
||||
BondManager bonds = main.getBonds();
|
||||
if (bonds != null) {
|
||||
int before = bonds.getBonds(
|
||||
player.getName());
|
||||
bonds.addBonds(
|
||||
player.getName(),
|
||||
bondAmt,
|
||||
"quest_reward",
|
||||
qf.displayName,
|
||||
"任务系统",
|
||||
"任务奖励:" + qf.displayName);
|
||||
int after = bonds.getBonds(
|
||||
player.getName());
|
||||
player.sendMessage(
|
||||
"§a§l[任务奖励] §f" + part
|
||||
+ " §7(债券: §e"
|
||||
+ before
|
||||
+ " §7→ §a"
|
||||
+ after + "§7)");
|
||||
} else {
|
||||
player.sendMessage(
|
||||
"§c债券系统不可用");
|
||||
}
|
||||
player.sendMessage("§c债券系统不可用");
|
||||
}
|
||||
sentCount++;
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 快速返回系统 (/back)
|
||||
* - 每分钟记录一次在线玩家的坐标
|
||||
* - /back 命令传送玩家到最近的一个记录坐标
|
||||
*/
|
||||
public class QuickBack implements Listener, CommandExecutor, TabCompleter {
|
||||
private final Main plugin;
|
||||
|
||||
// 内存缓存: player_name → Location (每分钟刷新)
|
||||
private final Map<String, Location> lastKnownLocations = new ConcurrentHashMap<>();
|
||||
|
||||
// 冷却: player_name → next_available_time
|
||||
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
|
||||
|
||||
// ★ 定时器引用(用于 shutdown 时取消)
|
||||
private BukkitTask scheduleTask = null;
|
||||
|
||||
private static final long COOLDOWN_MS = 10 * 1000; // 10秒冷却
|
||||
|
||||
public QuickBack(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
loadAllFromDB();
|
||||
|
||||
// 每分钟记录所有在线玩家的坐标
|
||||
scheduleTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
recordLocation(p);
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1200L, 1200L);
|
||||
}
|
||||
|
||||
// ==================== 事件监听 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
// 移动超过8格时才记录(减少数据库写入压力)
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
if (to == null || from.getWorld() != to.getWorld()) return;
|
||||
|
||||
double distance = from.distance(to);
|
||||
if (distance < 8.0) return;
|
||||
|
||||
recordLocation(event.getPlayer());
|
||||
}
|
||||
|
||||
// ==================== 命令处理 ====================
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c此命令仅限玩家使用");
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
|
||||
// 检查冷却
|
||||
Long nextAvailable = cooldowns.get(player.getName());
|
||||
if (nextAvailable != null && System.currentTimeMillis() < nextAvailable) {
|
||||
long remaining = (nextAvailable - System.currentTimeMillis()) / 1000;
|
||||
player.sendMessage("§c快速返回冷却中,请等待 §e" + remaining + " §c秒");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取最近的已知位置
|
||||
Location loc = getLastKnownLocation(player);
|
||||
if (loc == null) {
|
||||
player.sendMessage("§c没有找到可返回的位置记录");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 传送
|
||||
if (loc.getWorld() == null) {
|
||||
player.sendMessage("§c位置世界信息无效");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ★ 禁止/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); // 对齐格子中心
|
||||
player.teleport(dest);
|
||||
|
||||
// 设置冷却
|
||||
cooldowns.put(player.getName(), System.currentTimeMillis() + COOLDOWN_MS);
|
||||
player.sendMessage("§a§l[快速返回] §f已传送到最近记录点");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 位置记录 ====================
|
||||
|
||||
private void recordLocation(Player player) {
|
||||
Location loc = player.getLocation();
|
||||
lastKnownLocations.put(player.getName(), loc);
|
||||
saveToDB(player.getName(), loc);
|
||||
}
|
||||
|
||||
public Location getLastKnownLocation(Player player) {
|
||||
// 优先从内存获取
|
||||
Location loc = lastKnownLocations.get(player.getName());
|
||||
if (loc != null && loc.getWorld() != null) {
|
||||
return loc;
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
return loadFromDB(player.getName());
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private void loadAllFromDB() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name, world, x, y, z FROM quick_back_records");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
String playerName = rs.getString("player_name");
|
||||
Location loc = new Location(
|
||||
Bukkit.getWorld(rs.getString("world")),
|
||||
rs.getDouble("x"),
|
||||
rs.getDouble("y"),
|
||||
rs.getDouble("z")
|
||||
);
|
||||
lastKnownLocations.put(playerName, loc);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 表可能不存在
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToDB(String playerName, Location loc) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT OR REPLACE INTO quick_back_records (player_name, world, x, y, z) VALUES (?, ?, ?, ?, ?)");
|
||||
ps.setString(1, playerName);
|
||||
ps.setString(2, loc.getWorld() != null ? loc.getWorld().getName() : "world");
|
||||
ps.setDouble(3, loc.getX());
|
||||
ps.setDouble(4, loc.getY());
|
||||
ps.setDouble(5, loc.getZ());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
private Location loadFromDB(String playerName) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return null;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT world, x, y, z FROM quick_back_records WHERE player_name=? LIMIT 1");
|
||||
ps.setString(1, playerName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
if (rs.next()) {
|
||||
Location loc = new Location(
|
||||
Bukkit.getWorld(rs.getString("world")),
|
||||
rs.getDouble("x"),
|
||||
rs.getDouble("y"),
|
||||
rs.getDouble("z")
|
||||
);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return loc;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== Tab补全 ====================
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
public void initTable() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
java.sql.Statement st = conn.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS quick_back_records ("
|
||||
+ "player_name TEXT PRIMARY KEY, "
|
||||
+ "world TEXT, "
|
||||
+ "x REAL, y REAL, z REAL)");
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[快速返回] 建表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 关闭 ====================
|
||||
|
||||
public void shutdown() {
|
||||
if (scheduleTask != null) {
|
||||
scheduleTask.cancel();
|
||||
}
|
||||
lastKnownLocations.clear();
|
||||
cooldowns.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.BanList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
||||
* 风控管理器:密码错误阈值封禁 + 报警邮件
|
||||
*
|
||||
* 功能1:密码错误达到阈值 → 自动封禁账号N分钟 + 推送报警邮件
|
||||
* 功能2:封禁期间拒绝所有登录尝试
|
||||
*/
|
||||
public class RiskControlManager {
|
||||
|
||||
@@ -17,232 +22,261 @@ public class RiskControlManager {
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个IP -> 关联的可疑玩家列表
|
||||
private final Map<String, Set<String>> ipSuspicious =
|
||||
// 每个玩家名 -> 封禁到期时间戳(毫秒)
|
||||
private final Map<String, Long> banUntil =
|
||||
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) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次失败的登录尝试
|
||||
*
|
||||
* @param playerName 玩家名
|
||||
* @return 剩余尝试次数
|
||||
* 获取当前密码错误阈值(从配置读取)
|
||||
*/
|
||||
public int recordFailAttempt(String playerName) {
|
||||
int current = failAttempts
|
||||
.getOrDefault(playerName, 0) + 1;
|
||||
failAttempts.put(playerName, current);
|
||||
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
|
||||
private int getMaxFailAttempts() {
|
||||
return plugin.getConfig2().passwordFailThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定玩家的失败计数(登录成功时调用)
|
||||
* 获取封禁时长(毫秒)(从配置读取)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家的失败次数
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家剩余封禁时间(秒),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) {
|
||||
resetFailAttempts(playerName);
|
||||
failAttempts.remove(playerName);
|
||||
banUntil.remove(playerName);
|
||||
}
|
||||
|
||||
// ==================== 清理 ====================
|
||||
|
||||
/**
|
||||
* 清理过期的冷却记录(可由定时任务调用)
|
||||
* 清理过期的封禁记录(可由定时任务调用)
|
||||
* 同步解除过期玩家的Bukkit封禁
|
||||
*/
|
||||
public void cleanup() {
|
||||
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() {
|
||||
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() {
|
||||
failAttempts.clear();
|
||||
ipSuspicious.clear();
|
||||
verifyCooldown.clear();
|
||||
banUntil.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ public class ShopItem {
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
private final Material material;
|
||||
private final int buyPrice;
|
||||
private final int sellPrice;
|
||||
private int buyPrice;
|
||||
private int sellPrice;
|
||||
private int stock;
|
||||
private int hourlySales;
|
||||
private int totalSales;
|
||||
@@ -42,6 +42,8 @@ public class ShopItem {
|
||||
public void setStock(int s) { this.stock = s; }
|
||||
public void setHourlySales(int s) { this.hourlySales = 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) {
|
||||
this.hourlySales += n;
|
||||
this.totalSales += n;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -48,12 +48,16 @@ public class UpdateChecker {
|
||||
private static final String GITHUB_REPO = "sdf1_login";
|
||||
private static final String GITEE_OWNER = "nihaoshidifu";
|
||||
private static final String GITEE_REPO = "sdf1_login";
|
||||
private static final String GYP_OWNER = "youpaishidifu";
|
||||
private static final String GYP_REPO = "Sdf1_login";
|
||||
|
||||
// API 地址(使用releases/latest获取最新版本)
|
||||
private static final String GITHUB_API =
|
||||
"https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest";
|
||||
private static final String GITEE_API =
|
||||
"https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest";
|
||||
private static final String GYP_API =
|
||||
"https://git.ypshidifu.cn/api/v1/repos/" + GYP_OWNER + "/" + GYP_REPO + "/releases/latest";
|
||||
|
||||
// 第三路备选:宝塔静态HTML
|
||||
private static final String FALLBACK_HTML =
|
||||
@@ -64,6 +68,8 @@ public class UpdateChecker {
|
||||
"https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases";
|
||||
private static final String GITEE_LINK =
|
||||
"https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases";
|
||||
private static final String GYP_LINK =
|
||||
"https://git.ypshidifu.cn/" + GYP_OWNER + "/" + GYP_REPO + "/releases";
|
||||
|
||||
// 单次请求超时(毫秒)
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
@@ -157,18 +163,31 @@ public class UpdateChecker {
|
||||
plugin.getLogger().info("[Sdf1_login] 启动自动检查更新...");
|
||||
}
|
||||
|
||||
// ★ 双通道并行执行,一路成功掐断另一路
|
||||
// ★ 三通道并行执行,一路成功掐断其余
|
||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||
AtomicReference<CheckResult> gypResult = new AtomicReference<>();
|
||||
AtomicReference<CheckResult> ghResult = new AtomicReference<>();
|
||||
AtomicReference<CheckResult> geResult = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
|
||||
// 通道0:git.ypshidifu.cn(最高优先级,Gitea格式)
|
||||
Thread gypThread = new Thread(() -> {
|
||||
if (!cancelled.get()) {
|
||||
gypResult.set(tryChannel(GYP_API, "git.ypshidifu.cn", GYP_LINK));
|
||||
if (gypResult.get().isSuccess()) {
|
||||
cancelled.set(true);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
gypThread.setDaemon(true);
|
||||
|
||||
// 通道1:GitHub
|
||||
Thread ghThread = new Thread(() -> {
|
||||
if (!cancelled.get()) {
|
||||
ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK));
|
||||
if (ghResult.get().isSuccess()) {
|
||||
cancelled.set(true); // 成功则取消另一路
|
||||
cancelled.set(true);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
@@ -180,34 +199,38 @@ public class UpdateChecker {
|
||||
if (!cancelled.get()) {
|
||||
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
|
||||
if (geResult.get().isSuccess()) {
|
||||
cancelled.set(true); // 成功则取消另一路
|
||||
cancelled.set(true);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
geThread.setDaemon(true);
|
||||
|
||||
gypThread.start();
|
||||
ghThread.start();
|
||||
geThread.start();
|
||||
|
||||
try {
|
||||
latch.await(); // 等待两路都完成
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
CheckResult gyp = gypResult.get();
|
||||
CheckResult gh = ghResult.get();
|
||||
CheckResult ge = geResult.get();
|
||||
|
||||
// 选择成功的结果
|
||||
// 选择成功的结果(优先级:git.ypshidifu.cn > GitHub > Gitee)
|
||||
CheckResult chosen = null;
|
||||
|
||||
if (gh != null && gh.isSuccess()) {
|
||||
if (gyp != null && gyp.isSuccess()) {
|
||||
chosen = gyp;
|
||||
} else if (gh != null && gh.isSuccess()) {
|
||||
chosen = gh;
|
||||
} else if (ge != null && ge.isSuccess()) {
|
||||
chosen = ge;
|
||||
} else {
|
||||
// ★ 双通道均失败,尝试第三路备选(宝塔静态HTML)
|
||||
plugin.getLogger().info("[Sdf1_login] GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
|
||||
plugin.getLogger().info("[Sdf1_login] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
|
||||
CheckResult fbResult = tryFallbackChannel();
|
||||
if (fbResult != null && fbResult.isSuccess()) {
|
||||
chosen = fbResult;
|
||||
@@ -215,7 +238,11 @@ public class UpdateChecker {
|
||||
} else {
|
||||
failCount++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (gyp != null && gyp.error != null) {
|
||||
sb.append("§7git.ypshidifu.cn: §c").append(gyp.error);
|
||||
}
|
||||
if (gh != null && gh.error != null) {
|
||||
if (sb.length() > 0) sb.append("\n");
|
||||
sb.append("§7GitHub: §c").append(gh.error);
|
||||
}
|
||||
if (ge != null && ge.error != null) {
|
||||
@@ -310,10 +337,15 @@ public class UpdateChecker {
|
||||
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
|
||||
}
|
||||
|
||||
// 显示另一通道的失败原因
|
||||
CheckResult other = (src.equals("GitHub")) ? ge : gh;
|
||||
if (other != null && !other.isSuccess() && other.error != null) {
|
||||
sender.sendMessage("§7[Sdf1_login] [更新] " + other.source + " 不可用: " + other.error);
|
||||
// 显示其他失败通道的错误信息
|
||||
if (gyp != null && !gyp.isSuccess() && gyp.error != null && !src.equals("git.ypshidifu.cn")) {
|
||||
sender.sendMessage("§7[Sdf1_login] [更新] git.ypshidifu.cn 不可用: " + gyp.error);
|
||||
}
|
||||
if (gh != null && !gh.isSuccess() && gh.error != null && !src.equals("GitHub")) {
|
||||
sender.sendMessage("§7[Sdf1_login] [更新] GitHub 不可用: " + gh.error);
|
||||
}
|
||||
if (ge != null && !ge.isSuccess() && ge.error != null && !src.equals("Gitee")) {
|
||||
sender.sendMessage("§7[Sdf1_login] [更新] Gitee 不可用: " + ge.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,14 +3,18 @@ package Sdf1_login;
|
||||
import org.bukkit.Bukkit;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 玩家正版/离线身份验证管理器
|
||||
*
|
||||
* 核心逻辑:在离线模式(online-mode=false)服务器中,所有玩家UUID都是离线格式,
|
||||
* 无法通过UUID区分正版/盗版。因此:
|
||||
* - UUID非离线格式 → 服务器在线模式,正版玩家 → autoLogin
|
||||
* - UUID离线格式 → 一律视为非正版,需手动登录或OAuth验证
|
||||
* - 正版玩家通过 /mslogin OAuth验证后,由Main.addVerifiedPremiumPlayer()标记,
|
||||
* 在onJoin检查点0直接autoLogin,不经过此验证器
|
||||
*/
|
||||
public class VerificationManager {
|
||||
|
||||
private final Main plugin;
|
||||
@@ -29,6 +33,11 @@ public class VerificationManager {
|
||||
return Bukkit.getServer().getOnlineMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步验证玩家身份
|
||||
* 注意:此方法仅用于判断UUID格式,不执行自动登录
|
||||
* 自动登录由onJoin中的检查点0/1/2控制
|
||||
*/
|
||||
public void verifyPremiumAsync(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
Thread t = new Thread(() -> doVerify(player, callback),
|
||||
@@ -50,49 +59,35 @@ public class VerificationManager {
|
||||
"[Sdf1_login] 当前UUID: " + uuid);
|
||||
|
||||
try {
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(
|
||||
java.util.UUID offlineUuid = java.util.UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
String offlineStr = offlineUuid.toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 离线UUID: " + offlineStr);
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
boolean isOfflineUuid =
|
||||
player.getUniqueId().equals(offlineUuid);
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 离线UUID: " + offlineUuid);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID匹配离线格式: "
|
||||
+ isOfflineUuid);
|
||||
|
||||
if (!isOfflineUuid) {
|
||||
// UUID不是离线格式 → 服务器在线模式的正版玩家
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 正版玩家");
|
||||
"[Sdf1_login] 结论: 正版玩家(非离线UUID)");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 离线UUID → 无法通过UUID区分正版/盗版,一律视为非正版
|
||||
// 正版玩家需通过 /mslogin OAuth验证 或 /login 手动登录
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID为离线格式,"
|
||||
+ "查询Mojang API...");
|
||||
UUID mojangUuid = fetchMojangUuid(name);
|
||||
|
||||
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] 结论: 纯离线玩家");
|
||||
}
|
||||
|
||||
"[Sdf1_login] 结论: 离线UUID玩家,需手动登录");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] (正版玩家请使用 /mslogin 或 /正版 命令验证)");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(false);
|
||||
@@ -104,55 +99,4 @@ public class VerificationManager {
|
||||
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
@@ -0,0 +1,35 @@
|
||||
# 非法域名后缀列表
|
||||
# 每行一个后缀,支持以 * 开头表示匹配所有子域名
|
||||
# 修改此文件后无需重启,插件会在每次检测时重新加载
|
||||
# 默认包含常见非法注册域名后缀
|
||||
|
||||
com
|
||||
cn
|
||||
net
|
||||
org
|
||||
io
|
||||
xyz
|
||||
top
|
||||
club
|
||||
site
|
||||
online
|
||||
store
|
||||
tech
|
||||
info
|
||||
biz
|
||||
ru
|
||||
tk
|
||||
ml
|
||||
ga
|
||||
cf
|
||||
cc
|
||||
ws
|
||||
tv
|
||||
link
|
||||
click
|
||||
date
|
||||
vip
|
||||
party
|
||||
fun
|
||||
zone
|
||||
space
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Sdf1_login
|
||||
version: 2.2
|
||||
version: 2.81
|
||||
main: Sdf1_login.Main
|
||||
api-version: '1.21'
|
||||
commands:
|
||||
@@ -42,9 +42,9 @@ commands:
|
||||
签到:
|
||||
description: 每日签到(中文)
|
||||
usage: /签到
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
删除账号:
|
||||
description: 申请删除账号(中文)
|
||||
usage: /删除账号 [玩家名]
|
||||
@@ -97,11 +97,17 @@ commands:
|
||||
description: 打开商店
|
||||
usage: /商店
|
||||
# testorder:
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
protect:
|
||||
description: 区域保护
|
||||
usage: /protect
|
||||
landrec:
|
||||
description: 领地记录系统(回声碎片发放)
|
||||
usage: /landrec give [玩家]
|
||||
传送:
|
||||
description: 传送至领地
|
||||
usage: /传送 <领地名>
|
||||
update:
|
||||
description: 检查所有插件更新(集群控制)
|
||||
usage: /update
|
||||
@@ -110,4 +116,71 @@ commands:
|
||||
usage: /web
|
||||
控制台:
|
||||
description: 生成Web端登录令牌(中文版)
|
||||
usage: /控制台
|
||||
usage: /控制台
|
||||
mslogin:
|
||||
description: 正版登录(Microsoft OAuth)
|
||||
usage: /mslogin
|
||||
正版:
|
||||
description: 正版登录(中文版)
|
||||
usage: /正版
|
||||
tpa:
|
||||
description: tp传送指令
|
||||
usage: /tpa
|
||||
tpaccept:
|
||||
description: 接受传送
|
||||
usage: /tpaccept
|
||||
tpdeny:
|
||||
description: 拒绝传送
|
||||
usage: /tpdeny
|
||||
tpauto:
|
||||
description: 自动接收传送
|
||||
usage: /tpauto
|
||||
tpacancel:
|
||||
description: 取消传送请求
|
||||
usage: /tpacancel
|
||||
tpaall:
|
||||
description: 请求传送所有人
|
||||
usage: /tpaall
|
||||
tpahere:
|
||||
description: 请求传送ta到自己身边
|
||||
usage: /tpahere
|
||||
sethome:
|
||||
description: 设置一个家
|
||||
usage: /sethome <名称>
|
||||
delhome:
|
||||
description: 删除一个家
|
||||
usage: /delhome <名称>
|
||||
home:
|
||||
description: 传送到家
|
||||
usage: /home [名称]
|
||||
homes:
|
||||
description: 查看所有家
|
||||
usage: /homes
|
||||
group:
|
||||
description: 用户组管理
|
||||
usage: /group <buy|renew|info> [组名]
|
||||
back:
|
||||
description: 快速返回最近记录点
|
||||
usage: /back
|
||||
|
||||
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
|
||||
清空队列:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /清空队列
|
||||
cleartake:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /cleartake
|
||||
|
||||
Reference in New Issue
Block a user