Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00a2f6e4b | ||
|
|
720757463b | ||
|
|
6260ee11f9 | ||
|
|
e4ff388281 | ||
|
|
12c6d14916 | ||
|
|
39b74f6c4e | ||
|
|
eac0da081e | ||
|
|
b11beb7126 | ||
|
|
eeecb551bc | ||
|
|
7e9be87e7d | ||
|
|
5dcf887236 | ||
|
|
7e1f082b74 | ||
|
|
9a67a4a8bb | ||
|
|
1e7b1a21b2 | ||
|
|
15e7ab62f9 | ||
|
|
c249355e27 | ||
|
|
c50b06361e | ||
|
|
a521b4c59a | ||
|
|
1dba2690a1 | ||
|
|
2b7daa1928 | ||
|
|
f008c49e84 | ||
|
|
16214872aa | ||
|
|
d8a4c820fb | ||
|
|
31b4594c2e | ||
|
|
4f6f0d6252 | ||
|
|
499cf10aba | ||
|
|
df86721562 | ||
|
|
28a33e7c02 | ||
|
|
5ad1843200 | ||
|
|
6f844b5714 | ||
|
|
2429d2b614 | ||
|
|
e6e7664244 | ||
|
|
005b8db0d4 | ||
|
|
477dbab970 | ||
|
|
b91814d837 | ||
|
|
43df7808cd | ||
|
|
3b8fbdf1b7 | ||
|
|
920cf79641 | ||
|
|
1a83fe7c55 | ||
|
|
bcaac7ee71 | ||
|
|
d489c01000 | ||
|
|
411210c341 | ||
|
|
b78f87db62 | ||
|
|
079ff0587f | ||
|
|
e7c862f4e8 | ||
|
|
51e56ec85e | ||
|
|
8069070219 | ||
|
|
bf421cbd18 | ||
|
|
a517d12feb | ||
|
|
5cca422ba9 | ||
|
|
307db659bc | ||
|
|
fe89f9649e | ||
|
|
6609b29fab | ||
|
|
ef03f1b90b | ||
|
|
1d56705365 | ||
|
|
8d3442c966 | ||
|
|
89b876754c | ||
|
|
47678a4c3d | ||
|
|
f92e251bdc | ||
|
|
383e1bc7df | ||
|
|
ca8070e31b | ||
|
|
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 | ||
|
|
8072855d94 | ||
|
|
873aca91bf | ||
|
|
bcca065931 | ||
|
|
a34cc92327 | ||
|
|
44302d8eff | ||
|
|
7624b7cb7d | ||
|
|
a4b09254f4 | ||
|
|
68a6d2e7d4 | ||
|
|
7d70f8f92e | ||
|
|
c66ec5eaaa | ||
|
|
aa719dc2da | ||
|
|
32371a6e30 | ||
|
|
7953e9de13 | ||
|
|
038e2f3e87 | ||
|
|
a35ddb8418 | ||
|
|
3c65360f9c | ||
|
|
7b0722b039 | ||
|
|
49fb264378 | ||
|
|
7dca1a71e1 | ||
|
|
704b8a03d9 | ||
|
|
387fad8bdd | ||
|
|
acc4085509 | ||
|
|
a75be0ebae | ||
|
|
325ec9f877 | ||
|
|
851375502d | ||
|
|
c9da8c8ec2 | ||
|
|
cf246d4076 | ||
|
|
c747dbcc76 | ||
|
|
66f1e33d5e | ||
|
|
717a7a9266 | ||
|
|
34d46a7d12 | ||
|
|
1f12d3ec30 | ||
|
|
1a58caec96 | ||
|
|
ea93ccc087 | ||
|
|
302328bc8e | ||
|
|
8b73031ef0 | ||
|
|
677fea7de1 |
@@ -0,0 +1,21 @@
|
||||
name: Check Source
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check workspace
|
||||
run: |
|
||||
echo "=== PWD ==="
|
||||
pwd
|
||||
echo "=== LS ==="
|
||||
ls -la
|
||||
echo "=== FIND POM ==="
|
||||
find / -name "pom.xml" 2>/dev/null
|
||||
echo "=== ENV ==="
|
||||
env | grep -iE "workspace|repo|git" | sort
|
||||
echo "=== DONE ==="
|
||||
@@ -0,0 +1,93 @@
|
||||
name: Auto Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Setup JDK 25
|
||||
run: |
|
||||
echo "Setting up JDK 25..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq wget unzip
|
||||
wget -q https://api.adoptium.net/v3/binary/latest/25/ga/linux/x64/jdk/hotspot/normal/eclipse?project=jdk -O /tmp/jdk25.tar.gz
|
||||
tar -xzf /tmp/jdk25.tar.gz -C /opt
|
||||
export JAVA_HOME=$(ls -d /opt/jdk-25* | head -1)
|
||||
echo "JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV
|
||||
echo "$JAVA_HOME/bin" >> $GITHUB_PATH
|
||||
echo "JDK 25 installed: $($JAVA_HOME/bin/java -version 2>&1 | head -1)"
|
||||
echo "Setup completed"
|
||||
|
||||
- name: Setup Maven
|
||||
run: |
|
||||
echo "Setting up Maven..."
|
||||
apt-get install -y -qq maven
|
||||
echo "Maven version: $(mvn -version 2>&1 | head -1)"
|
||||
echo "Setup completed"
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
echo "Checking out code..."
|
||||
GIT_HOST=${GITHUB_SERVER_URL#http://}
|
||||
GIT_HOST=${GIT_HOST#https://}
|
||||
rm -rf /workspace/youpaishidifu/Sdf1_login/*
|
||||
rm -rf /workspace/youpaishidifu/Sdf1_login/.[!.]*
|
||||
git clone "http://${GIT_HOST}/$(echo ${GITHUB_REPOSITORY} | tr '[:upper:]' '[:lower:]').git" .
|
||||
echo "Checkout completed"
|
||||
|
||||
- name: Build with Maven
|
||||
run: |
|
||||
echo "Building with Maven..."
|
||||
mvn -B clean package -DskipTests
|
||||
echo "Build completed"
|
||||
|
||||
- name: Verify JAR
|
||||
run: |
|
||||
echo "Verifying JAR..."
|
||||
ls -la target/*.jar
|
||||
echo "Verification completed"
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
RUNNER_TOKEN: ${ACTIONS_RUNTIME_TOKEN}
|
||||
run: |
|
||||
set -e
|
||||
VERSION=$(grep "^version:" src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
|
||||
TAG="v$VERSION"
|
||||
BASE="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY"
|
||||
echo "Version: $VERSION, Tag: $TAG"
|
||||
|
||||
echo "=== Checking existing tag $TAG ==="
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $RUNNER_TOKEN" "$BASE/releases/tags/$TAG")
|
||||
echo "Check tag HTTP: $HTTP_CODE"
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
OLD_ID=$(curl -s -H "Authorization: token $RUNNER_TOKEN" "$BASE/releases/tags/$TAG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
|
||||
echo "Tag $TAG exists (release #$OLD_ID), deleting..."
|
||||
curl -s -X DELETE -H "Authorization: token $RUNNER_TOKEN" "$BASE/releases/$OLD_ID"
|
||||
curl -s -X DELETE -H "Authorization: token $RUNNER_TOKEN" "$BASE/tags/$TAG"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
echo "=== Creating release ==="
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $RUNNER_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$BASE/releases" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Auto release by Gitea Actions\",\"draft\":false,\"prerelease\":false}"
|
||||
|
||||
echo "=== Uploading artifact ==="
|
||||
JAR=$(ls target/*.jar | head -1)
|
||||
UPLOAD_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $RUNNER_TOKEN" \
|
||||
-H "Content-Type: application/jar" \
|
||||
"$BASE/releases/tags/$TAG/assets?name=$(basename $JAR)" \
|
||||
--data-binary "@$JAR")
|
||||
echo "Upload HTTP: $UPLOAD_CODE"
|
||||
|
||||
echo "=== Release $TAG created successfully ==="
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Simple Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Hello
|
||||
run: echo "Hello from Gitea Actions!"
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Test Env
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Show env
|
||||
run: |
|
||||
echo "=== ENV ==="
|
||||
env | sort
|
||||
echo "=== DONE ==="
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 报告一个 Bug 以帮助改进插件
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
请简明扼要地描述 Bug 是什么。
|
||||
|
||||
## 复现步骤
|
||||
1. 进入 ...
|
||||
2. 点击 ...
|
||||
3. 出现错误 ...
|
||||
|
||||
## 期望行为
|
||||
描述你期望发生的事情。
|
||||
|
||||
## 实际行为
|
||||
描述实际发生了什么。
|
||||
|
||||
## 服务端信息
|
||||
- 服务器类型和版本(如 Paper 1.21.4):
|
||||
- 插件版本:
|
||||
|
||||
## 截图 / 日志
|
||||
如有相关截图或报错日志,请粘贴在此处。
|
||||
|
||||
## 其他信息
|
||||
补充任何你认为有助于解决问题的信息。
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 问题排查指南
|
||||
url: https://github.com/ypsdf1/sdf1_login
|
||||
about: 在提交 Bug 前,请先查阅文档确认是否已有相关说明。
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 功能新增
|
||||
about: 提出一个新功能的需求建议
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 功能描述
|
||||
请描述你希望添加的新功能。
|
||||
|
||||
## 动机
|
||||
为什么需要这个功能?它能解决什么问题?
|
||||
|
||||
## 建议的实现方式
|
||||
如果对实现方式有想法,请在此描述。
|
||||
|
||||
## 额外信息
|
||||
补充任何你认为有助于理解需求的上下文。
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: 建议提交
|
||||
about: 对插件提出改进建议或反馈
|
||||
title: "[Suggestion] "
|
||||
labels: suggestion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 建议内容
|
||||
请描述你的建议或想法。
|
||||
|
||||
## 背景
|
||||
什么场景下产生的这个建议?
|
||||
|
||||
## 预期效果
|
||||
你认为采用这个建议后能达到什么效果?
|
||||
@@ -0,0 +1,39 @@
|
||||
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: Extract version from plugin.yml
|
||||
id: version
|
||||
run: |
|
||||
echo "version=$(grep '^version: ' src/main/resources/plugin.yml | sed 's/version: //')" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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: v${{ steps.version.outputs.version }}
|
||||
name: v${{ steps.version.outputs.version }}
|
||||
files: target/Sdf1_login.jar
|
||||
generate_release_notes: true
|
||||
@@ -36,4 +36,73 @@ build/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# 开发辅助文件(本地保留,不推送到仓库)
|
||||
/代码关联关系.md
|
||||
txt/
|
||||
*.xlsx
|
||||
*.patch
|
||||
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
|
||||
con
|
||||
prn
|
||||
aux
|
||||
com1
|
||||
com2
|
||||
com3
|
||||
com4
|
||||
com5
|
||||
com6
|
||||
com7
|
||||
com8
|
||||
com9
|
||||
lpt1
|
||||
lpt2
|
||||
lpt3
|
||||
lpt4
|
||||
lpt5
|
||||
lpt6
|
||||
lpt7
|
||||
lpt8
|
||||
lpt9hs_err_pid*.log
|
||||
hs_err_pid*.log
|
||||
replay_pid*.log
|
||||
|
||||
# 屏蔽patch文件
|
||||
*.patch
|
||||
# 屏蔽wb产生的文件
|
||||
.workbuddy/
|
||||
# 屏蔽备份文件
|
||||
backup/
|
||||
# 屏蔽垃圾文件
|
||||
*.idx
|
||||
*.pack
|
||||
*.rar
|
||||
test.*
|
||||
/*.rev
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,14 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="26" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,112 +0,0 @@
|
||||
# Sdf1_login Plugin User Guide
|
||||
|
||||
## 1. Features
|
||||
|
||||
| Name | Description/Function | Status |
|
||||
| :--- | :--- | :--- |
|
||||
| Login | User login/registration | Completed |
|
||||
| World Trash Can | Clear server drops | In Progress |
|
||||
| Ticket System | User ticket submission | Completed |
|
||||
| Check-in System | Check-in points for rewards | Completed |
|
||||
|
||||
## 2. Installation
|
||||
|
||||
| Prerequisite | Optional |
|
||||
| :--- | :--- |
|
||||
| CY_beibao | Optional |
|
||||
| EssentialsX | Optional |
|
||||
|
||||
### 2.1 Download
|
||||
Download the plugin from GitHub or Gitee, put it into the server plugin directory, restart the server.
|
||||
|
||||
### 2.2 Configuration Files
|
||||
|
||||
#### 2.2.1 messages.txt
|
||||
Controls all plugin prompt messages. Execute /sdf1_login reload after modifying.
|
||||
|
||||
#### 2.2.2 afk_whitelist.txt
|
||||
Players in this config will not be affected by AFK kick.
|
||||
|
||||
#### 2.2.3 SMTP Settings.txt
|
||||
Controls SMTP mail server config, used for password recovery emails.
|
||||
|
||||
#### 2.2.4 Plugin Settings.txt
|
||||
Controls the entire plugin including account limits, admin password, etc.
|
||||
|
||||
#### 2.2.5 chat.txt
|
||||
Controls chat filter for handling illegal links.
|
||||
|
||||
## 3. Commands
|
||||
|
||||
- Sdf1_login main command
|
||||
- Sdf1_login reload reload config
|
||||
- reg register account
|
||||
- login login to account
|
||||
- l login to account
|
||||
|
||||
## 4. Screenshots
|
||||
|
||||
Main Interface
|
||||
|
||||

|
||||
|
||||
Invite Data
|
||||
|
||||

|
||||
|
||||
Points Shop
|
||||
|
||||

|
||||
|
||||
Task Center
|
||||
|
||||

|
||||
|
||||
Trash Can
|
||||
|
||||

|
||||
|
||||
Admin Panel
|
||||
|
||||

|
||||
|
||||
Admin Panel - User Management
|
||||
|
||||

|
||||
|
||||
Ticket Center
|
||||
|
||||

|
||||
|
||||
Ticket Management
|
||||
|
||||

|
||||
|
||||
My Tickets
|
||||
|
||||

|
||||
|
||||
Submit Ticket
|
||||
|
||||

|
||||
|
||||
Provider List
|
||||
|
||||

|
||||
|
||||
Provider Grab Hall
|
||||
|
||||

|
||||
|
||||
Provider - Order Interface
|
||||
|
||||

|
||||
|
||||
Admin - Ticket Detail
|
||||
|
||||

|
||||
|
||||
## 5. Download Links
|
||||
|
||||
- GitHub: https://github.com/ypsdf1/sdf1_login
|
||||
- Gitee: https://gitee.com/nihaoshidifu/sdf1_login
|
||||
@@ -1,112 +1,612 @@
|
||||
# Sdf1系列登录插件使用说明
|
||||
|
||||
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
|
||||
|
||||
## 1. 功能介绍
|
||||
|
||||
| 名字 | 描述/作用 | 施工状态 |
|
||||
| :--- | :--- | :--- |
|
||||
| 名字 | 描述/作用 | 施工状态 |
|
||||
| :---: | :---: | :---: |
|
||||
| 登录 | 用户登录/注册 | 已完成 |
|
||||
| 世界垃圾箱 | 清理服务器凋落物 | 半成品 |
|
||||
| 工单系统 | 用户提交工单 | 已完成 |
|
||||
| 世界垃圾箱 | 清理服务器凋落物 | 已完成 |
|
||||
| 工单系统 | 用户提交工单 | 已完成 |
|
||||
| 签到系统 | 签到积分换奖励 | 已完成 |
|
||||
| 新人礼包 | 新玩家7日礼包 | 已完成 |
|
||||
| 挂机检测 | 挂机检测踢出 | 已完成 |
|
||||
| 自定义菜单 | 可以自己diy指令 | 已完成 |
|
||||
| 自定义菜单图标 | 可以用自己喜欢的物品当图标 | 已完工 |
|
||||
| 商城系统| 内置游戏商人可供销售商品 | 已完工 |
|
||||
| 虚拟货币 | 供签到、游戏商人使用的货币系统 | 已完工 |
|
||||
|
||||
|
||||
## 2.安装说明:
|
||||
|
||||
| 前置插件 | 选装类型 | 功能作用 |
|
||||
| :--- | :--- | :--- |
|
||||
| [CY_beibao](https://git.ypshidifu.cn/youpaishidifu/CY_beibao) | 可选,推荐 | 云背包,积分商城可购买背包容量 |
|
||||
| EssentialsX | 可选 | 主流经济系统 |
|
||||
| [sdf1](https://git.ypshidifu.cn/youpaishidifu/Sdf1_plugin) | 可选,推荐 | CDK兑换系统 |
|
||||
|
||||
## 2. 安装说明
|
||||
|
||||
| 前置插件 | 选装类型 |
|
||||
| :--- | :--- |
|
||||
| CY_beibao | 可选 |
|
||||
| EssentialsX | 可选 |
|
||||
|
||||
### 2.1 下载
|
||||
您可以从GitHub和Gitee下载插件,将其放入服务器plugin目录,重启服务器即可。
|
||||
您可以从GitHub和Gitee、gitcode下载到插件,将其丢入服务器plugin目录,重启服务器即可。
|
||||
|
||||
### 2.2 配置文件
|
||||
### 2.2. 配置文件
|
||||
|
||||
#### 2.2.1 消息.txt
|
||||
此文件控制整个插件的所有提示消息,修改后执行 /sdf1_login reload 重载插件。
|
||||
此文件控制着整个插件的所有提示消息,您可以根据自己的需求修改,修改后需要执行 /sdf1_login reload 重载插件。支持[颜色代码](https://zh.minecraft.wiki/w/%E6%A0%BC%E5%BC%8F%E5%8C%96%E4%BB%A3%E7%A0%81)<br>
|
||||
同时兼容`<b> </b>`标签,对段落加粗,`\n 和 <br>`进行段落换行
|
||||
|
||||
#### 2.2.2 挂机白名单.txt
|
||||
挂机踢出开启时,该配置下的玩家不受影响。
|
||||
|
||||
#### 2.2.3 SMTP设置.txt
|
||||
控制SMTP邮件服务器配置,用于找回密码时发送邮件。
|
||||
```txt
|
||||
# ==========================================
|
||||
# Sdf1_login 消息配置文件
|
||||
# ==========================================
|
||||
# 颜色代码使用 § 符号
|
||||
# §0黑色 §1深蓝 §2深绿 §3深青
|
||||
# §4深红 §5深紫 §6金色 §7灰色
|
||||
# §8深灰 §9蓝色 §a绿色 §b浅蓝
|
||||
# §c红色 §d粉色 §e黄色 §f白色
|
||||
# §l粗体 §n下划线 §o斜体 §r重置
|
||||
#
|
||||
# 占位符用 {xxx} 表示,运行时自动替换
|
||||
# 支持 <b>粗体</b> 标签
|
||||
# 支持 \n 或 <br> 换行
|
||||
# 修改后执行 /sdf1_login reload 重载
|
||||
# ==========================================
|
||||
|
||||
#### 2.2.4 插件设置.txt
|
||||
管控整个插件,包括账号注册限制、管理员密码等。
|
||||
# ---- 登录注册相关 ----
|
||||
# login_timeout: 登录超时被踢出时的提示
|
||||
login_timeout=§c登录超时,请重新登录
|
||||
# not_registered: 未注册玩家收到的提示
|
||||
not_registered=§e您尚未注册,请使用 /reg <密码>
|
||||
# not_logged_in: 已注册但未登录的提示
|
||||
not_logged_in=§e您尚未登录,请使用 /login <密码>
|
||||
# already_registered: 已注册玩家再次注册时的提示
|
||||
already_registered=§c您已注册过账号
|
||||
# already_logged_in: 已登录玩家再次登录时的提示
|
||||
already_logged_in=§c您已登录
|
||||
# reg_success: 注册成功提示
|
||||
reg_success=§a注册成功!欢迎来到服务器
|
||||
# login_success: 登录成功提示
|
||||
login_success=§a登录成功!
|
||||
# login_failed: 密码错误提示
|
||||
login_failed=§c密码错误,请重试
|
||||
|
||||
#### 2.2.5 chat.txt
|
||||
控制聊天过滤,处理违规链接。
|
||||
# ---- 密码相关 ----
|
||||
# password_wrong: 旧密码验证失败
|
||||
password_wrong=§c密码错误
|
||||
# password_changed: 密码修改成功
|
||||
password_changed=§a密码修改成功!
|
||||
# password_format_error: 密码格式不符合要求
|
||||
password_format_error=§c密码需6位以上,含大小写字母和数字
|
||||
# password_same: 新密码与旧密码相同
|
||||
password_same=§c新密码不能与旧密码相同
|
||||
# reset_no_email: 找回密码时未绑定邮箱
|
||||
reset_no_email=§c您未绑定邮箱,无法找回密码
|
||||
# reset_sent: 临时密码已发送到邮箱
|
||||
reset_sent=§a临时密码已发送到您的邮箱,请查收
|
||||
|
||||
## 3. 指令
|
||||
# ---- 签到积分相关 ----
|
||||
# checkin_already: 今日已签到
|
||||
checkin_already=§c您今日已签到
|
||||
# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率
|
||||
checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}
|
||||
# points_insufficient: 积分不足
|
||||
points_insufficient=§c积分不足,当前: {points}
|
||||
# points_purchase_success: 积分购买成功
|
||||
points_purchase_success=§a购买成功!消耗 {count} 积分
|
||||
|
||||
- Sdf1_login 主指令
|
||||
- Sdf1_login reload 重载配置
|
||||
- reg 注册账号
|
||||
- login 登录账号
|
||||
- l 登录账号
|
||||
# ---- 挂机检测相关 ----
|
||||
# afk_set_enabled: 挂机检测开启
|
||||
afk_set_enabled=§a挂机检测已开启
|
||||
# afk_set_disabled: 挂机检测关闭
|
||||
afk_set_disabled=§c挂机检测已关闭
|
||||
# afk_kick: 因挂机被踢出
|
||||
afk_kick=§c因挂机过久被踢出
|
||||
|
||||
## 4. 运行截图
|
||||
# ---- 邮箱相关 ----
|
||||
# email_set: 邮箱设置成功
|
||||
email_set=§a邮箱设置成功:{email}
|
||||
# email_format_error: 邮箱格式错误
|
||||
email_format_error=§c邮箱格式不正确
|
||||
|
||||
# ---- 邀请相关 ----
|
||||
# invite_code_generated: 邀请码生成
|
||||
invite_code_generated=§a邀请码:{code}
|
||||
# invite_used: 使用邀请码成功
|
||||
invite_used=§a邀请成功,邀请人:{player}
|
||||
# invite_self: 使用自己的邀请码
|
||||
invite_self=§c不能使用自己的邀请码
|
||||
# invite_not_found: 邀请码不存在
|
||||
invite_not_found=§c邀请码不存在
|
||||
# invite_referral_bonus: 被邀请人签到后邀请人获得奖励
|
||||
invite_referral_bonus=§a邀请奖励!获得 {points} 积分
|
||||
|
||||
# ---- 新人礼包相关 ----
|
||||
# gift_not_ready: 礼包条件未满足
|
||||
gift_not_ready=§c礼包条件未满足
|
||||
# gift_claimed: 礼包领取成功
|
||||
gift_claimed=§a礼包领取成功!
|
||||
|
||||
# ---- 审批与IP限制 ----
|
||||
# need_approval: 注册需审批
|
||||
need_approval=§e注册需审批 (工单#{id})
|
||||
# ip_max_accounts: IP注册已达上限
|
||||
ip_max_accounts=§cIP注册已达上限
|
||||
|
||||
# ---- 管理员操作 ----
|
||||
# admin_delete_confirm: 管理员删除账号确认提示
|
||||
admin_delete_confirm=§c请在聊天中输入玩家名确认删除
|
||||
|
||||
# ---- 聊天过滤 ----
|
||||
# chat_muted: 禁言玩家尝试聊天
|
||||
chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天
|
||||
# chat_url_blocked: 发送违规链接被拦截
|
||||
# {url}=检测到的链接地址
|
||||
chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}
|
||||
# chat_url_violation: 当前违规次数
|
||||
# {count}=当前违规次数
|
||||
chat_url_violation=§7当前违规次数: §e{count}
|
||||
# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数
|
||||
chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)
|
||||
# chat_url_broadcast: 全服通报,{player}=玩家名
|
||||
chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚
|
||||
|
||||
# ---- 通用 ----
|
||||
# 未知参数: 未知命令或权限不足时的提示
|
||||
未知参数=§c未知参数或权限不足
|
||||
# only_player: 仅玩家可用的命令
|
||||
# only_player=§c此命令仅玩家可用
|
||||
|
||||
```
|
||||
|
||||
### 2.2.2 挂机白名单.txt
|
||||
当挂机踢出开启时,该配置下的玩家不受挂机提出影响
|
||||
|
||||
### 2.2.3 SMTP设置.txt
|
||||
|
||||
此文件控制着SMTP邮件服务器的配置,用于找回密码时发送邮件。<br>可通过GUI进行配置,也可以手动修改此配置文件。修改后,重载插件即可生效
|
||||
|
||||
```txt
|
||||
# ===== SMTP 邮件服务器配置 =====
|
||||
smtp地址=smtp.example.com
|
||||
smtp端口=465
|
||||
smtp账号=mail@example.com
|
||||
smtp密码=example.com
|
||||
发件人名称=Sdf1_login
|
||||
smtp加密=true
|
||||
验证码接收邮箱=example@example.com
|
||||
|
||||
```
|
||||
|
||||
### 2.2.4 插件设置.txt
|
||||
|
||||
此配置管控这整个插件,包括玩家能注册多少个账号、默认的管理员面板密码多少、tag包含什么视为管理员等
|
||||
|
||||
```txt
|
||||
# ===== Sdf1_login 插件设置 =====
|
||||
管理标签=admin
|
||||
管理密码=qweasd
|
||||
每IP最大账号数=3
|
||||
审批模式=manual
|
||||
自动审批延迟分钟=30
|
||||
登录超时秒数=60
|
||||
挂机踢出=false
|
||||
挂机超时秒数=300
|
||||
服务商积分倍率=2.0
|
||||
基础经济奖励=100
|
||||
经济奖励每评分=50.0
|
||||
报单人积分奖励=10
|
||||
# ---- 签到盲盒奖励 ----
|
||||
签到固定金额=0
|
||||
签到最小金额=10
|
||||
签到最大金额=100
|
||||
签到给积分=true
|
||||
签到积分数量=10
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 2.2.5 chat.txt
|
||||
该文件控制着聊天过滤,当玩家发送违规链接时,会自动根据配置规则进行处理。<br>
|
||||
可用的处罚规则:
|
||||
- warn: 警告
|
||||
- mute: 禁言
|
||||
- kick: 踢出
|
||||
- ban: 封禁
|
||||
- banip:封禁IP
|
||||
|
||||
```txt
|
||||
# Sdf1_chat 聊天过滤配置
|
||||
通知管理员: true
|
||||
全服通报: true
|
||||
禁言时长: 300
|
||||
|
||||
白名单:
|
||||
*.minecraft.net
|
||||
baidu.com
|
||||
github.com
|
||||
|
||||
处罚规则:
|
||||
1:warn:0
|
||||
2:warn:0
|
||||
3:mute:300
|
||||
5:mute:600
|
||||
8:kick:0
|
||||
10:ban:0
|
||||
|
||||
白名单玩家:
|
||||
|
||||
```
|
||||
### 2.2.6 radio_config.txt
|
||||
```txt
|
||||
# ============ Sdf1 Radio Config ============
|
||||
#
|
||||
# resource-pack-url
|
||||
# 填外部地址(HTTP或HTTPS均可)
|
||||
# 例: https://example.com/radiopack.zip
|
||||
# 留空 = 使用内置HTTP服务
|
||||
#
|
||||
# http-port
|
||||
# 内置HTTP端口(仅resource-pack-url为空时生效)
|
||||
#
|
||||
# ============ Settings ===============
|
||||
|
||||
resource-pack-url=https://*.ypshidifu.cn/|*.mcserver.ccwu.cc
|
||||
http-port=443
|
||||
```
|
||||
- radio_config这里可以使用通配符域名,当使用通配符时,系统随机生成url,拒绝过资源包的玩家,每次都会收到提示。并且自1.2版本起,拒绝资源包=放弃游玩服务器。如果您无需广播功能,可以不放ogg文件进入`./plugin/radio/ogg`。这样服务端不会自动生成资源包,也不会强制下载了
|
||||
|
||||
### 2.2.7 菜单.txt
|
||||
- 自定义菜单配置文件
|
||||
|
||||
```json
|
||||
# 服务器菜单
|
||||
# 使用类json格式
|
||||
// 支持使用://Java单行注释、#yml单行注释、/* */块注释,也支持<!--- --->的块注释
|
||||
|
||||
{
|
||||
标题:主城传送
|
||||
指令:/dom tp 主城区 # 这里如果是xxx.txt则表示进入二级目录
|
||||
权限类型:玩家
|
||||
图标:COMPASS
|
||||
}
|
||||
|
||||
{
|
||||
标题:随机传送
|
||||
指令:/rtp
|
||||
权限类型:玩家
|
||||
图标:ENDER_PEARL
|
||||
}
|
||||
|
||||
{
|
||||
标题:擂台比武
|
||||
指令:/dom tp PVP
|
||||
权限类型:玩家
|
||||
图标:netherite_spear
|
||||
}
|
||||
|
||||
{
|
||||
标题:领地操作
|
||||
指令:/dom
|
||||
权限类型:玩家
|
||||
图标:paper
|
||||
}
|
||||
{
|
||||
标题:逛商店
|
||||
指令:/dom tp shop
|
||||
权限类型:玩家
|
||||
图标:honeycomb
|
||||
}
|
||||
|
||||
{
|
||||
标题:传送开关
|
||||
指令:/tptoggle
|
||||
权限类型:玩家
|
||||
图标:redstone_torch
|
||||
}
|
||||
|
||||
{
|
||||
标题:返回上一个位置
|
||||
指令:/back
|
||||
权限类型:玩家
|
||||
图标:red_bed
|
||||
}
|
||||
{
|
||||
标题:前往小黑塔
|
||||
指令:/dom tp 小黑塔
|
||||
权限类型:玩家
|
||||
图标:dragon_head
|
||||
}
|
||||
{
|
||||
标题:云背包
|
||||
指令:/cy
|
||||
权限类型:玩家
|
||||
图标:potato
|
||||
}
|
||||
{
|
||||
标题:自杀
|
||||
指令:/suicide
|
||||
权限类型:玩家
|
||||
图标:wooden_axe
|
||||
}
|
||||
{
|
||||
标题:游戏商人
|
||||
指令:/shop
|
||||
权限类型:玩家
|
||||
图标:shield
|
||||
格子:28
|
||||
}
|
||||
```
|
||||
### 2.2.8 商品配置
|
||||
- 配置此处的文件,负责展示游戏内所销售的商品。<br>
|
||||
- 一个文件名,对应一个商品分类,使用md文件格式
|
||||
```
|
||||
# 建筑材料
|
||||
|
||||
| ID | 品名 | 材质 | 购入价 | 售出价 | 库存 | 本小时销量 | 总销量 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| STONE | 石头 | STONE | 10 | -1 | -1 | 0 | 0 |
|
||||
| COBBLESTONE | 圆石 | COBBLESTONE | 5 | -1 | -1 | 0 | 26 |
|
||||
| OAK_PLANKS | 橡木木板 | OAK_PLANKS | 8 | 3 | -1 | 0 | 0 |
|
||||
| BRICKS | 砖块 | BRICKS | 12 | -1 | -1 | 0 | 0 |
|
||||
| GLASS | 玻璃 | GLASS | 15 | 6 | -1 | 0 | 0 |
|
||||
| QUARTZ_BLOCK | 石英块 | QUARTZ_BLOCK | 20 | 8 | -1 | 0 | 0 |
|
||||
| AMETHYST_BLOCK | 紫水晶 | AMETHYST_BLOCK | 10 | 0 | -1 | 0 | 999 |
|
||||
| OCHRE_FROGLIGHT | 赭黄蛙鸣灯 | OCHRE_FROGLIGHT | 100 | 0 | -1 | 0 | 1000 |
|
||||
|
||||
```
|
||||
|
||||
## 2.3 商城部分介绍
|
||||
1. 自助退款,买多买错,不用慌,一件退单很安心<br>
|
||||
2. 购物车,买的多怕买错,先加购物车,后续统一结算<br>
|
||||
3. 害怕订单找不到?一键查看历史订单<br>
|
||||

|
||||
4. 玩家要耍赖皮,拿着小票来找麻烦怎么办?退款订单自动贴标,拒绝白嫖
|
||||
<br>
|
||||

|
||||
5. 想查看玩家消费记录?小手一点,明明白白<br>
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
## 2.4 打印流水部分介绍
|
||||
1. 管理员怀疑某个玩家刷钱了,怎么办?/printer 玩家名,一检查流水,很安心。很多人都刷了怎么办?/printer不加参数,一键导出全服流水,查他个底朝天<br>
|
||||
2. 忘记删除怎么办?插件每分钟自动检测,旧文件超过24小时,自动删
|
||||
|
||||
|
||||
|
||||
## 3. 指令参数及介绍
|
||||
|
||||
- Sdf1_login 主指令<br>
|
||||
- Sdf1_login reload 重载配置文件<br>
|
||||
- reg 注册账号<br>
|
||||
- login 登录账号<br>
|
||||
- l 登录账号<br>
|
||||
- sdf1_login del 删除玩家账号<br>
|
||||
- sdf1_login radio 停止播放广播<br>
|
||||
- sdf1_login radio reload 重载广播并重新打包资源包<br>
|
||||
- menu 开/关 是否开启菜单<br>
|
||||
- printer 打印流水数据,支持不带参数&带玩家名为参数<br>
|
||||
- sdf1_login printer 作用同printer<br>
|
||||
- sdf1_login shop 商城
|
||||
- protect 区域防护
|
||||
|
||||
|
||||
|
||||
## 4. 自定义菜单物品
|
||||
- 打开主界面,选择"我的"进入<br>
|
||||

|
||||
- 将你的自定义物品与雪球交换<br>
|
||||

|
||||
- 随后系统会给你重新发放菜单,并且此时图标就是你的自定义物品了
|
||||
|
||||
## 5. pvp战神榜
|
||||
- 通过此功能,你可以在服务器里制作大服务器同款的击杀排行榜,而且能精准定位指定区域<br>
|
||||

|
||||
- 并且有着网游同款的击杀通报<br>
|
||||

|
||||
- 播报逻辑是:<br>
|
||||
- 3杀<br>
|
||||
- 5杀<br>
|
||||
- 5杀以上每杀一次都是一条播报<br>
|
||||
- 终结者通报,触发连杀的玩家被终结也会有全服通报
|
||||
|
||||
## 6. 区域防护
|
||||
- 通过此功能,您可在服务器里需要防护的地方(比如展示框不想被玩家随便破坏、主城不准玩家袭击等)加以防护。<br>
|
||||
- 还能够保护指定区域的玩家不受任何伤害。<br>
|
||||
|
||||
| 指令 | 参数1| 参数2 | 参数3 | 结果 |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| protect | 无 | 无 | 无 | 帮助信息 |
|
||||
| protect | add | 区域名 | 玩家名 | 加白指定玩家到区域 |
|
||||
| protect | add | 玩家名 | 无| 加白指定玩家为全局 |
|
||||
| protect | additem | 区域名 | 物品名 | 加黑指定物品到区域 |
|
||||
| protect | additem | 物品名 | 无 | 加黑指定物品到全局 |
|
||||
| protect | addname | 区域名 | 名字 | 加白指定生物名字到区域和平模式白名单 |
|
||||
| protect | addplayer | 区域名 | 玩家名字 | 加白指定玩家到区域强制模式豁免名单 |
|
||||
| protect | remove | 玩家名字 | 无 | 移除玩家为全白 |
|
||||
| protect | remove | 区域名 | 玩家名 | 移除玩家为区白 |
|
||||
| protect | removeitem | 物品名 | 无 | 删除指定物品为全黑
|
||||
| protect | removeitem | 区域名 | 物品名 | 删除指定物品为区黑
|
||||
| protect | removename | 区域名 | 名字 | 删除指定名字为区域和平模式白名单 |
|
||||
| protect | removeplayer | 区域名 | 名字 | 删除指定玩家为强制模式豁免名单 |
|
||||
| protect | 参数不分顺序 | 区名可以写这里也可以写后面 | 玩家名可以写这里也可以写前面 | 完美的宝宝化 |
|
||||
|
||||
示范:
|
||||
- `/protect add 末地保护区 players` 添加players玩家到末地保护区<br>
|
||||
- `/protect removeitem 主城区 TNT` 从主城删了TNT这个黑名单物品<br>
|
||||
- `/protect 筑城区 addwhite zhangsan` 把zhangsan加白到主城区的模式豁免名单。<br>
|
||||
- 所有参数`任意排序`,`区域名字`随便填写,强大的`兜底`会自动识别"宝宝画板",你可以放心、大胆的乱写
|
||||
|
||||
|
||||
| 参数 | 效果 |
|
||||
| :---: | :---: |
|
||||
| 和平模式 | 所有敌对生物全部进虚空 |
|
||||
| 禁止袭击 | 立即清理玩家身上的袭击效果,传灾厄村民进虚空 |
|
||||
| 清除XX效果 | 立即清除玩家身上的效果
|
||||
| 给与xx效果 | 根据配置文件,给予玩家指定等级、时长的效果
|
||||
|
||||
## 生物属性对照表
|
||||
| 中文名称 | 类型 | 附加属性 |
|
||||
| :---: | :----: | :----: |
|
||||
| 溺尸 | 近战/远程 | 亡灵、水生 |
|
||||
| 骷髅 | 远程 | 亡灵 |
|
||||
| 流浪者 | 远程 | 亡灵、寒冷群系变种 |
|
||||
| 沼骸 | 远程 | 亡灵、沼泽变种 |
|
||||
| 焦骸 | 远程 | 亡灵、沙漠变种、免疫阳光 |
|
||||
| 掠夺者 | 远程 | 灾厄村民、袭击成员 |
|
||||
| 卫道士 | 近战 | 灾厄村民、袭击成员 |
|
||||
| 幻术师 | 远程 | 灾厄村民、Java专属、隐身分身 |
|
||||
| 旋风人 | 远程 | 试炼密室生成、免疫弹射物、强击退 |
|
||||
| 监守者 | 近战/声波远程 | 深暗之域、附带盲视效果 |
|
||||
| 远古守卫者 | 近战/激光远程 | 海底神殿、水下、附加挖掘疲劳 |
|
||||
| 守卫者 | 近战/激光远程 | 海底神殿、水下 |
|
||||
|
||||
|
||||
配置文件解析:<br>
|
||||
- 支持注释格式:`Java的//单行注释`、`yml的#单行注释`、`HTML的<!--- --->注释`和`Java/* */块注释`
|
||||
```txt
|
||||
# 区域: 主城区
|
||||
起点: world,-130,-713
|
||||
终点: world,-427,-560
|
||||
高度范围: 0-255
|
||||
|
||||
没收物品: 蘑菇,毒蘑菇 #没收哪些物品(末地推荐这两个)
|
||||
// 禁止放置方块 #保护区内,非区白/全白玩家禁止放置方块
|
||||
// 禁止破坏方块 # 同上
|
||||
禁止PVP # 玩家之间不能PVP
|
||||
禁止摔伤 # 摔落无法造成任何伤害
|
||||
禁止饥饿 # 不会消耗饱食度
|
||||
禁止一切伤害 # 任何怪物都无法造成伤害
|
||||
# 禁止丢弃物品
|
||||
# 禁止末影珍珠
|
||||
# 禁止使用弓箭
|
||||
# 禁止骑乘
|
||||
// 和平模式 // 所有的敌对生物全部传送进虚空
|
||||
禁止袭击
|
||||
禁止交互展示框 # 核心,任何玩家不能触碰展示框。区白/全白玩家除外
|
||||
// 清除所有效果 # 清理所有负面效果
|
||||
效果 村庄英雄 255 999999 # 给予玩家效果。格式为: 效果 效果名字 等级(最高255) 时间
|
||||
效果 生命提升 255 999999
|
||||
效果 抗性提升 255 999999
|
||||
效果 抗火 255 999999
|
||||
# 禁止药水
|
||||
禁止爆炸
|
||||
禁止使用物品: TNT
|
||||
# 效果: 夜视 1 999
|
||||
进入提示: 欢迎来到保护区 # 是否开启进入提示,即自定义提示
|
||||
离开提示: 已离开保护区
|
||||
没收提示: 你的违禁品已被没收
|
||||
通报批评: §c§l{player} §f在{area}违规携带违禁品,被没收 §e{count}§f件! # 通报违禁品。{player} 玩家名 {count}数量 {area}区域
|
||||
# 惩罚命令: ban {player} 60s 违规
|
||||
|
||||
|
||||
```
|
||||
|
||||
## php端说明:
|
||||
1. 默认密码ypshidifu2026<br>
|
||||
2. 工单系统全面支持markdown语法:
|
||||
```md
|
||||
# 一级标题
|
||||
## 二级标题
|
||||
### 三级标题
|
||||
#### 四级标题
|
||||
##### 五级标题
|
||||
###### 六级标题
|
||||
|
||||
表格语法
|
||||
|
||||
| 表格列1 | 表格列2 |
|
||||
| --- | --- |
|
||||
| 表格1 | 表格2 |
|
||||
|
||||
>单嵌套语法
|
||||
|
||||
>嵌套块
|
||||
>
|
||||
>>语法
|
||||
|
||||
 图片语法
|
||||
[]() 超链接语法
|
||||
<br>换行语法
|
||||
代码块语法
|
||||
```
|
||||
## 工单状态:
|
||||
|
||||
| 状态名 | 是否可回复 | 备注说明 |
|
||||
| :---: | :---: | :---: |
|
||||
| 已提交 | 可回复 | 初始化状态 |
|
||||
| 已回复 | 可回复 | 玩家自己回复了/管理员/服务商回复了 |
|
||||
| 已完结 | 不可回复 | 管理员驳回了工单请求 |
|
||||
| 已撤销 | 不可回复 | 用户撤销了工单请求 |
|
||||
|
||||
## 服务商说明
|
||||
- 若您需要授权某个玩家为服务商,请登录游戏,打开工单系统-管理面板-添加服务商,然后输入他的名字即可邀请服务商
|
||||
|
||||
|
||||
## 运行截图
|
||||
|
||||
主界面
|
||||
|
||||

|
||||

|
||||
|
||||
邀请数据
|
||||
|
||||

|
||||

|
||||
|
||||
积分商店
|
||||
|
||||

|
||||

|
||||
|
||||
任务中心
|
||||
|
||||

|
||||

|
||||
|
||||
垃圾箱
|
||||
|
||||

|
||||

|
||||
|
||||
管理员面板
|
||||
|
||||

|
||||

|
||||
|
||||
管理面板-用户管理
|
||||

|
||||
|
||||

|
||||
|
||||
工单中心
|
||||
|
||||

|
||||

|
||||
|
||||
工单管理
|
||||
|
||||

|
||||

|
||||
|
||||
我的工单
|
||||

|
||||
|
||||

|
||||
|
||||
提交工单
|
||||
|
||||

|
||||

|
||||
|
||||
服务商列表
|
||||
|
||||

|
||||

|
||||
|
||||
服务商抢单大厅
|
||||
|
||||

|
||||

|
||||
|
||||
服务商-接单界面
|
||||

|
||||
|
||||

|
||||
管理员-工单详情页面
|
||||

|
||||
|
||||
管理员-工单详情
|
||||
商城主界面
|
||||

|
||||
|
||||

|
||||
商城购物车
|
||||

|
||||
|
||||
## 5. 下载地址
|
||||
商城退单
|
||||

|
||||
|
||||
- GitHub: https://github.com/ypsdf1/sdf1_login
|
||||
- Gitee: https://gitee.com/nihaoshidifu/sdf1_login
|
||||
## 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)
|
||||
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -11,7 +11,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<java.version>25</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -33,12 +33,7 @@
|
||||
<version>1.21.4-R0.1-SNAPSHOT</version>
|
||||
<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>
|
||||
@@ -54,10 +49,10 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<release>25</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AFKManager {
|
||||
|
||||
// 玩家最后活动时间(空闲才开始计时)
|
||||
private final Map<UUID, Long> lastAction =
|
||||
new HashMap<>();
|
||||
// 每个玩家当前的警告阶段
|
||||
private final Map<UUID, Integer> lastStage =
|
||||
new HashMap<>();
|
||||
// 是否已进入挂机检测(false=还在活动,不计时)
|
||||
private final Map<UUID, Boolean> afkTracking =
|
||||
new HashMap<>();
|
||||
|
||||
private final ConfigManager config;
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public AFKManager(ConfigManager config,
|
||||
JavaPlugin plugin) {
|
||||
this.config = config;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void startCheck() {
|
||||
new BukkitRunnable() {
|
||||
public void run() {
|
||||
tick();
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.afkEnabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
String name = p.getName();
|
||||
UUID uuid = p.getUniqueId();
|
||||
|
||||
if (!isLoggedIn(name)) continue;
|
||||
if (config.getAfkWhitelist().contains(name))
|
||||
continue;
|
||||
|
||||
// 确保有记录
|
||||
if (!lastAction.containsKey(uuid)) {
|
||||
lastAction.put(uuid, now);
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
long last = lastAction.get(uuid);
|
||||
long idle = (now - last) / 1000;
|
||||
int timeout = config.afkTimeout;
|
||||
|
||||
// 还在活动范围内:不进入挂机检测
|
||||
if (idle < 60) {
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 超过60秒无活动:开始挂机检测
|
||||
afkTracking.put(uuid, true);
|
||||
long remain = timeout - idle;
|
||||
|
||||
// 已超时:踢出
|
||||
if (remain <= 0) {
|
||||
p.kickPlayer("§c§l你已挂机 "
|
||||
+ fmtSec(idle) + ",被踢出");
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int stage = lastStage.getOrDefault(uuid, 0);
|
||||
|
||||
// 阶段3:最后10秒
|
||||
if (remain <= 10) {
|
||||
if (stage < 3) {
|
||||
lastStage.put(uuid, 3);
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + remain + " 秒踢出",
|
||||
0, 22, 3);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段2:1分钟~10秒
|
||||
if (remain <= 60) {
|
||||
if (stage < 2) {
|
||||
lastStage.put(uuid, 2);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + fmtSec(remain) + " 踢出",
|
||||
5, 25, 5);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段1:3分钟提醒
|
||||
if (remain <= 180 && stage < 1) {
|
||||
lastStage.put(uuid, 1);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§f还剩 " + fmtSec(remain) + " 踢出",
|
||||
10, 60, 20);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int delay = i * 10;
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
if (p.isOnline()) playSound(p);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (remain > 180) {
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家有活动时调用(移动/交互/放置/挖掘等)
|
||||
* 重置计时,退出挂机检测
|
||||
*/
|
||||
public void recordAction(UUID uuid) {
|
||||
lastAction.put(uuid, System.currentTimeMillis());
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家下线时调用
|
||||
* 彻底停止计时,清理所有数据
|
||||
*/
|
||||
public void remove(UUID uuid) {
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家当前是否在挂机检测中
|
||||
*/
|
||||
public boolean isAfkTracking(UUID uuid) {
|
||||
return afkTracking.getOrDefault(uuid, false);
|
||||
}
|
||||
|
||||
private boolean isLoggedIn(String name) {
|
||||
return ((Main) plugin).getLoggedIn()
|
||||
.contains(name);
|
||||
}
|
||||
|
||||
private void playSound(Player p) {
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_CHIME,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_BASS,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_PLING,
|
||||
1.0f, 0.79f);
|
||||
}
|
||||
|
||||
private String fmtSec(long sec) {
|
||||
if (sec < 60) return sec + "秒";
|
||||
long m = sec / 60;
|
||||
long s = sec % 60;
|
||||
return m + "分" + (s > 0 ? s + "秒" : "");
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AFKManager {
|
||||
|
||||
// 玩家最后活动时间(空闲才开始计时)
|
||||
private final Map<UUID, Long> lastAction =
|
||||
new HashMap<>();
|
||||
// 每个玩家当前的警告阶段
|
||||
private final Map<UUID, Integer> lastStage =
|
||||
new HashMap<>();
|
||||
// 是否已进入挂机检测(false=还在活动,不计时)
|
||||
private final Map<UUID, Boolean> afkTracking =
|
||||
new HashMap<>();
|
||||
|
||||
private final ConfigManager config;
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public AFKManager(ConfigManager config,
|
||||
JavaPlugin plugin) {
|
||||
this.config = config;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void startCheck() {
|
||||
new BukkitRunnable() {
|
||||
public void run() {
|
||||
tick();
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.afkEnabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
String name = p.getName();
|
||||
UUID uuid = p.getUniqueId();
|
||||
|
||||
if (!isLoggedIn(name)) continue;
|
||||
if (config.getAfkWhitelist().contains(name))
|
||||
continue;
|
||||
|
||||
// 确保有记录
|
||||
if (!lastAction.containsKey(uuid)) {
|
||||
lastAction.put(uuid, now);
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
long last = lastAction.get(uuid);
|
||||
long idle = (now - last) / 1000;
|
||||
int timeout = config.afkTimeout;
|
||||
|
||||
// 还在活动范围内:不进入挂机检测
|
||||
if (idle < 60) {
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 超过60秒无活动:开始挂机检测
|
||||
afkTracking.put(uuid, true);
|
||||
long remain = timeout - idle;
|
||||
|
||||
// 已超时:踢出
|
||||
if (remain <= 0) {
|
||||
p.kickPlayer("§c§l你已挂机 "
|
||||
+ fmtSec(idle) + ",被踢出");
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int stage = lastStage.getOrDefault(uuid, 0);
|
||||
|
||||
// 阶段3:最后10秒
|
||||
if (remain <= 10) {
|
||||
if (stage < 3) {
|
||||
lastStage.put(uuid, 3);
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + remain + " 秒踢出",
|
||||
0, 22, 3);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段2:1分钟~10秒
|
||||
if (remain <= 60) {
|
||||
if (stage < 2) {
|
||||
lastStage.put(uuid, 2);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
}
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + fmtSec(remain) + " 踢出",
|
||||
5, 25, 5);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 阶段1:3分钟提醒
|
||||
if (remain <= 180 && stage < 1) {
|
||||
lastStage.put(uuid, 1);
|
||||
p.sendMessage(
|
||||
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
|
||||
+ fmtSec(idle)
|
||||
+ "§f,还剩 §c"
|
||||
+ fmtSec(remain)
|
||||
+ "§f踢出");
|
||||
p.sendTitle(
|
||||
"§e§l挂机了 " + fmtSec(idle),
|
||||
"§f还剩 " + fmtSec(remain) + " 踢出",
|
||||
10, 60, 20);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int delay = i * 10;
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
if (p.isOnline()) playSound(p);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (remain > 180) {
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家有活动时调用(移动/交互/放置/挖掘等)
|
||||
* 重置计时,退出挂机检测
|
||||
*/
|
||||
public void recordAction(UUID uuid) {
|
||||
lastAction.put(uuid, System.currentTimeMillis());
|
||||
afkTracking.put(uuid, false);
|
||||
lastStage.put(uuid, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家下线时调用
|
||||
* 彻底停止计时,清理所有数据
|
||||
*/
|
||||
public void remove(UUID uuid) {
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
afkTracking.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家当前是否在挂机检测中
|
||||
*/
|
||||
public boolean isAfkTracking(UUID uuid) {
|
||||
return afkTracking.getOrDefault(uuid, false);
|
||||
}
|
||||
|
||||
private boolean isLoggedIn(String name) {
|
||||
return ((Main) plugin).getLoggedIn()
|
||||
.contains(name);
|
||||
}
|
||||
|
||||
private void playSound(Player p) {
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_CHIME,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_BASS,
|
||||
1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(),
|
||||
Sound.BLOCK_NOTE_BLOCK_PLING,
|
||||
1.0f, 0.79f);
|
||||
}
|
||||
|
||||
private String fmtSec(long sec) {
|
||||
if (sec < 60) return sec + "秒";
|
||||
long m = sec / 60;
|
||||
long s = sec % 60;
|
||||
return m + "分" + (s > 0 ? s + "秒" : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,159 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AccountRequestManager {
|
||||
|
||||
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
|
||||
|
||||
public static class Request {
|
||||
public final int id;
|
||||
public final String requester;
|
||||
public final String target;
|
||||
public final String ip;
|
||||
public final long time;
|
||||
public String status;
|
||||
public String denyReason;
|
||||
|
||||
public Request(int id, String requester, String target, String ip) {
|
||||
this.id = id;
|
||||
this.requester = requester;
|
||||
this.target = target;
|
||||
this.ip = ip;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.status = "PENDING";
|
||||
this.denyReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
|
||||
private int nextId = 1;
|
||||
private ApprovalMode mode = ApprovalMode.MANUAL;
|
||||
private int autoDelayMinutes = 60;
|
||||
|
||||
public AccountRequestManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ApprovalMode getMode() { return mode; }
|
||||
|
||||
public void setMode(String m) {
|
||||
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
|
||||
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
|
||||
}
|
||||
|
||||
public void setAutoDelayMinutes(int min) {
|
||||
this.autoDelayMinutes = min;
|
||||
}
|
||||
|
||||
public Request createRequest(String requester, String target, String ip) {
|
||||
int id = nextId++;
|
||||
Request req = new Request(id, requester, target, ip);
|
||||
requests.put(id, req);
|
||||
pendingByPlayer.put(requester, id);
|
||||
|
||||
if (mode == ApprovalMode.AUTO) {
|
||||
scheduleAutoApprove(id);
|
||||
} else if (mode == ApprovalMode.HYBRID) {
|
||||
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
|
||||
if (ip != null && ip.equals(targetIp)) {
|
||||
scheduleAutoApprove(id);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private void scheduleAutoApprove(int id) {
|
||||
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
|
||||
Request req = requests.get(id);
|
||||
if (req == null) return;
|
||||
String requesterName = req.requester;
|
||||
String targetName = req.target;
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
Request r = requests.get(id);
|
||||
if (r != null && "PENDING".equals(r.status)) {
|
||||
approveRequest(id);
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
|
||||
Player requester = Bukkit.getPlayer(requesterName);
|
||||
if (requester != null && requester.isOnline()) {
|
||||
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
|
||||
}
|
||||
}
|
||||
}, delayTicks);
|
||||
}
|
||||
|
||||
public boolean approveRequest(int id) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "APPROVED";
|
||||
plugin.getDb().deleteUser(req.target);
|
||||
pendingByPlayer.remove(req.requester);
|
||||
// 清背包+传送出生点+踢出
|
||||
Player target = Bukkit.getPlayer(req.target);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.getInventory().clear();
|
||||
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
|
||||
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
|
||||
target.teleport(target.getWorld().getSpawnLocation());
|
||||
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean denyRequest(int id, String reason) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "DENIED";
|
||||
req.denyReason = reason;
|
||||
pendingByPlayer.remove(req.requester);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<Request> getPendingRequests() {
|
||||
List<Request> list = new ArrayList<>();
|
||||
for (Request r : requests.values()) {
|
||||
if ("PENDING".equals(r.status)) list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Request getRequest(int id) {
|
||||
return requests.get(id);
|
||||
}
|
||||
|
||||
public boolean hasPending(String player) {
|
||||
return pendingByPlayer.containsKey(player);
|
||||
}
|
||||
|
||||
public void cancelPending(String player) {
|
||||
Integer id = pendingByPlayer.remove(player);
|
||||
if (id != null) {
|
||||
Request r = requests.get(id);
|
||||
if (r != null) r.status = "CANCELLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AccountRequestManager {
|
||||
|
||||
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
|
||||
|
||||
public static class Request {
|
||||
public final int id;
|
||||
public final String requester;
|
||||
public final String target;
|
||||
public final String ip;
|
||||
public final long time;
|
||||
public String status;
|
||||
public String denyReason;
|
||||
|
||||
public Request(int id, String requester, String target, String ip) {
|
||||
this.id = id;
|
||||
this.requester = requester;
|
||||
this.target = target;
|
||||
this.ip = ip;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.status = "PENDING";
|
||||
this.denyReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
|
||||
private int nextId = 1;
|
||||
private ApprovalMode mode = ApprovalMode.MANUAL;
|
||||
private int autoDelayMinutes = 60;
|
||||
|
||||
public AccountRequestManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ApprovalMode getMode() { return mode; }
|
||||
|
||||
public void setMode(String m) {
|
||||
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
|
||||
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
|
||||
}
|
||||
|
||||
public void setAutoDelayMinutes(int min) {
|
||||
this.autoDelayMinutes = min;
|
||||
}
|
||||
|
||||
public Request createRequest(String requester, String target, String ip) {
|
||||
int id = nextId++;
|
||||
Request req = new Request(id, requester, target, ip);
|
||||
requests.put(id, req);
|
||||
pendingByPlayer.put(requester, id);
|
||||
|
||||
if (mode == ApprovalMode.AUTO) {
|
||||
scheduleAutoApprove(id);
|
||||
} else if (mode == ApprovalMode.HYBRID) {
|
||||
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
|
||||
if (ip != null && ip.equals(targetIp)) {
|
||||
scheduleAutoApprove(id);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private void scheduleAutoApprove(int id) {
|
||||
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
|
||||
Request req = requests.get(id);
|
||||
if (req == null) return;
|
||||
String requesterName = req.requester;
|
||||
String targetName = req.target;
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
Request r = requests.get(id);
|
||||
if (r != null && "PENDING".equals(r.status)) {
|
||||
approveRequest(id);
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
|
||||
Player requester = Bukkit.getPlayer(requesterName);
|
||||
if (requester != null && requester.isOnline()) {
|
||||
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
|
||||
}
|
||||
}
|
||||
}, delayTicks);
|
||||
}
|
||||
|
||||
public boolean approveRequest(int id) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
// ★ 精准匹配:TEST 与 test 是两个人,名字必须完全相等才删
|
||||
if (plugin.getDb().findExactUserName(req.target) == null) {
|
||||
String twin = plugin.getDb()
|
||||
.findSimilarUserNameIgnoreCase(req.target);
|
||||
System.out.println("[Sdf1_login] 删号申请 #"+ id
|
||||
+ " 未执行:账号 " + req.target
|
||||
+ (twin != null ? " 不存在,仅存在大小写不同的账号 " + twin : " 不存在")
|
||||
+ "(精准匹配,未误删)");
|
||||
return false;
|
||||
}
|
||||
int deleted = plugin.getDb().deleteUser(req.target);
|
||||
if (deleted <= 0) {
|
||||
return false;
|
||||
}
|
||||
req.status = "APPROVED";
|
||||
pendingByPlayer.remove(req.requester);
|
||||
// 清背包+传送出生点+踢出(★ getPlayerExact:避免误伤大小写不同的在线玩家)
|
||||
Player target = Bukkit.getPlayerExact(req.target);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.getInventory().clear();
|
||||
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
|
||||
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
|
||||
target.teleport(target.getWorld().getSpawnLocation());
|
||||
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean denyRequest(int id, String reason) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "DENIED";
|
||||
req.denyReason = reason;
|
||||
pendingByPlayer.remove(req.requester);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<Request> getPendingRequests() {
|
||||
List<Request> list = new ArrayList<>();
|
||||
for (Request r : requests.values()) {
|
||||
if ("PENDING".equals(r.status)) list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Request getRequest(int id) {
|
||||
return requests.get(id);
|
||||
}
|
||||
|
||||
public boolean hasPending(String player) {
|
||||
return pendingByPlayer.containsKey(player);
|
||||
}
|
||||
|
||||
public void cancelPending(String player) {
|
||||
Integer id = pendingByPlayer.remove(player);
|
||||
if (id != null) {
|
||||
Request r = requests.get(id);
|
||||
if (r != null) r.status = "CANCELLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,807 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
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";
|
||||
public static final String TX_TRANSFER_OUT = "transfer_out";
|
||||
public static final String TX_TRANSFER_IN = "transfer_in";
|
||||
public static final String TX_REDEEM = "redeem";
|
||||
public static final String TX_SHOP_BUY = "shop_buy";
|
||||
public static final String TX_DAILY_SIGN = "daily_sign";
|
||||
public static final String TX_FREEZE = "freeze";
|
||||
public static final String TX_UNFREEZE = "unfreeze";
|
||||
public static final String TX_TICKET_REWARD = "ticket_reward";
|
||||
public static final String TX_TICKET_SERVICE = "ticket_service";
|
||||
|
||||
|
||||
public static final int MAX_WATER_DAYS = 14;
|
||||
public static final int ADMIN_MAX_WATER_DAYS = 30;
|
||||
|
||||
public BondManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDB();
|
||||
}
|
||||
|
||||
// ======================== 数据库初始化 ========================
|
||||
|
||||
private void initDB() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
plugin.getDataFolder(), "bond.db");
|
||||
db = DriverManager.getConnection(
|
||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
|
||||
// 债券余额(含冻结状态)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS bonds ("
|
||||
+ "player_name TEXT PRIMARY KEY,"
|
||||
+ "amount INTEGER DEFAULT 0,"
|
||||
+ "status TEXT DEFAULT 'normal')");
|
||||
|
||||
// 兼容旧表
|
||||
try {
|
||||
st.execute("ALTER TABLE bonds "
|
||||
+ "ADD COLUMN status "
|
||||
+ "TEXT DEFAULT 'normal'");
|
||||
} catch (SQLException ignored) {}
|
||||
|
||||
// 旧流水(保留兼容)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS bond_log ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "amount INTEGER NOT NULL,"
|
||||
+ "reason TEXT DEFAULT '',"
|
||||
+ "time INTEGER DEFAULT 0)");
|
||||
|
||||
// 新交易流水表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "bond_transaction ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "type TEXT NOT NULL,"
|
||||
+ "amount INTEGER NOT NULL,"
|
||||
+ "target_player TEXT DEFAULT '',"
|
||||
+ "operator TEXT DEFAULT '',"
|
||||
+ "reason TEXT DEFAULT '',"
|
||||
+ "balance_after INTEGER DEFAULT 0,"
|
||||
+ "time INTEGER DEFAULT 0)");
|
||||
|
||||
try {
|
||||
st.execute("CREATE INDEX "
|
||||
+ "IF NOT EXISTS idx_tx_pt "
|
||||
+ "ON bond_transaction("
|
||||
+ "player_name, time)");
|
||||
} catch (SQLException ignored) {}
|
||||
try {
|
||||
st.execute("ALTER TABLE bond_transaction "
|
||||
+ "ADD COLUMN balance_before "
|
||||
+ "INTEGER DEFAULT 0");
|
||||
} catch (SQLException ignored) {}
|
||||
|
||||
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"[Bond] DB init failed: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 账户状态 ========================
|
||||
|
||||
public boolean isFrozen(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT status FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String s = rs.getString("status");
|
||||
rs.close(); ps.close();
|
||||
return "frozen".equals(s);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getAccountStatus(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT status FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String s = rs.getString("status");
|
||||
rs.close(); ps.close();
|
||||
return s != null ? s : "normal";
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
return "normal";
|
||||
}
|
||||
|
||||
public void freezeAccount(String player) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET status='frozen' "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
}
|
||||
|
||||
public void unfreezeAccount(String player) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET status='normal' "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
}
|
||||
|
||||
private void ensureAccount(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR IGNORE INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,0,'normal')");
|
||||
ps.setString(1, player);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
|
||||
// ======================== 余额查询 ========================
|
||||
|
||||
public int getBonds(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT amount FROM bonds "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
int v = 0;
|
||||
if (rs.next()) v = rs.getInt("amount");
|
||||
rs.close(); ps.close();
|
||||
return v;
|
||||
} catch (SQLException e) { return 0; }
|
||||
}
|
||||
|
||||
/** 该玩家是否在 bond 表中有真实记录 */
|
||||
public boolean hasBondAccount(String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT 1 FROM bonds "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND amount > 0");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
boolean exists = rs.next();
|
||||
rs.close(); ps.close();
|
||||
return exists;
|
||||
} catch (SQLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 增加债券 ========================
|
||||
|
||||
public int addBonds(String player, int amount) {
|
||||
return addBondsInternal(player, amount,
|
||||
TX_ADMIN_GIVE, "", "",
|
||||
"管理员发放");
|
||||
}
|
||||
|
||||
public int addBonds(String player, int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
if (isFrozen(player)) return -1;
|
||||
return addBondsInternal(player, amount,
|
||||
type, targetPlayer, operator, reason);
|
||||
}
|
||||
|
||||
private int addBondsInternal(String player,
|
||||
int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
int before = getBonds(player);
|
||||
int after = before + amount;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status)"
|
||||
+ " VALUES (?,?, 'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, after);
|
||||
ps.setInt(3, after);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
logTransaction(player, type, amount,
|
||||
targetPlayer, operator, reason,
|
||||
before, after);
|
||||
logLegacy(player, amount);
|
||||
return after;
|
||||
}
|
||||
|
||||
// ======================== 扣除债券 ========================
|
||||
|
||||
|
||||
|
||||
private boolean deductBondsInternal(String player,
|
||||
int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
int before = getBonds(player);
|
||||
if (before < amount) return false;
|
||||
int after = before - amount;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"UPDATE bonds SET amount=? "
|
||||
+ "WHERE player_name=?");
|
||||
ps.setInt(1, after);
|
||||
ps.setString(2, player);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
logTransaction(player, type, amount,
|
||||
targetPlayer, operator, reason,
|
||||
before, after);
|
||||
// 记录扣除流水,use positive amount, UI uses type to determine sign
|
||||
logLegacy(player, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 流水写入 ========================
|
||||
|
||||
private void logTransaction(String player,
|
||||
String type,
|
||||
int amount,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason,
|
||||
int balanceBefore,
|
||||
int balanceAfter) {
|
||||
try {
|
||||
// 合并模式:shop_buy 30秒内同商品合并
|
||||
if ("shop_buy".equals(type)
|
||||
&& reason != null
|
||||
&& !reason.isEmpty()) {
|
||||
long mergeWindow =
|
||||
System.currentTimeMillis() - 30000L;
|
||||
PreparedStatement chk =
|
||||
db.prepareStatement(
|
||||
"SELECT id, amount "
|
||||
+ "FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND type=? "
|
||||
+ "AND reason=? "
|
||||
+ "AND time > ? "
|
||||
+ "ORDER BY time DESC "
|
||||
+ "LIMIT 1");
|
||||
chk.setString(1, player);
|
||||
chk.setString(2, type);
|
||||
chk.setString(3, reason);
|
||||
chk.setLong(4, mergeWindow);
|
||||
ResultSet rs = chk.executeQuery();
|
||||
if (rs.next()) {
|
||||
int existId = rs.getInt("id");
|
||||
int existAmt = rs.getInt("amount");
|
||||
rs.close();
|
||||
chk.close();
|
||||
PreparedStatement upd =
|
||||
db.prepareStatement(
|
||||
"UPDATE bond_transaction "
|
||||
+ "SET amount=?, "
|
||||
+ "balance_after=?, "
|
||||
+ "time=? "
|
||||
+ "WHERE id=?");
|
||||
upd.setInt(1, existAmt + amount);
|
||||
upd.setInt(2, balanceAfter);
|
||||
upd.setLong(3,
|
||||
System.currentTimeMillis());
|
||||
upd.setInt(4, existId);
|
||||
upd.executeUpdate();
|
||||
upd.close();
|
||||
return;
|
||||
}
|
||||
rs.close();
|
||||
chk.close();
|
||||
}
|
||||
|
||||
// 原始插入
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bond_transaction "
|
||||
+ "(player_name, type, amount,"
|
||||
+ " target_player, operator,"
|
||||
+ " reason, balance_before,"
|
||||
+ " balance_after, time)"
|
||||
+ " VALUES (?,?,?,?,?,?,?,?,?)");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, type);
|
||||
ps.setInt(3, amount);
|
||||
ps.setString(4,
|
||||
targetPlayer != null ? targetPlayer : "");
|
||||
ps.setString(5,
|
||||
operator != null ? operator : "");
|
||||
ps.setString(6,
|
||||
reason != null ? reason : "");
|
||||
ps.setInt(7, balanceBefore);
|
||||
ps.setInt(8, balanceAfter);
|
||||
ps.setLong(9, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
// 触发交易监听器
|
||||
fireTransactionEvent(player, type, amount, targetPlayer);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ======================== 扣除债券 ========================
|
||||
|
||||
/** 完整版:含冻结检查 + 流水记录 */
|
||||
public boolean deductBonds(String player, int amount,
|
||||
String type,
|
||||
String targetPlayer,
|
||||
String operator,
|
||||
String reason) {
|
||||
if (isFrozen(player)) return false;
|
||||
return deductBondsInternal(player, amount,
|
||||
type, targetPlayer, operator, reason);
|
||||
}
|
||||
|
||||
public boolean checkoutCart(String player,
|
||||
String[] itemNames,
|
||||
int[] subtotals) {
|
||||
int total = 0;
|
||||
for (int s : subtotals) total += s;
|
||||
if (isFrozen(player)) return false;
|
||||
|
||||
int before = getBonds(player);
|
||||
if (before < total) return false;
|
||||
int after = before - total;
|
||||
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,?,'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, after);
|
||||
ps.setInt(3, after);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
int running = before;
|
||||
for (int i = 0; i < itemNames.length; i++) {
|
||||
int next = running - subtotals[i];
|
||||
logTransaction(player, "shop_buy", subtotals[i],
|
||||
"", "商店系统",
|
||||
"购物车购买-" + itemNames[i],
|
||||
running, next);
|
||||
running = next;
|
||||
}
|
||||
|
||||
logLegacy(player, -total);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean setBonds(String player, int amount) {
|
||||
ensureAccount(player);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bonds "
|
||||
+ "(player_name,amount,status) "
|
||||
+ "VALUES (?,?, 'normal') "
|
||||
+ "ON CONFLICT(player_name) "
|
||||
+ "DO UPDATE SET amount=?");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, amount);
|
||||
ps.setInt(3, amount);
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================== 转账 ========================
|
||||
|
||||
public TransferResult transfer(String from,
|
||||
String to,
|
||||
int amount) {
|
||||
if (from.equalsIgnoreCase(to))
|
||||
return new TransferResult(false,
|
||||
"不能给自己转账");
|
||||
if (isFrozen(from))
|
||||
return new TransferResult(false,
|
||||
"你的账户已被冻结");
|
||||
if (isFrozen(to))
|
||||
return new TransferResult(false,
|
||||
to + " 的账户已被冻结");
|
||||
if (isSameIP(from, to))
|
||||
return new TransferResult(false,
|
||||
"不能与同IP玩家转账");
|
||||
|
||||
if (!deductBondsInternal(from, amount,
|
||||
TX_TRANSFER_OUT, to, from,
|
||||
"转账给" + to))
|
||||
return new TransferResult(false, "债券不足");
|
||||
|
||||
addBondsInternal(to, amount,
|
||||
TX_TRANSFER_IN, from, from,
|
||||
"收到来自" + from + "的转账");
|
||||
|
||||
return new TransferResult(true, "转账成功");
|
||||
}
|
||||
|
||||
// ======================== 同IP检测 ========================
|
||||
|
||||
public boolean isSameIP(String p1, String p2) {
|
||||
String ip1 = getPlayerIPAny(p1);
|
||||
String ip2 = getPlayerIPAny(p2);
|
||||
if (ip1 == null || ip2 == null) return false;
|
||||
return ip1.equals(ip2);
|
||||
}
|
||||
|
||||
/** 获取同IP下的所有其他在线玩家 */
|
||||
public List<String> getSameIPPlayers(String player) {
|
||||
String ip = getPlayerIPAny(player);
|
||||
if (ip == null) return Collections.emptyList();
|
||||
List<String> list = new ArrayList<>();
|
||||
for (Player p :
|
||||
plugin.getServer().getOnlinePlayers()) {
|
||||
String pIp = plugin.getPlayerIP(p);
|
||||
if (pIp != null && pIp.equals(ip)
|
||||
&& !p.getName()
|
||||
.equalsIgnoreCase(player)) {
|
||||
list.add(p.getName());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String getPlayerIPAny(String player) {
|
||||
// 在线:直接取
|
||||
Player p = plugin.getServer()
|
||||
.getPlayerExact(player);
|
||||
if (p != null) {
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
if (ip != null) return ip;
|
||||
}
|
||||
// 离线:查主库
|
||||
try {
|
||||
Object v = plugin.getDb()
|
||||
.getField(player, "last_login_ip");
|
||||
if (v != null) return v.toString();
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Object v = plugin.getDb()
|
||||
.getField(player, "register_ip");
|
||||
if (v != null) return v.toString();
|
||||
} catch (Exception ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ======================== 流水查询 ========================
|
||||
|
||||
public List<Map<String, Object>> getTransactions(
|
||||
String player, int days) {
|
||||
days = Math.min(days, MAX_WATER_DAYS);
|
||||
long cutoff = System.currentTimeMillis()
|
||||
- (long) days * 24 * 60 * 60 * 1000;
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND time>=? "
|
||||
+ "ORDER BY time DESC");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, cutoff);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("operator",
|
||||
rs.getString("operator"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return list;
|
||||
}
|
||||
// 放在 getTransactions 方法后面:
|
||||
|
||||
/**
|
||||
* 获取该玩家全部流水(不限时间,供打印使用)
|
||||
*/
|
||||
public List<Map<String, Object>>
|
||||
getAllTransactions(String player) {
|
||||
List<Map<String, Object>> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "ORDER BY time ASC");
|
||||
ps.setString(1, player);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("player_name",
|
||||
rs.getString("player_name"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("operator",
|
||||
rs.getString("operator"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
// 放在 getAllTransactions 方法后面:
|
||||
|
||||
/**
|
||||
* 获取所有有流水记录的玩家名(去重)
|
||||
*/
|
||||
public List<String> getAllPlayerNames() {
|
||||
List<String> list = new ArrayList<>();
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
ResultSet rs = st.executeQuery(
|
||||
"SELECT DISTINCT player_name "
|
||||
+ "FROM bond_transaction "
|
||||
+ "ORDER BY player_name ASC");
|
||||
while (rs.next()) {
|
||||
list.add(rs.getString("player_name"));
|
||||
}
|
||||
rs.close(); st.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>>
|
||||
getTransferTransactions(
|
||||
String player, int days) {
|
||||
days = Math.min(days, MAX_WATER_DAYS);
|
||||
long cutoff = System.currentTimeMillis()
|
||||
- (long) days * 24 * 60 * 60 * 1000;
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT * FROM bond_transaction "
|
||||
+ "WHERE player_name=? "
|
||||
+ "AND time>=? "
|
||||
+ "AND (type=? OR type=?) "
|
||||
+ "ORDER BY time DESC");
|
||||
ps.setString(1, player);
|
||||
ps.setLong(2, cutoff);
|
||||
ps.setString(3, TX_TRANSFER_OUT);
|
||||
ps.setString(4, TX_TRANSFER_IN);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("type", rs.getString("type"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
row.put("target_player",
|
||||
rs.getString("target_player"));
|
||||
row.put("reason",
|
||||
rs.getString("reason"));
|
||||
row.put("balance_after",
|
||||
rs.getInt("balance_after"));
|
||||
row.put("balance_before", rs.getInt("balance_before"));
|
||||
row.put("time", rs.getLong("time"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close(); ps.close();
|
||||
} catch (SQLException e) { e.printStackTrace(); }
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
private void logLegacy(String player, int amount) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO bond_log "
|
||||
+ "(player_name,amount,time) "
|
||||
+ "VALUES (?,?,?)");
|
||||
ps.setString(1, player);
|
||||
ps.setInt(2, amount);
|
||||
ps.setLong(3,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate(); ps.close();
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
|
||||
public Map<String, Object> getLog(
|
||||
String player, int limit) {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("balance", getBonds(player));
|
||||
return info;
|
||||
}
|
||||
|
||||
// ======================== 转账结果 ========================
|
||||
|
||||
public static class TransferResult {
|
||||
public final boolean success;
|
||||
public final String message;
|
||||
public TransferResult(boolean success,
|
||||
String message) {
|
||||
this.success = success;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class BondPrinter {
|
||||
|
||||
private final Main plugin;
|
||||
private final File outputDir;
|
||||
|
||||
private static final SimpleDateFormat FILE_SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd_HH-mm-ss");
|
||||
private static final SimpleDateFormat CELL_SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final LinkedHashMap<String, String>
|
||||
TYPE_NAMES = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
TYPE_NAMES.put("admin_give", "管理员发放");
|
||||
TYPE_NAMES.put("admin_deduct", "管理员扣除");
|
||||
TYPE_NAMES.put("transfer_out", "转出");
|
||||
TYPE_NAMES.put("transfer_in", "转入");
|
||||
TYPE_NAMES.put("redeem", "CDK兑换");
|
||||
TYPE_NAMES.put("shop_buy", "商城购买");
|
||||
TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上
|
||||
TYPE_NAMES.put("daily_sign", "签到奖励");
|
||||
TYPE_NAMES.put("freeze", "冻结");
|
||||
TYPE_NAMES.put("unfreeze", "解冻");
|
||||
TYPE_NAMES.put("ticket_reward", "工单奖励");
|
||||
TYPE_NAMES.put("ticket_service", "工单服务奖励");
|
||||
|
||||
}
|
||||
|
||||
public BondPrinter(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.outputDir = new File(
|
||||
plugin.getDataFolder(), "printer");
|
||||
if (!this.outputDir.exists()) {
|
||||
this.outputDir.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 打印单个玩家 ============
|
||||
|
||||
public void printPlayer(String player,
|
||||
CommandSender sender) {
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(plugin,
|
||||
() -> doPrintPlayer(
|
||||
player, sender));
|
||||
}
|
||||
|
||||
private void doPrintPlayer(String player,
|
||||
CommandSender sender) {
|
||||
try {
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getAllTransactions(player);
|
||||
|
||||
String fileName = player + "_"
|
||||
+ FILE_SDF.format(new Date())
|
||||
+ ".xls";
|
||||
File file = new File(outputDir, fileName);
|
||||
|
||||
deleteOld(player);
|
||||
|
||||
PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8));
|
||||
try {
|
||||
writeXmlHeader(pw,
|
||||
escXml(player));
|
||||
writeXmlRow(pw,
|
||||
new String[]{
|
||||
"时间", "类型",
|
||||
"金额", "对象",
|
||||
"操作者", "理由",
|
||||
"操作前余额",
|
||||
"操作后余额"});
|
||||
|
||||
for (Map<String, Object> tx : txs) {
|
||||
writeXmlRow(pw, new String[]{
|
||||
txTime(tx),
|
||||
txTypeName(tx),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"amount")),
|
||||
txStr(tx,
|
||||
"target_player"),
|
||||
txStr(tx, "operator"),
|
||||
txStr(tx, "reason"),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_before")),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_after"))
|
||||
});
|
||||
}
|
||||
|
||||
writeXmlFooter(pw);
|
||||
pw.flush();
|
||||
} finally {
|
||||
pw.close();
|
||||
}
|
||||
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§a[打印] §f"
|
||||
+ player + " §a流水已导出");
|
||||
sender.sendMessage("§7文件: §f"
|
||||
+ fileName);
|
||||
sender.sendMessage("§7共 §f"
|
||||
+ txs.size()
|
||||
+ " §7条,§724小时后删除");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c导出失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
plugin.getLogger().severe(
|
||||
"[Printer] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 打印全服 ============
|
||||
|
||||
public void printAll(CommandSender sender) {
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(plugin,
|
||||
() -> doPrintAll(sender));
|
||||
}
|
||||
|
||||
private void doPrintAll(CommandSender sender) {
|
||||
try {
|
||||
List<String> players =
|
||||
plugin.getBonds()
|
||||
.getAllPlayerNames();
|
||||
if (players.isEmpty()) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c没有流水数据");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String fileName = "全服流水_"
|
||||
+ FILE_SDF.format(new Date())
|
||||
+ ".xls";
|
||||
File file = new File(outputDir, fileName);
|
||||
|
||||
// ===== 收集所有流水 =====
|
||||
// key = 时间戳+玩家+金额,用于去重
|
||||
// value = 一行数据
|
||||
List<String[]> allRows = new ArrayList<>();
|
||||
Set<String> seenKeys = new HashSet<>();
|
||||
|
||||
for (String player : players) {
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getAllTransactions(player);
|
||||
for (Map<String, Object> tx : txs) {
|
||||
int txId = ((Number) tx.get("id"))
|
||||
.intValue();
|
||||
String dedupKey =
|
||||
String.valueOf(txId);
|
||||
if (!seenKeys.add(dedupKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String timeStr = txTime(tx);
|
||||
long timeMs = 0;
|
||||
Object t = tx.get("time");
|
||||
if (t != null) {
|
||||
timeMs =
|
||||
((Number) t).longValue();
|
||||
}
|
||||
|
||||
allRows.add(new String[]{
|
||||
player,
|
||||
timeStr,
|
||||
txTypeName(tx),
|
||||
String.valueOf(
|
||||
txInt(tx, "amount")),
|
||||
txStr(tx, "target_player"),
|
||||
txStr(tx, "operator"),
|
||||
txStr(tx, "reason"),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_before")),
|
||||
String.valueOf(
|
||||
txInt(tx,
|
||||
"balance_after")),
|
||||
// 额外存时间戳用于排序
|
||||
String.valueOf(timeMs)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 按时间戳排序 =====
|
||||
allRows.sort((a, b) -> {
|
||||
long ta = Long.parseLong(
|
||||
a[a.length - 1]);
|
||||
long tb = Long.parseLong(
|
||||
b[b.length - 1]);
|
||||
return Long.compare(ta, tb);
|
||||
});
|
||||
|
||||
// ===== 写入文件 =====
|
||||
PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8));
|
||||
try {
|
||||
writeXmlHeader(pw, "全服流水");
|
||||
writeXmlRow(pw, new String[]{
|
||||
"玩家", "时间", "类型",
|
||||
"金额", "对象", "操作者",
|
||||
"理由", "操作前余额",
|
||||
"操作后余额"
|
||||
});
|
||||
|
||||
for (String[] row : allRows) {
|
||||
// 去掉最后的时间戳列
|
||||
String[] cells =
|
||||
new String[row.length - 1];
|
||||
System.arraycopy(row, 0,
|
||||
cells, 0, cells.length);
|
||||
writeXmlRow(pw, cells);
|
||||
}
|
||||
|
||||
writeXmlFooter(pw);
|
||||
pw.flush();
|
||||
|
||||
if (sender != null) {
|
||||
sender.sendMessage(
|
||||
"§a[打印] §f全服流水 §a已导出");
|
||||
sender.sendMessage("§7文件: §f"
|
||||
+ fileName);
|
||||
sender.sendMessage("§7共 §f"
|
||||
+ players.size()
|
||||
+ " 名玩家,§f"
|
||||
+ allRows.size() + " §7条");
|
||||
sender.sendMessage(
|
||||
"§7按时间从早到晚排序");
|
||||
sender.sendMessage(
|
||||
"§724小时后自动删除");
|
||||
}
|
||||
} finally {
|
||||
pw.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (sender != null) {
|
||||
sender.sendMessage("§c导出失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
plugin.getLogger().severe(
|
||||
"[Printer] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 清理 ============
|
||||
|
||||
public void cleanupOldFiles() {
|
||||
long now = System.currentTimeMillis();
|
||||
long maxAge = 24L * 60 * 60 * 1000;
|
||||
File[] files = outputDir.listFiles(
|
||||
(d, n) -> n.endsWith(".xls"));
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
for (File f : files) {
|
||||
if (now - f.lastModified() > maxAge) {
|
||||
if (f.delete()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Printer] 清理 "
|
||||
+ count + " 个过期文件");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteOld(String player) {
|
||||
File[] old = outputDir.listFiles(
|
||||
(d, n) -> n.startsWith(player + "_")
|
||||
&& n.endsWith(".xls"));
|
||||
if (old != null) {
|
||||
for (File f : old) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ XML工具 ============
|
||||
|
||||
private void writeXmlHeader(PrintWriter pw,
|
||||
String sheetName) {
|
||||
pw.println("<?xml version=\"1.0\"?>");
|
||||
pw.println("<?mso-application "
|
||||
+ "progid=\"Excel.Sheet\"?>");
|
||||
pw.println("<Workbook xmlns="
|
||||
+ "\"urn:schemas-microsoft-com"
|
||||
+ ":office:spreadsheet\"");
|
||||
pw.println(" xmlns:ss="
|
||||
+ "\"urn:schemas-microsoft-com"
|
||||
+ ":office:spreadsheet\">");
|
||||
pw.println(" <Worksheet ss:Name=\""
|
||||
+ sheetName + "\">");
|
||||
pw.println(" <Table>");
|
||||
}
|
||||
|
||||
private void writeXmlRow(PrintWriter pw,
|
||||
String[] cells) {
|
||||
pw.println(" <Row>");
|
||||
for (String cell : cells) {
|
||||
pw.println(" <Cell>"
|
||||
+ "<Data ss:Type=\"String\">"
|
||||
+ escXml(cell != null ? cell : "")
|
||||
+ "</Data></Cell>");
|
||||
}
|
||||
pw.println(" </Row>");
|
||||
}
|
||||
|
||||
private void writeXmlFooter(PrintWriter pw) {
|
||||
pw.println(" </Table>");
|
||||
pw.println(" </Worksheet>");
|
||||
pw.println("</Workbook>");
|
||||
}
|
||||
|
||||
private String escXml(String val) {
|
||||
if (val == null) {
|
||||
return "";
|
||||
}
|
||||
return val.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """);
|
||||
}
|
||||
|
||||
private String txTime(Map<String, Object> tx) {
|
||||
Object t = tx.get("time");
|
||||
if (t == null) return "";
|
||||
long ms = ((Number) t).longValue();
|
||||
return CELL_SDF.format(new Date(ms));
|
||||
}
|
||||
|
||||
private String txTypeName(
|
||||
Map<String, Object> tx) {
|
||||
String type = (String) tx.get("type");
|
||||
return TYPE_NAMES.getOrDefault(type, type);
|
||||
}
|
||||
|
||||
private int txInt(Map<String, Object> tx,
|
||||
String key) {
|
||||
Object v = tx.get(key);
|
||||
if (v == null) return 0;
|
||||
return ((Number) v).intValue();
|
||||
}
|
||||
|
||||
private String txStr(Map<String, Object> tx,
|
||||
String key) {
|
||||
Object v = tx.get(key);
|
||||
return v != null ? v.toString() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.*;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CDKManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
private final Map<String, CDKInput> pendingInputs
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
public CDKManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initDB();
|
||||
}
|
||||
|
||||
private void initDB() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
plugin.getDataFolder(), "bond.db");
|
||||
db = DriverManager.getConnection(
|
||||
"jdbc:sqlite:" + dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS cdk ("
|
||||
+ "code TEXT PRIMARY KEY,"
|
||||
+ "amount INTEGER DEFAULT 0,"
|
||||
+ "type TEXT DEFAULT 'bond',"
|
||||
+ "used INTEGER DEFAULT 0,"
|
||||
+ "used_by TEXT DEFAULT '',"
|
||||
+ "created_time INTEGER DEFAULT 0,"
|
||||
+ "used_time INTEGER DEFAULT 0)");
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"[CDK] DB init failed: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CDK兑换(供SDF1调用) =====
|
||||
|
||||
/**
|
||||
* 兑换CDK,返回结果字符串
|
||||
* @return "success:金额:余额前:余额后"
|
||||
* 或 "fail:原因"
|
||||
*/
|
||||
public String redeem(String code, String player) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT amount,type,used FROM cdk "
|
||||
+ "WHERE code=?");
|
||||
ps.setString(1, code);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (!rs.next()) {
|
||||
rs.close(); ps.close();
|
||||
return "fail:not_found";
|
||||
}
|
||||
int amount = rs.getInt("amount");
|
||||
String type = rs.getString("type");
|
||||
int used = rs.getInt("used");
|
||||
rs.close(); ps.close();
|
||||
|
||||
if (used == 1)
|
||||
return "fail:already_used";
|
||||
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(player))
|
||||
return "fail:frozen";
|
||||
|
||||
PreparedStatement up = db.prepareStatement(
|
||||
"UPDATE cdk SET used=1,"
|
||||
+ "used_by=?,used_time=? "
|
||||
+ "WHERE code=?");
|
||||
up.setString(1, player);
|
||||
up.setLong(2,
|
||||
System.currentTimeMillis());
|
||||
up.setString(3, code);
|
||||
up.executeUpdate(); up.close();
|
||||
|
||||
if ("bond".equals(type)) {
|
||||
int bef = plugin.getBonds()
|
||||
.getBonds(player);
|
||||
plugin.getBonds().addBonds(player,
|
||||
amount,
|
||||
BondManager.TX_REDEEM,
|
||||
"", "CDK系统",
|
||||
"CDK兑换: " + code);
|
||||
int aft = plugin.getBonds()
|
||||
.getBonds(player);
|
||||
return "success:" + amount
|
||||
+ ":" + bef + ":" + aft;
|
||||
}
|
||||
return "fail:unknown_type";
|
||||
} catch (SQLException e) {
|
||||
return "fail:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 聊天输入系统 =====
|
||||
|
||||
public void requestInput(Player p,
|
||||
String type,
|
||||
String context) {
|
||||
CDKInput input = new CDKInput();
|
||||
input.type = type;
|
||||
input.context = context;
|
||||
input.time = System.currentTimeMillis();
|
||||
pendingInputs.put(p.getName(), input);
|
||||
}
|
||||
|
||||
public boolean isListening(String playerName) {
|
||||
return pendingInputs
|
||||
.containsKey(playerName);
|
||||
}
|
||||
|
||||
public void onChat(Player p, String msg) {
|
||||
CDKInput input = pendingInputs
|
||||
.remove(p.getName());
|
||||
if (input == null) return;
|
||||
|
||||
if (System.currentTimeMillis()
|
||||
- input.time > 60000) {
|
||||
p.sendMessage("§c输入超时");
|
||||
return;
|
||||
}
|
||||
|
||||
if ("cdk".equals(input.type)) {
|
||||
String result = redeem(
|
||||
msg.trim(), p.getName());
|
||||
if (result != null
|
||||
&& result.startsWith("success:")) {
|
||||
String[] parts = result.split(":");
|
||||
int amt = Integer.parseInt(parts[1]);
|
||||
int bef = Integer.parseInt(parts[2]);
|
||||
int aft = Integer.parseInt(parts[3]);
|
||||
p.sendMessage("§e§l[CDK] §f兑换成功!"
|
||||
+ "获得 §c" + amt + "§f债券");
|
||||
p.sendMessage("§7余额: §e" + bef
|
||||
+ " §7-> §a" + aft);
|
||||
} else {
|
||||
p.sendMessage("§c§l[CDK] §f"
|
||||
+ (result != null
|
||||
? result.replace("fail:", "")
|
||||
: "无效的兑换码"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ("econ".equals(input.type)) {
|
||||
handleEconInput(p, msg.trim(),
|
||||
input.context);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("bond".equals(input.type)) {
|
||||
handleBondInput(p, msg.trim(),
|
||||
input.context);
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Economy已移除,所有经济操作改用债券系统
|
||||
admin.sendMessage("§c[经济操作] 功能已移除,请使用债券系统");
|
||||
}
|
||||
|
||||
private void handleBondInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String sign = msg.substring(0, 1);
|
||||
String rest =
|
||||
msg.substring(1).trim();
|
||||
String[] parts =
|
||||
rest.split("\\s+", 2);
|
||||
int amount = Integer.parseInt(parts[0]);
|
||||
String reason = parts.length >= 2
|
||||
? parts[1].trim()
|
||||
: "管理员手动调整";
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
String opName = admin.getName();
|
||||
String txType = "-".equals(sign)
|
||||
? BondManager.TX_ADMIN_DEDUCT
|
||||
: BondManager.TX_ADMIN_GIVE;
|
||||
|
||||
if ("-".equals(sign)) {
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
admin.sendMessage("§c账户冻结");
|
||||
return;
|
||||
}
|
||||
if (plugin.getBonds().deductBonds(
|
||||
target, amount, txType,
|
||||
"", opName, reason)) {
|
||||
admin.sendMessage("§a扣除成功");
|
||||
} else {
|
||||
admin.sendMessage("§c债券不足");
|
||||
}
|
||||
} else {
|
||||
if (plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
admin.sendMessage("§c账户冻结");
|
||||
return;
|
||||
}
|
||||
plugin.getBonds().addBonds(target,
|
||||
amount, txType,
|
||||
"", opName, reason);
|
||||
admin.sendMessage("§a发放成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePointsInput(Player admin,
|
||||
String msg,
|
||||
String target) {
|
||||
if (!msg.startsWith("+")
|
||||
&& !msg.startsWith("-")) {
|
||||
admin.sendMessage("§c格式: §a+100"
|
||||
+ " §7或 §c-50");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String sign = msg.substring(0, 1);
|
||||
String rest =
|
||||
msg.substring(1).trim();
|
||||
int amount = Integer.parseInt(rest);
|
||||
if (amount <= 0) {
|
||||
admin.sendMessage("§c金额必须>0");
|
||||
return;
|
||||
}
|
||||
if ("-".equals(sign)) {
|
||||
if (plugin.getDb().deductPoints(
|
||||
target, amount)) {
|
||||
admin.sendMessage("§a扣除成功");
|
||||
} else {
|
||||
admin.sendMessage("§c积分不足");
|
||||
}
|
||||
} else {
|
||||
plugin.getDb().addPoints(
|
||||
target, amount);
|
||||
admin.sendMessage("§a发放成功");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
admin.sendMessage("§c格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelInput(String player) {
|
||||
pendingInputs.remove(player);
|
||||
}
|
||||
|
||||
static class CDKInput {
|
||||
String type;
|
||||
String context;
|
||||
long time;
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,11 @@ public class ChatInputManager {
|
||||
TICKET_ADMIN_TYPE,
|
||||
TICKET_ADMIN_TITLE,
|
||||
TICKET_ADMIN_DESC,
|
||||
TICKET_ADMIN_PROVIDER
|
||||
}
|
||||
TICKET_ADMIN_PROVIDER,
|
||||
BACK_CHECK_ECONOMY_CONFIRM,
|
||||
USER_MGMT_SEARCH,
|
||||
|
||||
}
|
||||
|
||||
public static class PlayerState {
|
||||
public InputType type = InputType.NONE;
|
||||
@@ -48,6 +51,7 @@ public class ChatInputManager {
|
||||
public int ticketId = 0;
|
||||
public int smtpStep = 0;
|
||||
public int scoreValue = 0;
|
||||
|
||||
}
|
||||
|
||||
private final Map<String, PlayerState> states =
|
||||
@@ -63,6 +67,11 @@ public class ChatInputManager {
|
||||
public void reset(Player p) {
|
||||
states.remove(p.getName());
|
||||
}
|
||||
public boolean isInFlow(Player p) {
|
||||
PlayerState state = states.get(p.getName());
|
||||
return state != null
|
||||
&& state.type != InputType.NONE;
|
||||
}
|
||||
|
||||
public void handleInput(Player p, String msg,
|
||||
Main mainPlugin) {
|
||||
@@ -81,9 +90,9 @@ public class ChatInputManager {
|
||||
name, "password_salt");
|
||||
String hash =
|
||||
PasswordUtils.hash(msg, salt);
|
||||
boolean mainOk =
|
||||
db.checkPassword(name, hash);
|
||||
if (mainOk) {
|
||||
// ★ 同时校验真实密码 + 临时密码(改密不受临时密码5分钟有效期限制)
|
||||
String result = db.checkPasswordForChange(name, hash);
|
||||
if ("main".equals(result)) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "main";
|
||||
@@ -91,10 +100,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";
|
||||
@@ -138,11 +144,26 @@ public class ChatInputManager {
|
||||
"password_hash", newHash);
|
||||
db.setField(name,
|
||||
"password_salt", newSalt);
|
||||
// ★ 改密成功:作废旧的临时密码
|
||||
db.clearTempPassword(name);
|
||||
// ★ 同步新密码到Web端(异步,不阻塞主线程)
|
||||
final Main self = mainPlugin;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(self, () -> {
|
||||
if (self.webManager != null) {
|
||||
self.webManager.pushWebLoginCredentials();
|
||||
}
|
||||
});
|
||||
mainPlugin
|
||||
.getNeedsPasswordChange()
|
||||
.remove(name);
|
||||
p.sendMessage(config.msg(
|
||||
"password_changed"));
|
||||
// ★ 改密成功:actionbar 提示1分钟保存账号密码
|
||||
mainPlugin.showCredentialSaveReminder(
|
||||
p, name, msg);
|
||||
if ("temp".equals(state.ticketTitle)) {
|
||||
p.sendMessage("§7(本次是用临时密码改的,临时密码已作废)");
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
@@ -155,10 +176,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()) {
|
||||
@@ -345,14 +377,33 @@ public class ChatInputManager {
|
||||
case ADMIN_DELETE_CONFIRM: {
|
||||
if (msg.equals(
|
||||
state.targetPlayer)) {
|
||||
db.deleteUser(
|
||||
// ★ 精准匹配:TEST 与 test 是两个人,只有名字完全相等才删
|
||||
if (db.findExactUserName(state.targetPlayer) == null) {
|
||||
String twin = db.findSimilarUserNameIgnoreCase(
|
||||
state.targetPlayer);
|
||||
p.sendMessage("§c账号 " + state.targetPlayer
|
||||
+ " 不存在"
|
||||
+ (twin != null
|
||||
? "(仅存在大小写不同的账号 §e" + twin
|
||||
+ "§c,已按精准匹配跳过)" : "")
|
||||
+ ",未执行删除");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
int deleted = db.deleteUser(
|
||||
state.targetPlayer);
|
||||
p.sendMessage("§a已删除 "
|
||||
+ state.targetPlayer);
|
||||
Player tgt =
|
||||
Bukkit.getPlayer(
|
||||
state.targetPlayer);
|
||||
if (tgt != null
|
||||
if (plugin.webManager != null && deleted > 0) {
|
||||
plugin.webManager.deleteWebUser(state.targetPlayer);
|
||||
}
|
||||
p.sendMessage(deleted > 0
|
||||
? "§a已删除 " + state.targetPlayer
|
||||
: "§c删除失败:账号 "
|
||||
+ state.targetPlayer + " 不存在");
|
||||
// ★ getPlayerExact:只踢名字完全相同的在线玩家,
|
||||
// 避免把 TEST 误当成 test 踢下线
|
||||
Player tgt = Bukkit.getPlayerExact(
|
||||
state.targetPlayer);
|
||||
if (deleted > 0 && tgt != null
|
||||
&& tgt.isOnline())
|
||||
tgt.kickPlayer(
|
||||
"§c账号已被删除");
|
||||
@@ -542,24 +593,33 @@ public class ChatInputManager {
|
||||
}
|
||||
|
||||
case BACK_CHECKIN: {
|
||||
int cost = 20;
|
||||
if (!mainPlugin.getDb()
|
||||
.deductPoints(
|
||||
p.getName(), cost)) {
|
||||
p.sendMessage(
|
||||
"§c积分不足,补签需 "
|
||||
+ cost + " 积分");
|
||||
reset(p);
|
||||
String result =
|
||||
mainPlugin.getCheckIn()
|
||||
.backCheckIn(p, msg);
|
||||
if (result == null) {
|
||||
// 等待选择(NEED_CONFIRM)
|
||||
return;
|
||||
}
|
||||
p.sendMessage(
|
||||
"§a补签成功!扣除 "
|
||||
+ cost + " 积分。"
|
||||
+ "日期: " + msg);
|
||||
if (result.startsWith(
|
||||
"NEED_CONFIRM:")) {
|
||||
String[] parts =
|
||||
result.split(":");
|
||||
// 存储到Main的Map
|
||||
mainPlugin.pendingBackCheck.put(
|
||||
p.getName(),
|
||||
parts[1] + ":" + parts[2]);
|
||||
return;
|
||||
}
|
||||
if (result.startsWith("§c")) {
|
||||
p.sendMessage(result);
|
||||
} else {
|
||||
p.sendMessage(result);
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
case INVITE_INPUT_CODE: {
|
||||
String code = msg.trim();
|
||||
String myName = p.getName();
|
||||
|
||||
@@ -1,114 +1,305 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CheckInManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public CheckInManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isCheckedInToday(String name) {
|
||||
String last = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
return today.equals(last);
|
||||
}
|
||||
|
||||
public String checkIn(Player p) {
|
||||
String name = p.getName();
|
||||
if (isCheckedInToday(name)) {
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
|
||||
Object streakObj = plugin.getDb()
|
||||
.getField(name, "checkin_streak");
|
||||
int streak = streakObj != null
|
||||
? ((Number) streakObj).intValue() : 0;
|
||||
|
||||
String lastDate = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
|
||||
if (lastDate != null && !lastDate.isEmpty()) {
|
||||
try {
|
||||
java.util.Date lastD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(lastDate);
|
||||
java.util.Date todayD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(today);
|
||||
long diff = todayD.getTime() - lastD.getTime();
|
||||
if (diff > 86400000L * 2) {
|
||||
streak = 0;
|
||||
} else if (diff > 86400000L) {
|
||||
streak++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (streak == 0) streak = 1;
|
||||
else streak++;
|
||||
|
||||
double multi = 1.0;
|
||||
if (streak >= 21) multi = 1.5;
|
||||
else if (streak >= 14) multi = 1.3;
|
||||
else if (streak >= 7) multi = 1.1;
|
||||
|
||||
int points = (int) (1 * multi);
|
||||
plugin.getDb().addPoints(name, points);
|
||||
plugin.getDb().setField(
|
||||
name, "checkin_streak", streak);
|
||||
plugin.getDb().setField(
|
||||
name, "last_checkin_date", today);
|
||||
Object totalObj = plugin.getDb()
|
||||
.getField(name, "total_checkin_days");
|
||||
int total = totalObj != null
|
||||
? ((Number) totalObj).intValue() : 0;
|
||||
plugin.getDb().setField(
|
||||
name, "total_checkin_days", total + 1);
|
||||
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_success")
|
||||
.replace("{points}",
|
||||
String.valueOf(points))
|
||||
.replace("{streak}",
|
||||
String.valueOf(streak))
|
||||
.replace("{multi}",
|
||||
String.valueOf(multi));
|
||||
}
|
||||
|
||||
public String backCheckIn(Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
if (!plugin.getDb().deductPoints(name, 10)) {
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
try {
|
||||
java.util.Date target = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(dateStr);
|
||||
java.util.Date now = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date()));
|
||||
long diff = now.getTime() - target.getTime();
|
||||
if (diff < 0 || diff > 3 * 86400000L) {
|
||||
plugin.getDb().addPoints(name, 10);
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
plugin.getDb().addPoints(name, 1);
|
||||
return "§a补签成功!日期: " + dateStr;
|
||||
} catch (Exception e) {
|
||||
plugin.getDb().addPoints(name, 10);
|
||||
return "§c日期格式错误,请用 yyyy-MM-dd";
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
public class CheckInManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public CheckInManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ===== 今日是否已签到 =====
|
||||
public boolean isCheckedInToday(
|
||||
String name) {
|
||||
String last = (String) plugin.getDb()
|
||||
.getField(name,
|
||||
"last_checkin_date");
|
||||
String today = new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
return today.equals(last);
|
||||
}
|
||||
|
||||
// ===== 签到 =====
|
||||
// ===== 签到 =====
|
||||
public String checkIn(Player p) {
|
||||
String name = p.getName();
|
||||
if (isCheckedInToday(name)) {
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
|
||||
Object streakObj = plugin.getDb()
|
||||
.getField(name, "checkin_streak");
|
||||
int streak = streakObj != null
|
||||
? ((Number) streakObj).intValue()
|
||||
: 0;
|
||||
|
||||
String lastDate = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
|
||||
if (lastDate != null && !lastDate.isEmpty()) {
|
||||
try {
|
||||
java.util.Date lastD =
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.parse(lastDate);
|
||||
java.util.Date todayD =
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.parse(today);
|
||||
long diff = todayD.getTime()
|
||||
- lastD.getTime();
|
||||
if (diff > 86400000L * 2) {
|
||||
// 间隔超过2天,重置连续天数
|
||||
streak = 1;
|
||||
} else if (diff > 86400000L) {
|
||||
// 间隔1-2天(连续签到),天数+1
|
||||
streak++;
|
||||
}
|
||||
// 间隔<=1天(同一天),不改变连续天数
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} else {
|
||||
// 首次签到
|
||||
streak = 1;
|
||||
}
|
||||
|
||||
double multi = 1.0;
|
||||
if (streak >= 21) multi = 1.5;
|
||||
else if (streak >= 14) multi = 1.3;
|
||||
else if (streak >= 7) multi = 1.1;
|
||||
|
||||
int basePoints =
|
||||
plugin.getConfig2().checkinPoints;
|
||||
int points = (int) (basePoints * multi);
|
||||
|
||||
plugin.getDb().addPoints(name, points);
|
||||
plugin.getDb().setField(name,
|
||||
"checkin_streak", streak);
|
||||
plugin.getDb().setField(name,
|
||||
"last_checkin_date", today);
|
||||
|
||||
Object totalObj = plugin.getDb()
|
||||
.getField(name, "total_checkin_days");
|
||||
int total = totalObj != null
|
||||
? ((Number) totalObj).intValue()
|
||||
: 0;
|
||||
plugin.getDb().setField(name,
|
||||
"total_checkin_days", total + 1);
|
||||
|
||||
// ★ 债券盲盒奖励(6参数版,写入流水)★
|
||||
int bondMin =
|
||||
plugin.getConfig2().checkinBondMin;
|
||||
int bondMax =
|
||||
plugin.getConfig2().checkinBondMax;
|
||||
if (bondMin <= 0) bondMin = 1;
|
||||
if (bondMax < bondMin) bondMax = bondMin;
|
||||
int bondReward = bondMin
|
||||
+ new java.util.Random()
|
||||
.nextInt(bondMax - bondMin + 1);
|
||||
plugin.getBonds().addBonds(name, bondReward,
|
||||
"daily_sign", "", "签到系统",
|
||||
"每日签到连续" + streak + "天");
|
||||
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_success")
|
||||
.replace("{points}",
|
||||
String.valueOf(points))
|
||||
.replace("{streak}",
|
||||
String.valueOf(streak))
|
||||
.replace("{multi}",
|
||||
String.valueOf(multi))
|
||||
+ " §e+" + bondReward + "债券";
|
||||
}
|
||||
|
||||
// ===== 获取连续天数 =====
|
||||
public int getStreak(String name) {
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"checkin_streak");
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val)
|
||||
.intValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// 标准化日期
|
||||
private String normalizeDate(String input) {
|
||||
String s = input.trim().replace("/", "-");
|
||||
if (s.matches("\\d{8}")) {
|
||||
return s.substring(0, 4) + "-"
|
||||
+ s.substring(4, 6) + "-"
|
||||
+ s.substring(6, 8);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// ===== 补签入口 =====
|
||||
public String backCheckIn(
|
||||
Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
String normalized =
|
||||
normalizeDate(dateStr);
|
||||
|
||||
// 严格日期验证
|
||||
java.text.SimpleDateFormat sdf =
|
||||
new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd");
|
||||
sdf.setLenient(false);
|
||||
java.util.Date target;
|
||||
try {
|
||||
target = sdf.parse(normalized);
|
||||
} catch (Exception ex) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
String reformat = sdf.format(target);
|
||||
if (!reformat.equals(normalized)) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
|
||||
String today = sdf.format(new Date());
|
||||
if (normalized.compareTo(today) >= 0) {
|
||||
return "§c今天不需要补签";
|
||||
}
|
||||
|
||||
String threeDaysAgo = getPastDate(3);
|
||||
if (normalized.compareTo(
|
||||
threeDaysAgo) < 0) {
|
||||
return "§c只能补签3天内"
|
||||
+ "(最多到 " + threeDaysAgo + ")";
|
||||
}
|
||||
|
||||
Object val = plugin.getDb()
|
||||
.getField(name, "backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
boolean alreadyBacked = false;
|
||||
if (!existing.isEmpty()) {
|
||||
for (String d : existing.split(",")) {
|
||||
if (normalizeDate(d.trim())
|
||||
.equals(normalized)) {
|
||||
alreadyBacked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (alreadyBacked) {
|
||||
return "§c该日期已补签: " + normalized;
|
||||
}
|
||||
|
||||
Object normalCheck =
|
||||
plugin.getDb().getField(
|
||||
name, "last_checkin_date");
|
||||
String lastCheckin = normalCheck != null
|
||||
? normalCheck.toString() : "";
|
||||
if (lastCheckin.equals(normalized)) {
|
||||
return "§c该日期已签到: " + normalized;
|
||||
}
|
||||
|
||||
// ★ 补签消耗:3债券 + 10积分
|
||||
// 积分不够时:用 9债券(3倍)代替 ★
|
||||
int bondCost = 3;
|
||||
int pointCost = 10;
|
||||
int bondAltCost = 9; // 3倍债券替代
|
||||
|
||||
int myBonds = plugin.getBonds()
|
||||
.getBonds(name);
|
||||
Object ptsObj = plugin.getDb()
|
||||
.getField(name, "points");
|
||||
int myPoints = ptsObj != null
|
||||
? ((Number) ptsObj).intValue()
|
||||
: 0;
|
||||
|
||||
|
||||
// 方案A:3债券 + 10积分
|
||||
boolean canA = myBonds >= bondCost
|
||||
&& myPoints >= pointCost;
|
||||
// 方案B:9债券(积分不够时)
|
||||
boolean canB = myBonds >= bondAltCost
|
||||
&& myPoints < pointCost;
|
||||
|
||||
if (!canA && !canB) {
|
||||
if (myPoints < pointCost
|
||||
&& myBonds < bondAltCost) {
|
||||
return "§c补签费用不足"
|
||||
+ "(需3债券+10积分,"
|
||||
+ "或9债券代替)"
|
||||
+ " 当前: "
|
||||
+ myBonds + "债券 "
|
||||
+ myPoints + "积分";
|
||||
}
|
||||
if (myBonds < bondCost) {
|
||||
return "§c债券不足(需"
|
||||
+ bondCost + "枚)";
|
||||
}
|
||||
return "§c积分不足(需"
|
||||
+ pointCost + "分)";
|
||||
}
|
||||
|
||||
// 执行扣款
|
||||
if (canA) {
|
||||
// 方案A:3债券 + 10积分
|
||||
plugin.getBonds().deductBonds(
|
||||
name, bondCost,
|
||||
"admin_deduct", "",
|
||||
"补签系统", "补签扣款");
|
||||
plugin.getDb().deductPoints(
|
||||
name, pointCost);
|
||||
} else {
|
||||
// 方案B:9债券代替
|
||||
plugin.getBonds().deductBonds(
|
||||
name, bondAltCost,
|
||||
"admin_deduct", "",
|
||||
"补签系统", "补签扣款(债券替代)");
|
||||
}
|
||||
|
||||
doBackCheckIn(p, normalized);
|
||||
|
||||
String costMsg = canA
|
||||
? bondCost + "债券+" + pointCost + "积分"
|
||||
: bondAltCost + "债券(替代)";
|
||||
return "§a补签成功!日期: "
|
||||
+ normalized
|
||||
+ " §7消耗: " + costMsg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===== 执行补签 =====
|
||||
// ===== 执行补签 =====
|
||||
public void doBackCheckIn(
|
||||
Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
Object val = plugin.getDb()
|
||||
.getField(name, "backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
String updated = existing.isEmpty()
|
||||
? dateStr
|
||||
: existing + "," + dateStr;
|
||||
plugin.getDb().setField(name,
|
||||
"backcheck_dates", updated);
|
||||
int streak = getStreak(name);
|
||||
plugin.getDb().setField(name,
|
||||
"checkin_streak", streak + 1);
|
||||
// ★ 补签不发放奖励,只记录
|
||||
}
|
||||
|
||||
// ===== 获取N天前日期 =====
|
||||
private String getPastDate(
|
||||
int daysAgo) {
|
||||
Calendar cal =
|
||||
Calendar.getInstance();
|
||||
cal.add(Calendar.DAY_OF_MONTH,
|
||||
-daysAgo);
|
||||
return new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.format(cal.getTime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class CommissionManager {
|
||||
|
||||
private final Main main;
|
||||
private final Logger logger;
|
||||
private final List<double[]> rules =
|
||||
new ArrayList<>();
|
||||
|
||||
public CommissionManager(Main main) {
|
||||
this.main = main;
|
||||
this.logger = main.getLogger();
|
||||
loadRules();
|
||||
}
|
||||
|
||||
public void loadRules() {
|
||||
rules.clear();
|
||||
File f = new File(
|
||||
main.getDataFolder(),
|
||||
"提成规则.txt");
|
||||
if (!f.exists()) {
|
||||
writeDefault(f);
|
||||
}
|
||||
try (BufferedReader br =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(f),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()
|
||||
|| line.startsWith("#"))
|
||||
continue;
|
||||
// 格式: 邀请人数:提成百分比
|
||||
// 如 "5:15" = 邀请5人提成15%
|
||||
String[] parts =
|
||||
line.split(":");
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
int count = Integer.parseInt(
|
||||
parts[0].trim());
|
||||
double rate = Double
|
||||
.parseDouble(
|
||||
parts[1]
|
||||
.trim()) / 100.0;
|
||||
rules.add(new double[]{
|
||||
count, rate});
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 按人数排序(从大到小)
|
||||
rules.sort((a, b) ->
|
||||
Double.compare(b[0], a[0]));
|
||||
logger.info(
|
||||
"[Commission] 加载 "
|
||||
+ rules.size() + " 条提成规则");
|
||||
} catch (Exception e) {
|
||||
logger.warning(
|
||||
"[Commission] 加载失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDefault(File f) {
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.println("# 邀请提成规则");
|
||||
pw.println("# 格式: 邀请人数:提成百分比");
|
||||
pw.println("# 邀请10人提成20%");
|
||||
pw.println("10:20");
|
||||
pw.println("# 邀请5人提成15%");
|
||||
pw.println("5:15");
|
||||
pw.println("# 邀请1人提成10%");
|
||||
pw.println("1:10");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请人的提成比例
|
||||
* 根据已邀请人数匹配最高规则
|
||||
*/
|
||||
public double getCommissionRate(
|
||||
String inviter) {
|
||||
int inviteCount = getInviteCount(
|
||||
inviter);
|
||||
for (double[] rule : rules) {
|
||||
if (inviteCount >= (int) rule[0]) {
|
||||
return rule[1];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请人数
|
||||
*/
|
||||
public int getInviteCount(
|
||||
String inviter) {
|
||||
try {
|
||||
Object val =
|
||||
main.getDb().getField(
|
||||
inviter,
|
||||
"monthly_invite_count");
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).intValue();
|
||||
}
|
||||
Object total =
|
||||
main.getDb().getField(
|
||||
inviter,
|
||||
"total_invite_count");
|
||||
if (total instanceof Number) {
|
||||
return ((Number) total).intValue();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发放提成
|
||||
* 被邀请人获得奖励时,邀请人获得提成
|
||||
*/
|
||||
public void payCommission(
|
||||
String invitee,
|
||||
double originalAmount) {
|
||||
// 查找邀请人
|
||||
String inviter = getInviter(invitee);
|
||||
if (inviter == null
|
||||
|| inviter.isEmpty()) return;
|
||||
|
||||
double rate = getCommissionRate(
|
||||
inviter);
|
||||
if (rate <= 0) return;
|
||||
|
||||
double commission =
|
||||
originalAmount * rate;
|
||||
if (commission <= 0) return;
|
||||
|
||||
// Economy已移除,佣金提成功能不可用
|
||||
// 邀请人可通过债券系统获取奖励
|
||||
|
||||
// 通知邀请人
|
||||
Player inviterP =
|
||||
main.getServer()
|
||||
.getPlayer(inviter);
|
||||
if (inviterP != null
|
||||
&& inviterP.isOnline()) {
|
||||
inviterP.sendMessage(
|
||||
"§a§l[邀请提成] §f"
|
||||
+ invitee + " 完成任务,"
|
||||
+ "你获得 §e"
|
||||
+ String.format("%.2f",
|
||||
commission)
|
||||
+ " §f游戏币提成 ("
|
||||
+ (int)(rate * 100) + "%)");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[Commission] 提成: "
|
||||
+ inviter + " ← "
|
||||
+ invitee + " | "
|
||||
+ String.format("%.2f",
|
||||
originalAmount)
|
||||
+ " × "
|
||||
+ (int)(rate * 100) + "% = "
|
||||
+ String.format("%.2f",
|
||||
commission));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请人
|
||||
*/
|
||||
private String getInviter(String player) {
|
||||
try {
|
||||
Object val =
|
||||
main.getDb().getField(
|
||||
player,
|
||||
"invited_by");
|
||||
if (val != null
|
||||
&& !val.toString()
|
||||
.isEmpty()) {
|
||||
return val.toString();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断金额是否包含经济内容
|
||||
*/
|
||||
public boolean hasEconomyContent(
|
||||
String text) {
|
||||
String t = text.toLowerCase();
|
||||
return t.contains("游戏币")
|
||||
|| t.contains("金币")
|
||||
|| t.contains("积分")
|
||||
|| t.contains("块钱")
|
||||
|| t.contains("元")
|
||||
|| t.contains("$")
|
||||
|| t.contains("经济")
|
||||
|| t.contains("余额");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.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 java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CypayCommand
|
||||
implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final Main plugin;
|
||||
private static final SimpleDateFormat SDF =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public CypayCommand(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private boolean hasAdminPerm(CommandSender s) {
|
||||
if (s.isOp()) return true;
|
||||
if (!(s instanceof Player)) return true;
|
||||
Player p = (Player) s;
|
||||
String tag =
|
||||
plugin.getConfig2().adminTag;
|
||||
return !tag.isEmpty()
|
||||
&& p.getScoreboardTags().contains(tag);
|
||||
}
|
||||
|
||||
private int parseAmount(String s) {
|
||||
return Integer.parseInt(
|
||||
s.trim().replaceAll("[+]", ""));
|
||||
}
|
||||
|
||||
private List<String> filter(
|
||||
List<String> list, String prefix) {
|
||||
String low = prefix.toLowerCase();
|
||||
return list.stream()
|
||||
.filter(x -> x.toLowerCase()
|
||||
.startsWith(low))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender,
|
||||
Command cmd,
|
||||
String label,
|
||||
String[] args) {
|
||||
|
||||
// ===== /cypay — 查自己 =====
|
||||
if (args.length == 0) {
|
||||
String name = sender instanceof Player
|
||||
? sender.getName() : "Console";
|
||||
int bonds =
|
||||
plugin.getBonds().getBonds(name);
|
||||
String status = plugin.getBonds()
|
||||
.getAccountStatus(name);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f" + name
|
||||
+ " 余额: §a" + bonds);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f状态: "
|
||||
+ ("frozen".equals(status)
|
||||
? "§c冻结中"
|
||||
: "§a正常"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay import =====
|
||||
/* if (args.length == 2
|
||||
&& "import".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String file = args[1];
|
||||
if (!file.endsWith(".txt"))
|
||||
file += ".txt";
|
||||
int result = plugin.getCDK()
|
||||
.importFromFile(file);
|
||||
if (result == -1)
|
||||
sender.sendMessage("§c文件不存在: "
|
||||
+ file);
|
||||
else
|
||||
sender.sendMessage("§a导入完成: "
|
||||
+ result + " 条");
|
||||
return true;
|
||||
}*/
|
||||
|
||||
// ===== /cypay freeze =====
|
||||
if (args.length == 2
|
||||
&& "freeze".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
if (plugin.getBonds().isFrozen(target)) {
|
||||
sender.sendMessage("§c" + target
|
||||
+ " 已处于冻结状态");
|
||||
return true;
|
||||
}
|
||||
plugin.getBonds().freezeAccount(target);
|
||||
sender.sendMessage("§a已冻结 " + target
|
||||
+ " 的债券账户");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§c§l[系统] 你的债券账户"
|
||||
+ "已被管理员冻结");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay unfreeze =====
|
||||
if (args.length == 2
|
||||
&& "unfreeze".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
if (!plugin.getBonds()
|
||||
.isFrozen(target)) {
|
||||
sender.sendMessage("§c" + target
|
||||
+ " 未被冻结");
|
||||
return true;
|
||||
}
|
||||
plugin.getBonds().unfreezeAccount(target);
|
||||
sender.sendMessage("§a已解冻 " + target
|
||||
+ " 的债券账户");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§a§l[系统] 你的债券账户"
|
||||
+ "已被解冻");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /cypay water [days] =====
|
||||
if (args.length >= 2
|
||||
&& "water".equalsIgnoreCase(
|
||||
args[0])) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
int days = 7;
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
days = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
days = Math.min(days,
|
||||
BondManager.ADMIN_MAX_WATER_DAYS);
|
||||
|
||||
if (days <= 0) days = 1;
|
||||
|
||||
List<Map<String, Object>> txs =
|
||||
plugin.getBonds()
|
||||
.getTransactions(
|
||||
target, days);
|
||||
|
||||
sender.sendMessage(
|
||||
"§e§l===== " + target
|
||||
+ " 流水记录 (近"
|
||||
+ days + "天) =====");
|
||||
sender.sendMessage("§f当前余额: §a"
|
||||
+ plugin.getBonds()
|
||||
.getBonds(target)
|
||||
+ " §f状态: "
|
||||
+ (plugin.getBonds()
|
||||
.isFrozen(target)
|
||||
? "§c冻结" : "§a正常"));
|
||||
|
||||
if (txs.isEmpty()) {
|
||||
sender.sendMessage("§7 (暂无记录)");
|
||||
} else {
|
||||
for (Map<String, Object> tx : txs) {
|
||||
sender.sendMessage(
|
||||
formatTxLine(tx));
|
||||
}
|
||||
}
|
||||
sender.sendMessage(
|
||||
"§e§l=========================="
|
||||
+ "==========");
|
||||
return true;
|
||||
}
|
||||
// ===== /cypay printer [player] =====
|
||||
if ("printer".equalsIgnoreCase(args[0])) {
|
||||
if (!hasAdminPerm(sender)
|
||||
&& !(sender instanceof Player)
|
||||
== false) {
|
||||
// 控制台也可以用
|
||||
if (!(sender instanceof Player))
|
||||
; // 放行
|
||||
else if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (args.length == 1) {
|
||||
// 打印全服
|
||||
sender.sendMessage(
|
||||
"§e正在导出全服流水...");
|
||||
plugin.getBonds(); // 确保实例就绪
|
||||
if (plugin.getBondPrinter() != null) {
|
||||
plugin.getBondPrinter()
|
||||
.printAll(sender);
|
||||
} else {
|
||||
sender.sendMessage("§cPrinter未初始化");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (args.length == 2) {
|
||||
// 打印指定玩家
|
||||
String target = args[1];
|
||||
sender.sendMessage("§e正在导出 "
|
||||
+ target + " 的流水...");
|
||||
if (plugin.getBondPrinter() != null) {
|
||||
plugin.getBondPrinter()
|
||||
.printPlayer(target, sender);
|
||||
} else {
|
||||
sender.sendMessage("§cPrinter未初始化");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay give/remove =====
|
||||
if (args.length >= 3 && args.length <= 4) {
|
||||
String action = args[0].toLowerCase();
|
||||
if ("give".equals(action)
|
||||
|| "remove".equals(action)) {
|
||||
if (!hasAdminPerm(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
try {
|
||||
int amount =
|
||||
parseAmount(args[2]);
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
"§c金额必须大于0");
|
||||
return true;
|
||||
}
|
||||
String txType =
|
||||
"give".equals(action)
|
||||
? BondManager
|
||||
.TX_ADMIN_GIVE
|
||||
: BondManager
|
||||
.TX_ADMIN_DEDUCT;
|
||||
String opName =
|
||||
sender instanceof Player
|
||||
? sender.getName()
|
||||
: "Console";
|
||||
// 自定义理由(可选)
|
||||
String reason = args.length == 4
|
||||
? args[3]
|
||||
: ("give".equals(action)
|
||||
? "管理员发放"
|
||||
: "管理员扣除");
|
||||
|
||||
if ("give".equals(action)) {
|
||||
plugin.getBonds().addBonds(
|
||||
target, amount,
|
||||
txType, "",
|
||||
opName, reason);
|
||||
sender.sendMessage(
|
||||
"§a已给 " + target
|
||||
+ " " + amount
|
||||
+ " 债券");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§a管理员给了你 "
|
||||
+ amount
|
||||
+ " 债券");
|
||||
} else {
|
||||
if (plugin.getBonds()
|
||||
.deductBonds(target,
|
||||
amount,
|
||||
txType, "",
|
||||
opName,
|
||||
reason)) {
|
||||
sender.sendMessage(
|
||||
"§a已扣除 " + target
|
||||
+ " " + amount
|
||||
+ " 债券");
|
||||
Player tp = plugin
|
||||
.getServer()
|
||||
.getPlayerExact(
|
||||
target);
|
||||
if (tp != null)
|
||||
tp.sendMessage(
|
||||
"§c管理员扣除了"
|
||||
+ "你的 "
|
||||
+ amount
|
||||
+ " 债券");
|
||||
} else {
|
||||
sender.sendMessage(
|
||||
"§c" + target
|
||||
+ " 债券不足"
|
||||
+ "或账户冻结");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(
|
||||
"§c金额格式错误");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay <player> <amount> — 仅玩家转账 =====
|
||||
if (args.length == 2) {
|
||||
try {
|
||||
int amount = parseAmount(args[1]);
|
||||
if (amount <= 0) {
|
||||
sender.sendMessage(
|
||||
"§c金额必须大于0");
|
||||
return true;
|
||||
}
|
||||
// ★ 控制台禁止转账
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage(
|
||||
"§c控制台不能转账,请使用 "
|
||||
+ "/cypay give <玩家> <金额>");
|
||||
return true;
|
||||
}
|
||||
Player p = (Player) sender;
|
||||
String target = args[0];
|
||||
BondManager.TransferResult r =
|
||||
plugin.getBonds().transfer(
|
||||
p.getName(),
|
||||
target, amount);
|
||||
if (r.success) {
|
||||
p.sendMessage("§e§l[债券转账] §f向 §a"
|
||||
+ target + " §f转出 §c"
|
||||
+ amount + " §f枚债券");
|
||||
Player tp = plugin.getServer()
|
||||
.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage("§e§l[债券转账] §f收到 §a"
|
||||
+ p.getName()
|
||||
+ " §f转入 §a"
|
||||
+ amount + " §f枚债券");
|
||||
} else {
|
||||
p.sendMessage("§c" + r.message
|
||||
+ ",当前: "
|
||||
+ plugin.getBonds()
|
||||
.getBonds(p.getName()));
|
||||
}
|
||||
return true;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /cypay <player> — 查指定玩家 =====
|
||||
if (args.length == 1) {
|
||||
String target = args[0];
|
||||
// ★ 验证玩家在 login.db 中存在 ★
|
||||
if (!plugin.getDb().userExists(target)) {
|
||||
sender.sendMessage("§c玩家 " + target
|
||||
+ " 不存在于服务器");
|
||||
return true;
|
||||
}
|
||||
|
||||
// String target = args[0];
|
||||
if (hasAdminPerm(sender)) {
|
||||
int bonds = plugin.getBonds()
|
||||
.getBonds(target);
|
||||
String status = plugin.getBonds()
|
||||
.getAccountStatus(target);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f" + target
|
||||
+ " 余额: §a"
|
||||
+ bonds);
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f状态: "
|
||||
+ ("frozen".equals(
|
||||
status)
|
||||
? "§c冻结中"
|
||||
: "§a正常"));
|
||||
} else {
|
||||
int bonds = plugin.getBonds()
|
||||
.getBonds(
|
||||
sender.getName());
|
||||
sender.sendMessage(
|
||||
"§e§l[债券] §f你的余额: §a"
|
||||
+ bonds);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
showHelp(sender);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== 流水行格式化 =====
|
||||
|
||||
private String formatTxLine(
|
||||
Map<String, Object> tx) {
|
||||
String type = (String) tx.get("type");
|
||||
int amount = ((Number) tx.get("amount"))
|
||||
.intValue();
|
||||
String tgt = (String) tx.get("target_player");
|
||||
String reason = (String) tx.get("reason");
|
||||
int bal = ((Number) tx.get("balance_after"))
|
||||
.intValue();
|
||||
long time = ((Number) tx.get("time"))
|
||||
.longValue();
|
||||
|
||||
boolean pos = amount > 0;
|
||||
String sign = pos ? "+" : "";
|
||||
String color = pos ? "§a" : "§c";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("§7[")
|
||||
.append(SDF.format(new Date(time)))
|
||||
.append("] ")
|
||||
.append(formatTxType(type))
|
||||
.append(" ")
|
||||
.append(color).append(sign)
|
||||
.append(amount).append("§7");
|
||||
if (tgt != null && !tgt.isEmpty())
|
||||
sb.append(" §e↔ ").append(tgt);
|
||||
if (reason != null && !reason.isEmpty())
|
||||
sb.append(" §7(").append(reason)
|
||||
.append(")");
|
||||
sb.append(" §7余额:§f").append(bal);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatTxType(String type) {
|
||||
if (type == null) return "§7未知";
|
||||
switch (type) {
|
||||
case BondManager.TX_ADMIN_GIVE:
|
||||
return "§a管理员发放";
|
||||
case BondManager.TX_ADMIN_DEDUCT:
|
||||
return "§c管理员扣除";
|
||||
case BondManager.TX_TRANSFER_OUT:
|
||||
return "§c转出";
|
||||
case BondManager.TX_TRANSFER_IN:
|
||||
return "§a转入";
|
||||
case BondManager.TX_REDEEM:
|
||||
return "§aCDK兑换";
|
||||
case BondManager.TX_SHOP_BUY:
|
||||
return "§e商城购买";
|
||||
case BondManager.TX_DAILY_SIGN:
|
||||
return "§a签到奖励";
|
||||
case BondManager.TX_FREEZE:
|
||||
return "§c冻结";
|
||||
case BondManager.TX_UNFREEZE:
|
||||
return "§a解冻";
|
||||
default:
|
||||
return "§7" + type;
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelp(CommandSender sender) {
|
||||
sender.sendMessage("§e§l债券命令帮助:");
|
||||
sender.sendMessage(
|
||||
"§e/cypay §7- 查询自己余额");
|
||||
sender.sendMessage(
|
||||
"§e/cypay <玩家> §7- 查余额(管理员)");
|
||||
sender.sendMessage(
|
||||
"§e/cypay <玩家> <金额> §7- 转账");
|
||||
if (hasAdminPerm(sender)) {
|
||||
sender.sendMessage(
|
||||
"§e/cypay give <玩家> <金额> [理由] §7- 给债券");
|
||||
sender.sendMessage(
|
||||
"§e/cypay remove <玩家> <金额> [理由] §7- 扣债券");
|
||||
sender.sendMessage(
|
||||
"§e/cypay import <文件> §7- 导入CDK");
|
||||
sender.sendMessage(
|
||||
"§e/cypay freeze <玩家> §7- 冻结");
|
||||
sender.sendMessage(
|
||||
"§e/cypay unfreeze <玩家> §7- 解冻");
|
||||
sender.sendMessage(
|
||||
"§e/cypay water <玩家> [天数] §7- 流水");
|
||||
sender.sendMessage(
|
||||
"§e/cypay printer §7- 导出全服流水xlsx");
|
||||
sender.sendMessage(
|
||||
"§e/cypay printer <玩家> §7- 导出指定玩家xlsx");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender, Command command,
|
||||
String label, String[] args) {
|
||||
List<String> list = new ArrayList<>();
|
||||
|
||||
if (args.length == 1) {
|
||||
String last = args[0].toLowerCase();
|
||||
if (hasAdminPerm(sender)) {
|
||||
list.add("give");
|
||||
list.add("remove");
|
||||
list.add("import");
|
||||
list.add("freeze");
|
||||
list.add("unfreeze");
|
||||
list.add("water");
|
||||
list.add("printer");
|
||||
|
||||
File cdkDir = new File(
|
||||
plugin.getDataFolder(),
|
||||
"cdk");
|
||||
if (cdkDir.exists()) {
|
||||
File[] files = cdkDir.listFiles(
|
||||
(d, n) -> n
|
||||
.endsWith(".txt"));
|
||||
if (files != null)
|
||||
for (File f : files)
|
||||
list.add(f.getName());
|
||||
}
|
||||
}
|
||||
for (Player o :
|
||||
Bukkit.getOnlinePlayers())
|
||||
list.add(o.getName());
|
||||
return filter(list, last);
|
||||
}
|
||||
|
||||
if (args.length == 2) {
|
||||
String first = args[0].toLowerCase();
|
||||
if ("give".equals(first)
|
||||
|| "remove".equals(first)
|
||||
|| "freeze".equals(first)
|
||||
|| "unfreeze".equals(first)
|
||||
|| "water".equals(first)) {
|
||||
for (Player o :
|
||||
Bukkit.getOnlinePlayers())
|
||||
list.add(o.getName());
|
||||
return filter(list,
|
||||
args[1].toLowerCase());
|
||||
}
|
||||
list.add("10");
|
||||
list.add("50");
|
||||
list.add("100");
|
||||
return filter(list,
|
||||
args[1].toLowerCase());
|
||||
}
|
||||
|
||||
if (args.length == 3) {
|
||||
String first = args[0].toLowerCase();
|
||||
if ("give".equals(first)
|
||||
|| "remove".equals(first)) {
|
||||
list.add("10");
|
||||
list.add("50");
|
||||
list.add("100");
|
||||
list.add("999");
|
||||
return filter(list,
|
||||
args[2].toLowerCase());
|
||||
}
|
||||
if ("water".equals(first)) {
|
||||
list.add("1");
|
||||
list.add("3");
|
||||
list.add("7");
|
||||
list.add("14");
|
||||
return filter(list,
|
||||
args[2].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length == 4) {
|
||||
String first = args[0].toLowerCase();
|
||||
if ("give".equals(first)
|
||||
|| "remove".equals(first)) {
|
||||
list.add("管理员发放");
|
||||
list.add("管理员扣除");
|
||||
list.add("工单奖励");
|
||||
list.add("活动奖励");
|
||||
return filter(list,
|
||||
args[3].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,275 +1,284 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class EmailManager {
|
||||
|
||||
private final ConfigManager config;
|
||||
private final Logger logger;
|
||||
|
||||
public EmailManager(ConfigManager config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.getLogger("Sdf1_login");
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
return host != null && !host.isEmpty()
|
||||
&& user != null && !user.isEmpty()
|
||||
&& pass != null && !pass.isEmpty();
|
||||
}
|
||||
|
||||
public boolean sendVerifyCode(String to,
|
||||
String code) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置,"
|
||||
+ "无法发送验证码");
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送验证码到 " + to);
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 验证码",
|
||||
"您的验证码: " + code
|
||||
+ "\n5分钟内有效。\n"
|
||||
+ "如非本人操作请忽略。");
|
||||
}
|
||||
|
||||
public boolean sendTempPassword(String to,
|
||||
String player, String tempPwd) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 临时密码",
|
||||
"玩家 " + player
|
||||
+ " 的临时密码: " + tempPwd
|
||||
+ "\n请尽快登录并修改密码!");
|
||||
}
|
||||
|
||||
private boolean sendEmailSync(String to,
|
||||
String subject, String body) {
|
||||
final boolean[] result = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
result[0] = sendEmail(
|
||||
to, subject, body);
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件线程异常: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}, "Sdf1_login-SMTP");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try {
|
||||
t.join(15000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP超时(15秒)");
|
||||
return false;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private boolean sendEmail(String to,
|
||||
String subject, String body) {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String portStr = config.getSmtp("smtp端口");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
String fromName = config.getSmtp(
|
||||
"发件人名称");
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (Exception e) {
|
||||
port = 465;
|
||||
}
|
||||
|
||||
try {
|
||||
String encSubject = "=?UTF-8?B?"
|
||||
+ Base64.getEncoder()
|
||||
.encodeToString(
|
||||
subject.getBytes("UTF-8"))
|
||||
+ "?=";
|
||||
String encBody = Base64.getEncoder()
|
||||
.encodeToString(
|
||||
body.getBytes("UTF-8"));
|
||||
|
||||
if (port == 465) {
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket socket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
host, port);
|
||||
socket.setSoTimeout(10000);
|
||||
doSmtp(socket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
socket.close();
|
||||
} else {
|
||||
Socket plainSocket =
|
||||
new Socket(host, port);
|
||||
plainSocket.setSoTimeout(10000);
|
||||
BufferedReader preReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
plainSocket
|
||||
.getInputStream(),
|
||||
"UTF-8"));
|
||||
PrintWriter preWriter =
|
||||
new PrintWriter(
|
||||
plainSocket
|
||||
.getOutputStream(),
|
||||
true);
|
||||
checkResponse(preReader, "220");
|
||||
sendLine(preWriter,
|
||||
"EHLO " + host);
|
||||
checkResponse(preReader, "250");
|
||||
sendLine(preWriter, "STARTTLS");
|
||||
checkResponse(preReader, "220");
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket sslSocket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
plainSocket, host,
|
||||
port, true);
|
||||
sslSocket.startHandshake();
|
||||
doSmtp(sslSocket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
sslSocket.close();
|
||||
}
|
||||
logger.info(
|
||||
"[Sdf1_login] 邮件发送成功 -> "
|
||||
+ to);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件异常: "
|
||||
+ e.getClass()
|
||||
.getSimpleName()
|
||||
+ ": " + e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 根因: "
|
||||
+ e.getCause()
|
||||
.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSmtp(SSLSocket socket,
|
||||
String host, String user, String pass,
|
||||
String to, String subject, String body,
|
||||
String fromName) throws Exception {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
socket.getInputStream(),
|
||||
"UTF-8"));
|
||||
OutputStream rawOut =
|
||||
socket.getOutputStream();
|
||||
PrintWriter writer =
|
||||
new PrintWriter(
|
||||
new java.io.BufferedOutputStream(
|
||||
rawOut), true);
|
||||
|
||||
checkResponse(reader, "220");
|
||||
sendLine(writer, "EHLO " + host);
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "AUTH LOGIN");
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
user.getBytes("UTF-8")));
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
pass.getBytes("UTF-8")));
|
||||
checkResponse(reader, "235");
|
||||
sendLine(writer,
|
||||
"MAIL FROM:<" + user + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer,
|
||||
"RCPT TO:<" + to + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "DATA");
|
||||
checkResponse(reader, "354");
|
||||
sendLine(writer,
|
||||
"From: " + fromName
|
||||
+ " <" + user + ">");
|
||||
sendLine(writer,
|
||||
"To: <" + to + ">");
|
||||
sendLine(writer,
|
||||
"Reply-To: <" + user + ">");
|
||||
sendLine(writer,
|
||||
"Subject: " + subject);
|
||||
sendLine(writer, "MIME-Version: 1.0");
|
||||
sendLine(writer,
|
||||
"Content-Type: text/plain; "
|
||||
+ "charset=UTF-8");
|
||||
sendLine(writer,
|
||||
"Content-Transfer-Encoding: "
|
||||
+ "base64");
|
||||
sendLine(writer, "");
|
||||
sendLine(writer, body);
|
||||
sendLine(writer, ".");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "QUIT");
|
||||
}
|
||||
|
||||
private void sendLine(PrintWriter writer,
|
||||
String line) {
|
||||
writer.print(line + "\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private void checkResponse(
|
||||
BufferedReader reader,
|
||||
String expected) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
if (line.length() >= 4
|
||||
&& Character.isDigit(
|
||||
line.charAt(0))
|
||||
&& line.charAt(3) == ' ')
|
||||
break;
|
||||
}
|
||||
String resp = sb.toString().trim();
|
||||
if (!resp.startsWith(expected)) {
|
||||
if (resp.startsWith("535"))
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP 535 "
|
||||
+ "认证失败");
|
||||
throw new Exception(
|
||||
"SMTP响应异常: 期望 "
|
||||
+ expected
|
||||
+ ",实际: " + resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class EmailManager {
|
||||
|
||||
private final ConfigManager config;
|
||||
private final Logger logger;
|
||||
|
||||
public EmailManager(ConfigManager config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.getLogger("Sdf1_login");
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
return host != null && !host.isEmpty()
|
||||
&& user != null && !user.isEmpty()
|
||||
&& pass != null && !pass.isEmpty();
|
||||
}
|
||||
|
||||
public boolean sendVerifyCode(String to,
|
||||
String code) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置,"
|
||||
+ "无法发送验证码");
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送验证码到 " + to);
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 验证码",
|
||||
"您的验证码: " + code
|
||||
+ "\n5分钟内有效。\n"
|
||||
+ "如非本人操作请忽略。");
|
||||
}
|
||||
|
||||
public boolean sendTempPassword(String to,
|
||||
String player, String tempPwd) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning(
|
||||
"[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to,
|
||||
"[Sdf1_login] 临时密码",
|
||||
"玩家 " + player
|
||||
+ " 的临时密码: " + tempPwd
|
||||
+ "\n请尽快登录并修改密码!");
|
||||
}
|
||||
|
||||
public boolean sendBody(String to, String subject, String body) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning("[Sdf1_login] SMTP未配置");
|
||||
return false;
|
||||
}
|
||||
return sendEmailSync(to, subject, body);
|
||||
}
|
||||
|
||||
|
||||
private boolean sendEmailSync(String to,
|
||||
String subject, String body) {
|
||||
final boolean[] result = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
result[0] = sendEmail(
|
||||
to, subject, body);
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件线程异常: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}, "Sdf1_login-SMTP");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try {
|
||||
t.join(15000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP超时(15秒)");
|
||||
return false;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private boolean sendEmail(String to,
|
||||
String subject, String body) {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String portStr = config.getSmtp("smtp端口");
|
||||
String user = config.getSmtp("smtp账号");
|
||||
String pass = config.getSmtp("smtp密码");
|
||||
String fromName = config.getSmtp(
|
||||
"发件人名称");
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} catch (Exception e) {
|
||||
port = 465;
|
||||
}
|
||||
|
||||
try {
|
||||
String encSubject = "=?UTF-8?B?"
|
||||
+ Base64.getEncoder()
|
||||
.encodeToString(
|
||||
subject.getBytes("UTF-8"))
|
||||
+ "?=";
|
||||
String encBody = Base64.getEncoder()
|
||||
.encodeToString(
|
||||
body.getBytes("UTF-8"));
|
||||
|
||||
if (port == 465) {
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket socket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
host, port);
|
||||
socket.setSoTimeout(10000);
|
||||
doSmtp(socket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
socket.close();
|
||||
} else {
|
||||
Socket plainSocket =
|
||||
new Socket(host, port);
|
||||
plainSocket.setSoTimeout(10000);
|
||||
BufferedReader preReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
plainSocket
|
||||
.getInputStream(),
|
||||
"UTF-8"));
|
||||
PrintWriter preWriter =
|
||||
new PrintWriter(
|
||||
plainSocket
|
||||
.getOutputStream(),
|
||||
true);
|
||||
checkResponse(preReader, "220");
|
||||
sendLine(preWriter,
|
||||
"EHLO " + host);
|
||||
checkResponse(preReader, "250");
|
||||
sendLine(preWriter, "STARTTLS");
|
||||
checkResponse(preReader, "220");
|
||||
SSLSocketFactory factory =
|
||||
(SSLSocketFactory)
|
||||
SSLSocketFactory
|
||||
.getDefault();
|
||||
SSLSocket sslSocket = (SSLSocket)
|
||||
factory.createSocket(
|
||||
plainSocket, host,
|
||||
port, true);
|
||||
sslSocket.startHandshake();
|
||||
doSmtp(sslSocket, host, user, pass,
|
||||
to, encSubject, encBody,
|
||||
fromName);
|
||||
sslSocket.close();
|
||||
}
|
||||
logger.info(
|
||||
"[Sdf1_login] 邮件发送成功 -> "
|
||||
+ to);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 邮件异常: "
|
||||
+ e.getClass()
|
||||
.getSimpleName()
|
||||
+ ": " + e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
logger.severe(
|
||||
"[Sdf1_login] 根因: "
|
||||
+ e.getCause()
|
||||
.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSmtp(SSLSocket socket,
|
||||
String host, String user, String pass,
|
||||
String to, String subject, String body,
|
||||
String fromName) throws Exception {
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
socket.getInputStream(),
|
||||
"UTF-8"));
|
||||
OutputStream rawOut =
|
||||
socket.getOutputStream();
|
||||
PrintWriter writer =
|
||||
new PrintWriter(
|
||||
new java.io.BufferedOutputStream(
|
||||
rawOut), true);
|
||||
|
||||
checkResponse(reader, "220");
|
||||
sendLine(writer, "EHLO " + host);
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "AUTH LOGIN");
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
user.getBytes("UTF-8")));
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder()
|
||||
.encodeToString(
|
||||
pass.getBytes("UTF-8")));
|
||||
checkResponse(reader, "235");
|
||||
sendLine(writer,
|
||||
"MAIL FROM:<" + user + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer,
|
||||
"RCPT TO:<" + to + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "DATA");
|
||||
checkResponse(reader, "354");
|
||||
sendLine(writer,
|
||||
"From: " + fromName
|
||||
+ " <" + user + ">");
|
||||
sendLine(writer,
|
||||
"To: <" + to + ">");
|
||||
sendLine(writer,
|
||||
"Reply-To: <" + user + ">");
|
||||
sendLine(writer,
|
||||
"Subject: " + subject);
|
||||
sendLine(writer, "MIME-Version: 1.0");
|
||||
sendLine(writer,
|
||||
"Content-Type: text/plain; "
|
||||
+ "charset=UTF-8");
|
||||
sendLine(writer,
|
||||
"Content-Transfer-Encoding: "
|
||||
+ "base64");
|
||||
sendLine(writer, "");
|
||||
sendLine(writer, body);
|
||||
sendLine(writer, ".");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "QUIT");
|
||||
}
|
||||
|
||||
private void sendLine(PrintWriter writer,
|
||||
String line) {
|
||||
writer.print(line + "\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private void checkResponse(
|
||||
BufferedReader reader,
|
||||
String expected) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
if (line.length() >= 4
|
||||
&& Character.isDigit(
|
||||
line.charAt(0))
|
||||
&& line.charAt(3) == ' ')
|
||||
break;
|
||||
}
|
||||
String resp = sb.toString().trim();
|
||||
if (!resp.startsWith(expected)) {
|
||||
if (resp.startsWith("535"))
|
||||
logger.severe(
|
||||
"[Sdf1_login] SMTP 535 "
|
||||
+ "认证失败");
|
||||
throw new Exception(
|
||||
"SMTP响应异常: 期望 "
|
||||
+ expected
|
||||
+ ",实际: " + resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -230,27 +228,26 @@ public class GarbageManager {
|
||||
}
|
||||
|
||||
public void saveItem(ItemStack stack) {
|
||||
{
|
||||
try {
|
||||
String data =
|
||||
stackToBase64(stack);
|
||||
if (data.isEmpty()) return;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO items "
|
||||
+ "(item_data, amount, "
|
||||
+ "round, create_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, data);
|
||||
ps.setInt(2, stack.getAmount());
|
||||
ps.setInt(3, currentRound);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}}
|
||||
try {
|
||||
String data = stackToBase64(stack);
|
||||
if (data.isEmpty()) return;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO items "
|
||||
+ "(item_data, amount, "
|
||||
+ "round, create_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, data);
|
||||
ps.setInt(2, stack.getAmount());
|
||||
ps.setInt(3, currentRound);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int deleteOldItems(int beforeRound) {
|
||||
try {
|
||||
@@ -288,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) {
|
||||
@@ -344,11 +338,9 @@ public class GarbageManager {
|
||||
}
|
||||
|
||||
public void openRecyclePage(Player p, int page) {
|
||||
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, "§6§l垃圾回收站");
|
||||
|
||||
// 获取全部存储物品
|
||||
List<Map<String, Object>> allItems =
|
||||
getStoredItems(999);
|
||||
int total = allItems.size();
|
||||
@@ -381,38 +373,6 @@ public class GarbageManager {
|
||||
.get("amount")).intValue();
|
||||
ItemStack stack = base64ToStack(data);
|
||||
if (stack != null) {
|
||||
try {
|
||||
byte[] bytes =
|
||||
stack.serializeAsBytes();
|
||||
String b64 = java.util.Base64
|
||||
.getEncoder()
|
||||
.encodeToString(bytes);
|
||||
int len = b64.length();
|
||||
String f = len > 15
|
||||
? b64.substring(10, 15) : "短";
|
||||
String m = len >= 5
|
||||
? b64.substring(len / 2,
|
||||
Math.min(len / 2 + 5, len))
|
||||
: "短";
|
||||
String b = len > 10
|
||||
? b64.substring(len - 10, len - 5)
|
||||
: "短";
|
||||
plugin.getLogger().info(
|
||||
"[垃圾站加载] ID#" + id
|
||||
+ " " + stack.getType()
|
||||
+ " x" + amount
|
||||
+ " base64[" + len + "]"
|
||||
+ " 前=" + f
|
||||
+ " 中=" + m
|
||||
+ " 后=" + b);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning(
|
||||
"[垃圾站加载] 序列化异常: "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (stack != null) {
|
||||
stack.setAmount(amount);
|
||||
ItemMeta im =
|
||||
stack.getItemMeta();
|
||||
@@ -429,10 +389,7 @@ public class GarbageManager {
|
||||
slot++;
|
||||
}
|
||||
|
||||
// 45-48 格:额外投放区(空位)
|
||||
// 52-53 格:额外投放区(空位)
|
||||
|
||||
// 49 格:翻页-上一页
|
||||
// 49 格:上一页
|
||||
if (page > 0) {
|
||||
g.setItem(49, mk(Material.ARROW,
|
||||
"§e上一页 §7("
|
||||
@@ -451,9 +408,8 @@ public class GarbageManager {
|
||||
"§7当前页: §f" + (page + 1)
|
||||
+ "/" + totalPages));
|
||||
|
||||
// 51 格:翻页-下一页
|
||||
boolean canNext = total >= pageSize
|
||||
&& page < totalPages - 1;
|
||||
// 51 格:下一页
|
||||
boolean canNext = page < totalPages - 1;
|
||||
if (canNext) {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§e下一页 §7("
|
||||
@@ -463,13 +419,15 @@ public class GarbageManager {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§7已是最后一页"));
|
||||
}
|
||||
// 52格:返回按钮
|
||||
|
||||
// 52 格:返回主菜单
|
||||
g.setItem(52, mk(Material.ARROW,
|
||||
"§7返回",
|
||||
"§7返回主菜单"));
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public int getRecyclePage(String name) {
|
||||
return recyclePages
|
||||
.getOrDefault(name, 0);
|
||||
|
||||
@@ -22,7 +22,7 @@ public class GiftManager {
|
||||
public GiftManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.giftFolder = new File(
|
||||
plugin.getDataFolder(), "新人礼包");
|
||||
plugin.getDataFolder(), "任务面板");
|
||||
}
|
||||
|
||||
public void loadStages() {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,114 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IPGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final int maxAccounts;
|
||||
|
||||
public IPGroupManager(Main plugin, int maxAccounts) {
|
||||
this.plugin = plugin;
|
||||
this.maxAccounts = maxAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查该IP是否还能注册新账号
|
||||
*/
|
||||
public boolean canRegister(String ip) {
|
||||
if (maxAccounts <= 0) return true;
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
return queryAccountsByIP(ip).size() < maxAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该IP下已注册的账号数量
|
||||
*/
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该IP下所有账号名
|
||||
*/
|
||||
public List<String> getAccounts(String ip) {
|
||||
return queryAccountsByIP(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接通过数据库连接查询指定IP下的所有账号
|
||||
*/
|
||||
private List<String> queryAccountsByIP(String ip) {
|
||||
List<String> accounts = new ArrayList<>();
|
||||
Connection conn = plugin.getDb().getDb();
|
||||
if (conn == null) return accounts;
|
||||
try {
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
accounts.add(
|
||||
rs.getString("player_name"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IPGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final int maxAccounts;
|
||||
|
||||
public IPGroupManager(Main plugin, int maxAccounts) {
|
||||
this.plugin = plugin;
|
||||
this.maxAccounts = maxAccounts;
|
||||
}
|
||||
|
||||
public boolean canRegister(String ip) {
|
||||
if (maxAccounts <= 0) return true;
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
int count = getAccountCount(ip);
|
||||
boolean can = count < maxAccounts;
|
||||
plugin.getLogger().info(
|
||||
"[IP限制] IP=" + ip
|
||||
+ " 已注册=" + count
|
||||
+ " 上限=" + maxAccounts
|
||||
+ " 允许=" + can);
|
||||
return can;
|
||||
}
|
||||
|
||||
public int getMaxAccounts() {
|
||||
return maxAccounts;
|
||||
}
|
||||
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
}
|
||||
|
||||
public List<String> getAccounts(String ip) {
|
||||
return queryAccountsByIP(ip);
|
||||
}
|
||||
|
||||
private List<String> queryAccountsByIP(String ip) {
|
||||
List<String> accounts = new ArrayList<>();
|
||||
Connection conn = plugin.getDb().getDb();
|
||||
if (conn == null) return accounts;
|
||||
try {
|
||||
// 主查询:ip_address 列
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
accounts.add(
|
||||
rs.getString("player_name"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
|
||||
// 兜底:如果主列没数据,查 last_login_ip
|
||||
if (accounts.isEmpty()) {
|
||||
PreparedStatement ps2 = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE last_login_ip = ? "
|
||||
+ "AND (ip_address IS NULL "
|
||||
+ "OR ip_address = '' "
|
||||
+ "OR ip_address != ?)");
|
||||
ps2.setString(1, ip);
|
||||
ps2.setString(2, ip);
|
||||
ResultSet rs2 = ps2.executeQuery();
|
||||
while (rs2.next()) {
|
||||
String name = rs2.getString("player_name");
|
||||
if (!accounts.contains(name))
|
||||
accounts.add(name);
|
||||
}
|
||||
rs2.close();
|
||||
ps2.close();
|
||||
}
|
||||
|
||||
// 兜底2:查 register_ip
|
||||
if (accounts.isEmpty()) {
|
||||
PreparedStatement ps3 = conn.prepareStatement(
|
||||
"SELECT player_name FROM users "
|
||||
+ "WHERE register_ip = ? "
|
||||
+ "AND (ip_address IS NULL "
|
||||
+ "OR ip_address = '') "
|
||||
+ "AND (last_login_ip IS NULL "
|
||||
+ "OR last_login_ip = '' "
|
||||
+ "OR last_login_ip != ?)");
|
||||
ps3.setString(1, ip);
|
||||
ps3.setString(2, ip);
|
||||
ResultSet rs3 = ps3.executeQuery();
|
||||
while (rs3.next()) {
|
||||
String name = rs3.getString("player_name");
|
||||
if (!accounts.contains(name))
|
||||
accounts.add(name);
|
||||
}
|
||||
rs3.close();
|
||||
ps3.close();
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[IP限制] 查询IP=" + ip
|
||||
+ " 结果数=" + accounts.size()
|
||||
+ " 账号=" + accounts);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + " 背包已备份");
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class InventorySerializer {
|
||||
|
||||
public static String toBase64(ItemStack[] items) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
dataOutput.close();
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to serialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack[] fromBase64(String data) throws IOException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||
ItemStack[] items = new ItemStack[dataInput.readInt()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = (ItemStack) dataInput.readObject();
|
||||
}
|
||||
dataInput.close();
|
||||
return items;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to deserialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class InventorySerializer {
|
||||
|
||||
public static String toBase64(ItemStack[] items) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
dataOutput.close();
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to serialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack[] fromBase64(String data) throws IOException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||
ItemStack[] items = new ItemStack[dataInput.readInt()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = (ItemStack) dataInput.readObject();
|
||||
}
|
||||
dataInput.close();
|
||||
return items;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to deserialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class IronBlockElevator implements Listener {
|
||||
|
||||
private static final int MAX_RANGE = 64;
|
||||
private static final int MIN_RANGE = 5; // 屏蔽±5格以内的铁块
|
||||
private final Set<UUID> cooldown = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 检查指定位置脚下是否是铁块
|
||||
*/
|
||||
private boolean isIronBlockBelow(Location loc) {
|
||||
Block block = loc.getWorld().getBlockAt(
|
||||
loc.getBlockX(), loc.getBlockY() - 1,
|
||||
loc.getBlockZ());
|
||||
return block.getType() == Material.IRON_BLOCK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从起始位置向上或向下搜索铁块并传送
|
||||
* @param dir 1=向上 -1=向下
|
||||
*/
|
||||
private boolean tryTeleport(Player p, int dir) {
|
||||
Location loc = p.getLocation();
|
||||
int baseY = loc.getBlockY();
|
||||
|
||||
for (int i = MIN_RANGE; i < MAX_RANGE; i++) {
|
||||
int y = baseY + i * dir;
|
||||
Block block = loc.getWorld().getBlockAt(
|
||||
loc.getBlockX(), y, loc.getBlockZ());
|
||||
if (block.getType() == Material.IRON_BLOCK) {
|
||||
cooldown.add(p.getUniqueId());
|
||||
Location dest = new Location(
|
||||
loc.getWorld(),
|
||||
loc.getX(),
|
||||
block.getY() + 1,
|
||||
loc.getZ());
|
||||
p.teleport(dest);
|
||||
org.bukkit.Bukkit.getScheduler()
|
||||
.runTaskLater(
|
||||
org.bukkit.Bukkit.getPluginManager()
|
||||
.getPlugin("Sdf1_login"),
|
||||
() -> cooldown.remove(
|
||||
p.getUniqueId()),
|
||||
15L);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== 下蹲 → 向下传送 =====
|
||||
@EventHandler
|
||||
public void onSneak(PlayerToggleSneakEvent e) {
|
||||
if (!e.isSneaking()) return;
|
||||
Player p = e.getPlayer();
|
||||
if (cooldown.contains(p.getUniqueId())) return;
|
||||
// 必须站在铁块上
|
||||
if (!isIronBlockBelow(p.getLocation())) return;
|
||||
tryTeleport(p, -1);
|
||||
}
|
||||
|
||||
// ===== 跳跃 → 向上传送 =====
|
||||
@EventHandler
|
||||
public void onMove(PlayerMoveEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
if (cooldown.contains(p.getUniqueId())) return;
|
||||
if (p.isSneaking()) return;
|
||||
|
||||
Location from = e.getFrom();
|
||||
Location to = e.getTo();
|
||||
if (to == null) return;
|
||||
if (to.getY() <= from.getY()) return;
|
||||
if (p.getVelocity().getY() <= 0) return;
|
||||
|
||||
// 关键:检查跳跃前的位置(from)脚下是否是铁块
|
||||
// 跳跃后玩家已离开铁块上方,检查 to 会失败
|
||||
if (!isIronBlockBelow(from)) return;
|
||||
|
||||
tryTeleport(p, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 收集查询坐标组:大箱子(double chest)由两个方块组成,
|
||||
* 记录写入时用"被点击的那一半"坐标,查询时用"准心对准的那一半"坐标,
|
||||
* 两边坐标不一致就会查不到记录。这里沿同层同类型箱子扩展,
|
||||
* 把整只大箱子的所有坐标一起查出来再合并。
|
||||
*/
|
||||
private List<Location> collectQueryLocations(Location loc) {
|
||||
List<Location> result = new ArrayList<>();
|
||||
result.add(loc);
|
||||
Block start;
|
||||
try {
|
||||
start = loc.getBlock();
|
||||
} catch (Exception ex) {
|
||||
return result; // 世界未加载等异常,退回单点查询
|
||||
}
|
||||
Material type = start.getType();
|
||||
// 只有能拼成大箱子的类型才需要扩展
|
||||
if (type != Material.CHEST && type != Material.TRAPPED_CHEST) {
|
||||
return result;
|
||||
}
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
seen.add(keyOf(start));
|
||||
Deque<Block> queue = new ArrayDeque<>();
|
||||
queue.add(start);
|
||||
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
while (!queue.isEmpty()) {
|
||||
Block cur = queue.poll();
|
||||
for (int[] d : dirs) {
|
||||
Block n = cur.getRelative(d[0], 0, d[1]);
|
||||
if (n.getType() != type) continue;
|
||||
if (seen.add(keyOf(n))) {
|
||||
result.add(n.getLocation());
|
||||
queue.add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String keyOf(Block b) {
|
||||
return b.getX() + "," + b.getY() + "," + b.getZ();
|
||||
}
|
||||
|
||||
private void queryAndPrint(Player p, Location loc, int page) {
|
||||
String world = loc.getWorld().getName();
|
||||
int x = loc.getBlockX(), y = loc.getBlockY(),
|
||||
z = loc.getBlockZ();
|
||||
|
||||
// ★ 大箱子:把整只箱子的所有半边坐标都查一遍
|
||||
List<Location> group = collectQueryLocations(loc);
|
||||
|
||||
// 按天分页:page=0今日,1昨日,2前日...
|
||||
List<Map<String, Object>> rawBlocks = new ArrayList<>();
|
||||
List<Map<String, Object>> rawContainers = new ArrayList<>();
|
||||
for (Location g : group) {
|
||||
int gx = g.getBlockX(), gy = g.getBlockY(), gz = g.getBlockZ();
|
||||
rawBlocks.addAll(plugin.getDb().getLandBlockLogAt(
|
||||
world, gx, gy, gz, QUERY_LIMIT, page));
|
||||
rawContainers.addAll(plugin.getDb().getLandContainerLogAt(
|
||||
world, gx, gy, gz, QUERY_LIMIT, page));
|
||||
}
|
||||
sortByTimeDesc(rawBlocks);
|
||||
sortByTimeDesc(rawContainers);
|
||||
List<Map<String, Object>> blocks = truncate(rawBlocks);
|
||||
List<Map<String, Object>> containers = truncate(rawContainers);
|
||||
|
||||
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;
|
||||
for (Location g : group) {
|
||||
int gx = g.getBlockX(), gy = g.getBlockY(), gz = g.getBlockZ();
|
||||
if (!plugin.getDb().getLandBlockLogAt(world, gx, gy, gz, 1, page + 1).isEmpty()) {
|
||||
hasNext = true;
|
||||
break;
|
||||
}
|
||||
if (!plugin.getDb().getLandContainerLogAt(world, gx, gy, gz, 1, page + 1).isEmpty()) {
|
||||
hasNext = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 按 time 倒序(新→旧),兼容 Long/Number 两种取值 */
|
||||
private void sortByTimeDesc(List<Map<String, Object>> list) {
|
||||
list.sort((a, b) -> Long.compare(timeOf(b), timeOf(a)));
|
||||
}
|
||||
|
||||
private long timeOf(Map<String, Object> r) {
|
||||
Object v = r.get("time");
|
||||
return v instanceof Number ? ((Number) v).longValue() : 0L;
|
||||
}
|
||||
|
||||
/** 截断到 QUERY_LIMIT 条,返回新列表 */
|
||||
private List<Map<String, Object>> truncate(List<Map<String, Object>> list) {
|
||||
if (list.size() <= QUERY_LIMIT) return list;
|
||||
return new ArrayList<>(list.subList(0, QUERY_LIMIT));
|
||||
}
|
||||
|
||||
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,12 @@ 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);
|
||||
plugin.getDb().setField(name,
|
||||
"last_login_time",
|
||||
@@ -84,6 +90,8 @@ public class LoginManager {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("reg_success"));
|
||||
playRegisterSound(p);
|
||||
// ★ 注册成功:actionbar 提示1分钟保存刚才的账号和密码
|
||||
plugin.showCredentialSaveReminder(p, name, pwd);
|
||||
|
||||
try {
|
||||
org.bukkit.plugin.Plugin cy =
|
||||
@@ -98,6 +106,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;
|
||||
}
|
||||
|
||||
@@ -116,7 +135,16 @@ public class LoginManager {
|
||||
|
||||
public boolean handleLogin(Player p,
|
||||
String[] args) {
|
||||
|
||||
String name = p.getName();
|
||||
// ★ 在 handleLogin 方法开头
|
||||
if (plugin.getNeedsPasswordChange().contains(name)
|
||||
&& plugin.getLoggedIn().contains(name)) {
|
||||
// 已登录但还没改密码,只允许执行 /sdf1_login pw
|
||||
p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (plugin.getLoggedIn().contains(name)) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
@@ -128,6 +156,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;
|
||||
@@ -150,7 +188,15 @@ 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(
|
||||
name, true);
|
||||
plugin.getDb().setField(name,
|
||||
@@ -159,6 +205,19 @@ 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);
|
||||
// 只在有有效备份时恢复
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
@@ -166,41 +225,66 @@ public class LoginManager {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("login_success"));
|
||||
activateCY(p, name);
|
||||
// ★ 手动登录后恢复区域效果
|
||||
Sdf1_login.AreaProtection areaProt =
|
||||
plugin.getAreaProtection();
|
||||
if (areaProt != null) {
|
||||
areaProt.onPlayerJoin(p);
|
||||
}
|
||||
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);
|
||||
plugin.getDb().setLoggedIn(
|
||||
name, true);
|
||||
plugin.getDb().setField(name,
|
||||
"last_login_time",
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
plugin.webManager.recordJavaLogin(name);
|
||||
}
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
p.setAllowFlight(false);
|
||||
p.setFlying(false);
|
||||
plugin.getDb().setField(name, "last_login_time",
|
||||
System.currentTimeMillis());
|
||||
plugin.getDb().setField(name,
|
||||
"last_online_check",
|
||||
plugin.getDb().setField(name, "last_online_check",
|
||||
System.currentTimeMillis());
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
plugin.giveMenuSnowball(p);
|
||||
plugin.getNeedsPasswordChange()
|
||||
.add(name);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("login_success"));
|
||||
p.sendMessage(
|
||||
"§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
|
||||
p.sendMessage(
|
||||
"§7用法: /sdf1_login pw");
|
||||
// ★ 确保这行存在
|
||||
plugin.getNeedsPasswordChange().add(name);
|
||||
p.sendMessage(plugin.getConfig2().msg("login_success"));
|
||||
p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
|
||||
p.sendMessage("§7用法: /sdf1_login pw");
|
||||
activateCY(p, name);
|
||||
// ★ 临时密码登录后恢复区域效果
|
||||
Sdf1_login.AreaProtection areaProt =
|
||||
plugin.getAreaProtection();
|
||||
if (areaProt != null) {
|
||||
areaProt.onPlayerJoin(p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ★ 密码错误,记录风控
|
||||
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异常: "
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 多源在线翻译工具 —— 将MC英文材料名翻译为中文。
|
||||
* 优先使用国内接口(有道/简心),失败后降级到Google。
|
||||
* 结果缓存避免重复请求。
|
||||
*/
|
||||
public class MaterialTranslator {
|
||||
|
||||
private static final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
||||
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
private static final HttpClient http = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(3))
|
||||
.build();
|
||||
|
||||
// 翻译源优先级:国内接口优先
|
||||
private static final List<TranslationSource> SOURCES = Arrays.asList(
|
||||
TranslationSource.YOUDAO,
|
||||
TranslationSource.JIANXIN,
|
||||
TranslationSource.GOOGLE
|
||||
);
|
||||
|
||||
/**
|
||||
* 翻译源枚举
|
||||
*/
|
||||
private enum TranslationSource {
|
||||
YOUDAO("有道翻译"),
|
||||
JIANXIN("简心翻译"),
|
||||
GOOGLE("Google翻译");
|
||||
|
||||
final String name;
|
||||
|
||||
TranslationSource(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将材料名(如 GRASS_BLOCK)转为可读英文(Grass Block)
|
||||
*/
|
||||
public static String toReadable(String materialName) {
|
||||
if (materialName == null || materialName.isEmpty()) return "";
|
||||
return Arrays.stream(materialName.split("_"))
|
||||
.map(w -> w.substring(0, 1).toUpperCase() + w.substring(1).toLowerCase())
|
||||
.reduce((a, b) -> a + " " + b)
|
||||
.orElse(materialName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步翻译单个材料名(有缓存则直接返回)。
|
||||
* 优先走 MC 官方 zh_cn.json 翻译,查不到才走机翻。
|
||||
*/
|
||||
public static String translate(String materialName) {
|
||||
if (materialName == null || materialName.isEmpty()) return "";
|
||||
String key = materialName.toUpperCase();
|
||||
|
||||
// 缓存命中
|
||||
if (cache.containsKey(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
// 1. 官方翻译(基于原始 Material 名 GRASS_BLOCK → block.minecraft.grass_block)
|
||||
String official = OfficialTranslations.lookUp(materialName);
|
||||
if (official != null && !official.isEmpty()) {
|
||||
cache.put(key, official);
|
||||
return official;
|
||||
}
|
||||
|
||||
// 2. 机翻兜底
|
||||
String readable = toReadable(key);
|
||||
String zh = translateWithFallback(readable);
|
||||
if (zh != null && !zh.isEmpty()) {
|
||||
cache.put(key, zh);
|
||||
return zh;
|
||||
}
|
||||
|
||||
return readable; // fallback 可读英文
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量异步翻译,返回 future map
|
||||
*/
|
||||
public static CompletableFuture<Map<String, String>> translateBatch(Collection<String> materials) {
|
||||
Set<String> unique = new LinkedHashSet<>();
|
||||
for (String m : materials) {
|
||||
if (m != null && !m.isEmpty() && !cache.containsKey(m.toUpperCase())) {
|
||||
unique.add(m);
|
||||
}
|
||||
}
|
||||
if (unique.isEmpty()) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String m : materials) {
|
||||
if (m != null) result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||
}
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
for (String m : unique) {
|
||||
futures.add(CompletableFuture.runAsync(() -> translate(m), executor));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.thenApply(v -> {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String m : materials) {
|
||||
if (m != null) {
|
||||
result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 多源翻译,按优先级尝试,失败自动降级
|
||||
*/
|
||||
private static String translateWithFallback(String text) {
|
||||
for (TranslationSource source : SOURCES) {
|
||||
try {
|
||||
String result = null;
|
||||
switch (source) {
|
||||
case YOUDAO:
|
||||
result = translateViaYoudao(text);
|
||||
break;
|
||||
case JIANXIN:
|
||||
result = translateViaJianxin(text);
|
||||
break;
|
||||
case GOOGLE:
|
||||
result = translateViaGoogle(text);
|
||||
break;
|
||||
}
|
||||
if (result != null && !result.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Bukkit.getLogger().warning("[翻译] 机翻 " + source.name + " 异常: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 翻译源实现 ====================
|
||||
|
||||
/**
|
||||
* 有道翻译API
|
||||
* URL: https://v.api.aa1.cn/api/api-fanyi-yd/index.php
|
||||
* 参数: msg=翻译内容, type=1(中→英) / 2(英→中)
|
||||
*/
|
||||
private static String translateViaYoudao(String text) {
|
||||
try {
|
||||
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||
// type=2 表示英→中
|
||||
String url = "https://v.api.aa1.cn/api/api-fanyi-yd/index.php?msg=" + encoded + "&type=2";
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() == 200) {
|
||||
// 返回: {"type":"中英互译","desc":"中文翻译英文","text":"翻译结果"}
|
||||
return parseJsonField(resp.body(), "text");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 静默降级
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简心翻译API
|
||||
* URL: https://api.qvqa.cn/api/fanyi
|
||||
* 参数: text=翻译内容, source=en, target=zh
|
||||
*/
|
||||
private static String translateViaJianxin(String text) {
|
||||
try {
|
||||
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||
String url = "https://api.qvqa.cn/api/fanyi?text=" + encoded + "&source=en&target=zh";
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() == 200) {
|
||||
// 返回: {"code":200,"message":"OK","data":{"sourceText":"...","targetText":"翻译结果",...}}
|
||||
return parseNestedJsonField(resp.body(), "data", "targetText");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 静默降级
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Translate 免费接口
|
||||
*/
|
||||
private static String translateViaGoogle(String text) {
|
||||
try {
|
||||
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||
String url = "https://clients5.google.com/translate_a/single"
|
||||
+ "?client=gtx&sl=en&tl=zh-CN&dt=t&q=" + encoded;
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("User-Agent", "Mozilla/5.0")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() == 200) {
|
||||
// 响应格式: [["翻译结果","原文",...],...]
|
||||
String body = resp.body();
|
||||
int firstQuote = body.indexOf("[[\"");
|
||||
if (firstQuote >= 0) {
|
||||
int endQuote = body.indexOf("\"", firstQuote + 3);
|
||||
if (endQuote > firstQuote) {
|
||||
return body.substring(firstQuote + 3, endQuote);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 静默降级
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== JSON解析工具 ====================
|
||||
|
||||
/**
|
||||
* 简单JSON字段解析(单层)
|
||||
* 从 {"key":"value"} 中提取 value
|
||||
*/
|
||||
private static String parseJsonField(String json, String field) {
|
||||
try {
|
||||
String pattern = "\"" + field + "\"";
|
||||
int idx = json.indexOf(pattern);
|
||||
if (idx < 0) return null;
|
||||
|
||||
// 找到冒号后的引号
|
||||
int colon = json.indexOf(':', idx + pattern.length());
|
||||
if (colon < 0) return null;
|
||||
|
||||
int quoteStart = json.indexOf('"', colon + 1);
|
||||
if (quoteStart < 0) return null;
|
||||
|
||||
int quoteEnd = json.indexOf('"', quoteStart + 1);
|
||||
if (quoteEnd < 0) return null;
|
||||
|
||||
return json.substring(quoteStart + 1, quoteEnd);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌套JSON字段解析
|
||||
* 从 {"outer":{"inner":"value"}} 中提取 value
|
||||
*/
|
||||
private static String parseNestedJsonField(String json, String outerField, String innerField) {
|
||||
try {
|
||||
// 找到外层对象
|
||||
String outerPattern = "\"" + outerField + "\"";
|
||||
int outerIdx = json.indexOf(outerPattern);
|
||||
if (outerIdx < 0) return null;
|
||||
|
||||
// 找到外层对象的开始大括号
|
||||
int braceStart = json.indexOf('{', outerIdx);
|
||||
if (braceStart < 0) return null;
|
||||
|
||||
// 从外层对象内部解析内层字段
|
||||
int depth = 1;
|
||||
int i = braceStart + 1;
|
||||
while (i < json.length() && depth > 0) {
|
||||
char c = json.charAt(i);
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}') depth--;
|
||||
i++;
|
||||
}
|
||||
String innerJson = json.substring(braceStart, i);
|
||||
return parseJsonField(innerJson, innerField);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存(可选)
|
||||
*/
|
||||
public static void clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前使用的翻译源信息(调试用)
|
||||
*/
|
||||
public static String getSourceInfo() {
|
||||
StringBuilder sb = new StringBuilder("翻译源优先级: ");
|
||||
for (int i = 0; i < SOURCES.size(); i++) {
|
||||
TranslationSource src = SOURCES.get(i);
|
||||
sb.append(i + 1).append(".").append(src.name);
|
||||
if (i < SOURCES.size() - 1) sb.append(" > ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
public class MenuIconManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final NamespacedKey iconKey;
|
||||
|
||||
public MenuIconManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.iconKey = new NamespacedKey(
|
||||
plugin, "custom_menu_icon");
|
||||
}
|
||||
|
||||
// ========== GUIManager 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* Base64 → ItemStack
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().deserializeItem(...)
|
||||
*/
|
||||
public ItemStack deserializeItem(String b64) {
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
try {
|
||||
byte[] data =
|
||||
Base64.getDecoder().decode(b64);
|
||||
ByteArrayInputStream bais =
|
||||
new ByteArrayInputStream(data);
|
||||
BukkitObjectInputStream is =
|
||||
new BukkitObjectInputStream(bais);
|
||||
Object obj = is.readObject();
|
||||
is.close();
|
||||
if (obj instanceof ItemStack)
|
||||
return (ItemStack) obj;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemStack → Base64
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().serializeItem(...)
|
||||
*/
|
||||
public String serializeItem(ItemStack item) {
|
||||
if (item == null) return null;
|
||||
try {
|
||||
ByteArrayOutputStream baos =
|
||||
new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream os =
|
||||
new BukkitObjectOutputStream(baos);
|
||||
os.writeObject(item);
|
||||
os.close();
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(
|
||||
baos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给物品打上菜单图标标记
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().ensureMenuIconTag(...)
|
||||
*/
|
||||
public void ensureMenuIconTag(ItemStack item) {
|
||||
if (item == null) return;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return;
|
||||
meta.getPersistentDataContainer().set(
|
||||
iconKey,
|
||||
PersistentDataType.STRING,
|
||||
"menu");
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标Key
|
||||
* 对应 GUIManager 中的
|
||||
* plugin.getMenuIconMgr().getIconKey()
|
||||
*/
|
||||
public NamespacedKey getIconKey() {
|
||||
return iconKey;
|
||||
}
|
||||
|
||||
// ========== LoginManager / Main 调用的方法 ==========
|
||||
|
||||
/**
|
||||
* 获取玩家自定义图标物品
|
||||
* 有返回物品,无返回null
|
||||
*/
|
||||
public ItemStack getIcon(Player player) {
|
||||
String b64 = plugin.getDb()
|
||||
.getMenuIcon(player.getName());
|
||||
if (b64 == null || b64.isEmpty())
|
||||
return null;
|
||||
ItemStack item = deserializeItem(b64);
|
||||
if (item == null) return null;
|
||||
ensureMenuIconTag(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图标到DB
|
||||
*/
|
||||
public void saveIcon(Player player,
|
||||
ItemStack item) {
|
||||
String b64 = serializeItem(item);
|
||||
if (b64 != null) {
|
||||
plugin.getDb().saveMenuIcon(
|
||||
player.getName(), b64,
|
||||
item.getType().name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图标
|
||||
*/
|
||||
public void removeIcon(Player player) {
|
||||
plugin.getDb().deleteMenuIcon(
|
||||
player.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查物品是否是自定义菜单图标
|
||||
*/
|
||||
public boolean isCustomMenuIcon(
|
||||
ItemStack item) {
|
||||
if (item == null) return false;
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) return false;
|
||||
return meta.getPersistentDataContainer()
|
||||
.has(iconKey,
|
||||
PersistentDataType.STRING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.*;
|
||||
import org.bukkit.event.inventory.*;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class MenuManager implements Listener {
|
||||
|
||||
private final Main plugin;
|
||||
private final List<MenuItem> items =
|
||||
new ArrayList<>();
|
||||
|
||||
private final Set<String> chatWait =
|
||||
ConcurrentHashMap.newKeySet();
|
||||
private final Map<String, String> chatField =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> chatIdx =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
// ===== 菜单项 =====
|
||||
public static class MenuItem {
|
||||
public String title = "新菜单项";
|
||||
public String command = "";
|
||||
public String permType = "玩家";
|
||||
public Integer slot = null;
|
||||
public ItemStack icon;
|
||||
public MenuItem() {
|
||||
icon = randomIcon();
|
||||
}
|
||||
|
||||
public MenuItem(String t,
|
||||
String c, String p) {
|
||||
title = t;
|
||||
command = c;
|
||||
permType = p;
|
||||
icon = randomIcon();
|
||||
}
|
||||
|
||||
static ItemStack randomIcon() {
|
||||
Material[] pool = {
|
||||
Material.COMPASS,
|
||||
Material.ENDER_PEARL,
|
||||
Material.DIAMOND,
|
||||
Material.GOLD_INGOT,
|
||||
Material.EMERALD,
|
||||
Material.NETHER_STAR,
|
||||
Material.BEACON,
|
||||
Material.ENCHANTING_TABLE,
|
||||
Material.CHEST,
|
||||
Material.FIREWORK_ROCKET,
|
||||
Material.BOOK,
|
||||
Material.ELYTRA,
|
||||
Material.TOTEM_OF_UNDYING
|
||||
};
|
||||
Material m = pool[
|
||||
(int) (Math.random()
|
||||
* pool.length)];
|
||||
ItemStack it =
|
||||
new ItemStack(m);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
im.setDisplayName("§e默认图标");
|
||||
it.setItemMeta(im);
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
public MenuManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public List<MenuItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ===== 读取菜单.txt(主菜单唯一来源) =====
|
||||
public void loadMenu() {
|
||||
items.clear();
|
||||
|
||||
// 优先菜单.txt,备选menu.txt
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"菜单.txt");
|
||||
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()
|
||||
+ " 个菜单项 (源: "
|
||||
+ f.getName() + ")");
|
||||
}
|
||||
|
||||
private void parseFile(File f) {
|
||||
plugin.getLogger().info("[Menu] ===== 扫描 =====");
|
||||
plugin.getLogger().info("[Menu] 路径: "
|
||||
+ f.getAbsolutePath());
|
||||
plugin.getLogger().info("[Menu] 大小: "
|
||||
+ f.length());
|
||||
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(
|
||||
f.toPath(), StandardCharsets.UTF_8);
|
||||
plugin.getLogger().info("[Menu] 总行数: "
|
||||
+ lines.size());
|
||||
|
||||
boolean inBlock = false;
|
||||
boolean inHtml = false;
|
||||
StringBuilder buf = new StringBuilder();
|
||||
boolean inItem = false;
|
||||
int parsed = 0;
|
||||
|
||||
for (int lineNum = 1;
|
||||
lineNum <= lines.size();
|
||||
lineNum++) {
|
||||
String raw = lines.get(lineNum - 1);
|
||||
String l = raw.trim();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": 原文[" + l.length()
|
||||
+ "字符] " + l);
|
||||
|
||||
// ===== 第1优先:跨行块注释 =====
|
||||
if (inBlock) {
|
||||
if (l.contains("*/")) {
|
||||
inBlock = false;
|
||||
l = l.substring(
|
||||
l.indexOf("*/") + 2)
|
||||
.trim();
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": 块注释结束");
|
||||
} else {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": [块注释中]");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 第2优先:跨行HTML注释 =====
|
||||
if (inHtml) {
|
||||
if (l.contains("-->")) {
|
||||
inHtml = false;
|
||||
l = l.substring(
|
||||
l.indexOf("-->") + 3)
|
||||
.trim();
|
||||
} else {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": [HTML注释中]");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 第3优先:整行注释(最高裁决) =====
|
||||
// 以 # 或 // 开头的行,直接跳过
|
||||
// 不管后面包含什么 /* <!-- 等
|
||||
if (l.startsWith("#")
|
||||
|| l.startsWith("//")) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": [整行注释,跳过]");
|
||||
continue;
|
||||
}
|
||||
|
||||
// ===== 第4优先:块注释开始 =====
|
||||
if (l.contains("/*")) {
|
||||
l = l.substring(0, l.indexOf("/*"))
|
||||
.trim();
|
||||
inBlock = true;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": 进入/*块注释"
|
||||
+ " 剩余[" + l + "]");
|
||||
if (l.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 第5优先:HTML注释开始 =====
|
||||
if (l.contains("<!--")) {
|
||||
l = l.substring(0,
|
||||
l.indexOf("<!--")).trim();
|
||||
inHtml = true;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": 进入<!--注释");
|
||||
if (l.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 第6优先:行内注释 =====
|
||||
int hashPos = l.indexOf("#");
|
||||
int slashPos = l.indexOf("//");
|
||||
if (hashPos >= 0) {
|
||||
l = l.substring(0, hashPos).trim();
|
||||
}
|
||||
if (slashPos >= 0) {
|
||||
if (hashPos < 0
|
||||
|| slashPos < hashPos) {
|
||||
l = l.substring(0, slashPos)
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (l.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": 空");
|
||||
continue;
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] L" + lineNum
|
||||
+ ": [保留] " + l);
|
||||
|
||||
if (l.equals("{")) {
|
||||
inItem = true;
|
||||
buf.setLength(0);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] === 块开始 ===");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (l.equals("}")) {
|
||||
if (inItem) {
|
||||
plugin.getLogger().info(
|
||||
"[Menu] === 块结束 ===");
|
||||
parseBlock(buf.toString());
|
||||
parsed++;
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 当前菜单项: "
|
||||
+ items.size());
|
||||
}
|
||||
inItem = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inItem) {
|
||||
buf.append(l).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Menu] ===== 完毕 =====");
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 块数: " + parsed);
|
||||
plugin.getLogger().info(
|
||||
"[Menu] 菜单项: "
|
||||
+ items.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 异常: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void parseBlock(String block) {
|
||||
String title = "";
|
||||
String cmd = "";
|
||||
String perm = "玩家";
|
||||
String iconMat = "";
|
||||
int slotNum = -1;
|
||||
|
||||
for (String line : block.split("\n")) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
String v = "";
|
||||
int idx = line.indexOf(":");
|
||||
if (idx < 0) idx = line.indexOf(":");
|
||||
if (idx >= 0) {
|
||||
v = line.substring(idx + 1).trim();
|
||||
if (v.startsWith("\"")
|
||||
&& v.endsWith("\"")
|
||||
&& v.length() >= 2) {
|
||||
v = v.substring(1, v.length() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (line.startsWith("标题")) {
|
||||
title = v;
|
||||
} else if (line.startsWith("指令")) {
|
||||
cmd = v;
|
||||
if (!cmd.isEmpty()
|
||||
&& !cmd.startsWith("/")) {
|
||||
cmd = "/" + cmd;
|
||||
}
|
||||
} else if (line.startsWith("权限")) {
|
||||
perm = v;
|
||||
} else if (line.startsWith("图标")) {
|
||||
iconMat = v;
|
||||
} else if (line.startsWith("格子")) {
|
||||
try {
|
||||
int s = Integer.parseInt(v);
|
||||
if (s >= 1 && s <= 54) {
|
||||
slotNum = s;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (title.isEmpty()) return;
|
||||
|
||||
MenuItem mi = new MenuItem(title, cmd, perm);
|
||||
if (slotNum >= 1 && slotNum <= 54) {
|
||||
mi.slot = slotNum;
|
||||
}
|
||||
if (!iconMat.isEmpty()) {
|
||||
try {
|
||||
String matName = iconMat
|
||||
.replace("minecraft:", "")
|
||||
.replace("bukkit:", "")
|
||||
.trim().toUpperCase();
|
||||
Material m =
|
||||
Material.valueOf(matName);
|
||||
mi.icon = new ItemStack(m);
|
||||
ItemMeta im =
|
||||
mi.icon.getItemMeta();
|
||||
im.setDisplayName(
|
||||
"§e" + title);
|
||||
mi.icon.setItemMeta(im);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 无法解析: "
|
||||
+ iconMat);
|
||||
}
|
||||
}
|
||||
items.add(mi);
|
||||
}
|
||||
|
||||
|
||||
private void createDefault(File f) {
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
pw.println("# 服务器菜单");
|
||||
pw.println("# {}定义菜单项");
|
||||
pw.println("# 支持"
|
||||
+ " # // /* */ <!-- -->");
|
||||
pw.println();
|
||||
pw.println("{");
|
||||
pw.println(" 标题:主城传送");
|
||||
pw.println(" 指令:/spawn");
|
||||
pw.println(" 权限类型:玩家");
|
||||
pw.println(" 图标:COMPASS");
|
||||
pw.println("}");
|
||||
pw.println();
|
||||
pw.println("{");
|
||||
pw.println(" 标题:随机传送");
|
||||
pw.println(" 指令:/rtp");
|
||||
pw.println(" 权限类型:玩家");
|
||||
pw.println(" 图标:ENDER_PEARL");
|
||||
pw.println("}");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 保存 =====
|
||||
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),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.println("# 服务器菜单");
|
||||
pw.println();
|
||||
for (MenuItem mi : items) {
|
||||
pw.println("{");
|
||||
pw.println(" 标题:"
|
||||
+ mi.title);
|
||||
pw.println(" 指令:"
|
||||
+ mi.command);
|
||||
pw.println(" 权限类型:"
|
||||
+ mi.permType);
|
||||
// ★ 保存实际材质名 ★
|
||||
pw.println(" 图标:"
|
||||
+ mi.icon.getType().name());
|
||||
pw.println("}");
|
||||
pw.println();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Menu] 保存失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 聊天编辑 =====
|
||||
public boolean isEditing(String name) {
|
||||
return chatWait.contains(name);
|
||||
}
|
||||
|
||||
public void editTitle(
|
||||
Player p, int idx) {
|
||||
chatIdx.put(p.getName(), idx);
|
||||
chatWait.add(p.getName());
|
||||
chatField.put(p.getName(), "title");
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e请输入菜单标题:");
|
||||
}
|
||||
|
||||
public void editCommand(
|
||||
Player p, int idx) {
|
||||
chatIdx.put(p.getName(), idx);
|
||||
chatWait.add(p.getName());
|
||||
chatField.put(p.getName(), "command");
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e请输入指令"
|
||||
+ "(自动补全/):");
|
||||
}
|
||||
|
||||
public void onChat(Player p, String input) {
|
||||
String name = p.getName();
|
||||
if (!chatWait.remove(name)) return;
|
||||
|
||||
String field = chatField.remove(name);
|
||||
Integer idx = chatIdx.get(name);
|
||||
if (idx == null || idx >= items.size()) return;
|
||||
|
||||
MenuItem mi = items.get(idx);
|
||||
|
||||
if ("title".equals(field)) {
|
||||
mi.title = input;
|
||||
p.sendMessage("§a标题: " + input);
|
||||
} else if ("command".equals(field)) {
|
||||
if (!input.startsWith("/"))
|
||||
input = "/" + input;
|
||||
mi.command = input;
|
||||
p.sendMessage("§a指令: " + input);
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
plugin.gui.openEditor(p, idx));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,77 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordUtils {
|
||||
|
||||
private static final SecureRandom RANDOM =
|
||||
new SecureRandom();
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hash(String password, String salt) {
|
||||
try {
|
||||
MessageDigest md =
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
md.update(Base64.getDecoder().decode(salt));
|
||||
byte[] hash =
|
||||
md.digest(password.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(String password) {
|
||||
if (password == null) return false;
|
||||
if (password.length() < 6 || password.length() > 20)
|
||||
return false;
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
}
|
||||
return hasUpper && hasLower && hasDigit;
|
||||
}
|
||||
|
||||
public static String generateTempPassword() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateInviteCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateVerifyCode() {
|
||||
int code = 100000 + RANDOM.nextInt(900000);
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordUtils {
|
||||
|
||||
private static final SecureRandom RANDOM =
|
||||
new SecureRandom();
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hash(String password, String salt) {
|
||||
try {
|
||||
MessageDigest md =
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
// 清理非 Base64 字符,防止数据库中转义字符导致解析错误
|
||||
String cleanSalt = salt.replaceAll("[^A-Za-z0-9+/=]", "");
|
||||
if (cleanSalt.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid salt: " + salt);
|
||||
}
|
||||
md.update(Base64.getDecoder().decode(cleanSalt));
|
||||
byte[] hash =
|
||||
md.digest(password.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(String password) {
|
||||
if (password == null) return false;
|
||||
if (password.length() < 6 || password.length() > 20)
|
||||
return false;
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
}
|
||||
return hasUpper && hasLower && hasDigit;
|
||||
}
|
||||
|
||||
public static String generateTempPassword() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateInviteCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateVerifyCode() {
|
||||
int code = 100000 + RANDOM.nextInt(900000);
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class PlayerStorage {
|
||||
private Connection conn;
|
||||
private final Logger logger;
|
||||
|
||||
public PlayerStorage(File dataFolder,
|
||||
Logger logger) {
|
||||
this.logger = logger;
|
||||
File dbFile = new File(
|
||||
dataFolder,
|
||||
"quest_data.db");
|
||||
try {
|
||||
Class.forName(
|
||||
"org.sqlite.JDBC");
|
||||
conn = DriverManager
|
||||
.getConnection(
|
||||
"jdbc:sqlite:"
|
||||
+ dbFile
|
||||
.getAbsolutePath());
|
||||
conn.setAutoCommit(true);
|
||||
Statement s =
|
||||
conn.createStatement();
|
||||
s.executeUpdate(
|
||||
"CREATE TABLE IF NOT EXISTS "
|
||||
+ "quest_kv ("
|
||||
+ "player TEXT NOT NULL,"
|
||||
+ "key TEXT NOT NULL,"
|
||||
+ "value TEXT,"
|
||||
+ "PRIMARY KEY"
|
||||
+ "(player, key))");
|
||||
s.close();
|
||||
logger.info(
|
||||
"[QuestStorage] 初始化: "
|
||||
+ dbFile.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
logger.severe(
|
||||
"[QuestStorage] 失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Object getCondition(
|
||||
String player, String key) {
|
||||
if (conn == null) return null;
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
conn.prepareStatement(
|
||||
"SELECT value "
|
||||
+ "FROM quest_kv "
|
||||
+ "WHERE player=? "
|
||||
+ "AND key=?");
|
||||
ps.setString(1,
|
||||
player.toLowerCase());
|
||||
ps.setString(2, key);
|
||||
ResultSet rs =
|
||||
ps.executeQuery();
|
||||
String val = rs.next()
|
||||
? rs.getString("value")
|
||||
: null;
|
||||
rs.close();
|
||||
ps.close();
|
||||
return parseValue(val);
|
||||
} catch (SQLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCondition(
|
||||
String player, String key) {
|
||||
setConditionRaw(
|
||||
player, key, "true");
|
||||
}
|
||||
public void debugPlayer(String player) {
|
||||
|
||||
}
|
||||
public void setConditionRaw(
|
||||
String player, String key,
|
||||
String value) {
|
||||
if (conn == null) return;
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
conn.prepareStatement(
|
||||
"INSERT OR REPLACE "
|
||||
+ "INTO quest_kv"
|
||||
+ "(player,key,value)"
|
||||
+ " VALUES(?,?,?)");
|
||||
ps.setString(1,
|
||||
player.toLowerCase());
|
||||
ps.setString(2, key);
|
||||
ps.setString(3, value);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
logger.warning(
|
||||
"[QuestStorage] 写入失败: "
|
||||
+ key + "=" + value
|
||||
+ " | " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (conn != null
|
||||
&& !conn.isClosed())
|
||||
conn.close();
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
|
||||
private Object parseValue(String v) {
|
||||
if (v == null) return null;
|
||||
try {
|
||||
return Long.parseLong(v);
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
return Double.parseDouble(v);
|
||||
} catch (Exception e2) {
|
||||
if ("true".equalsIgnoreCase(v))
|
||||
return true;
|
||||
if ("false".equalsIgnoreCase(v))
|
||||
return false;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,374 +1,375 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
public class PointsManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Object cyInstance = null;
|
||||
private Method cyActivationMethod = null;
|
||||
private Method cyGetShopItemsMethod = null;
|
||||
|
||||
private final List<Map<String, Object>> cyItems =
|
||||
new ArrayList<>();
|
||||
|
||||
public PointsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
discoverCY();
|
||||
}
|
||||
|
||||
private void discoverCY() {
|
||||
try {
|
||||
org.bukkit.plugin.Plugin p = Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (p != null && p.isEnabled()) {
|
||||
cyInstance = p;
|
||||
cyGetShopItemsMethod = p.getClass()
|
||||
.getMethod("onSdf1GetShopItems");
|
||||
cyActivationMethod = p.getClass()
|
||||
.getMethod("onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] CY_beibao 联控已连接");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshCYItems() {
|
||||
cyItems.clear();
|
||||
if (cyInstance == null
|
||||
|| cyGetShopItemsMethod == null) return;
|
||||
try {
|
||||
Object result =
|
||||
cyGetShopItemsMethod.invoke(cyInstance);
|
||||
if (result == null) return;
|
||||
String data = result.toString();
|
||||
if (data.isEmpty()) return;
|
||||
String[] items = data.split(";");
|
||||
for (String itemStr : items) {
|
||||
String[] p = itemStr.split("\\|");
|
||||
if (p.length < 8) continue;
|
||||
Map<String, Object> item =
|
||||
new LinkedHashMap<>();
|
||||
item.put("id", p[0]);
|
||||
item.put("name", p[1]);
|
||||
item.put("days",
|
||||
Integer.parseInt(p[2]));
|
||||
item.put("slots",
|
||||
Integer.parseInt(p[3]));
|
||||
item.put("price",
|
||||
Integer.parseInt(p[4]));
|
||||
item.put("stock",
|
||||
Integer.parseInt(p[5]));
|
||||
item.put("icon", p[6]);
|
||||
item.put("lifetime",
|
||||
"1".equals(p[7]));
|
||||
cyItems.add(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 拉取CY商品失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void activateCY(String name,
|
||||
int slots, int days) {
|
||||
try {
|
||||
if (cyInstance == null
|
||||
|| cyActivationMethod == null) return;
|
||||
cyActivationMethod.invoke(
|
||||
cyInstance, name, slots, days);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] CY激活失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public int getPoints(String name) {
|
||||
return ((Number) plugin.getDb()
|
||||
.getField(name, "points")).intValue();
|
||||
}
|
||||
|
||||
public Inventory createShopGUI(Player p) {
|
||||
int points = getPoints(p.getName());
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§d§l积分商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
inv.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a游戏币充值",
|
||||
"§7消耗积分兑换游戏币",
|
||||
"§7100积分 = $1000",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(12, mkItem(Material.ENDER_CHEST,
|
||||
"§b背包空间",
|
||||
"§7消耗积分兑换背包格子",
|
||||
"§750积分 = 9格永久空间",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
|
||||
"§e经验瓶",
|
||||
"§7消耗积分兑换经验",
|
||||
"§720积分 = 10级经验",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
if (cyInstance != null) {
|
||||
inv.setItem(31, mkItem(Material.CHEST,
|
||||
"§6背包商城",
|
||||
"§7来自CY_beibao的商品",
|
||||
"",
|
||||
"§e点击查看"));
|
||||
}
|
||||
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public Inventory createCYShopGUI(Player p) {
|
||||
refreshCYItems();
|
||||
int points = getPoints(p.getName());
|
||||
int size = Math.max(54,
|
||||
((cyItems.size() / 9) + 1) * 9);
|
||||
if (size > 54) size = 54;
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§lCY背包商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
for (int i = 0; i < cyItems.size()
|
||||
&& i < 45; i++) {
|
||||
Map<String, Object> item = cyItems.get(i);
|
||||
String name = (String) item.get("name");
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + price + " 积分");
|
||||
if (lifetime) {
|
||||
lore.add("§7格子: §e终身 +" + slots);
|
||||
} else {
|
||||
lore.add("§7格子: §e+" + slots);
|
||||
lore.add("§7时间: §e" + days + "天");
|
||||
}
|
||||
if (stock == 0) {
|
||||
lore.add("§7库存: §a无限");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§7库存: §c已下架");
|
||||
} else {
|
||||
lore.add("§7库存: §e" + stock);
|
||||
}
|
||||
lore.add("");
|
||||
if (points >= price && stock != -1) {
|
||||
lore.add("§e点击购买");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§c已下架");
|
||||
} else {
|
||||
lore.add("§c积分不足");
|
||||
}
|
||||
inv.setItem(i, mkItem(Material.CHEST,
|
||||
"§a" + name,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
inv.setItem(size - 1,
|
||||
mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public void addItem(String name,
|
||||
int price, String cmd2) {
|
||||
// 追加到商品配置文件
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.io.FileWriter fw =
|
||||
new java.io.FileWriter(file, true);
|
||||
fw.write(name + ":" + price
|
||||
+ ":" + cmd2 + "\n");
|
||||
fw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int index) {
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.util.List<String> lines =
|
||||
new java.util.ArrayList<>();
|
||||
java.io.BufferedReader br =
|
||||
new java.io.BufferedReader(
|
||||
new java.io.FileReader(
|
||||
file));
|
||||
String line;
|
||||
int i = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.trim().isEmpty()
|
||||
|| line.startsWith("#")) {
|
||||
lines.add(line);
|
||||
continue;
|
||||
}
|
||||
if (i != index) {
|
||||
lines.add(line);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
br.close();
|
||||
java.io.PrintWriter pw =
|
||||
new java.io.PrintWriter(file);
|
||||
for (String l : lines) pw.println(l);
|
||||
pw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void handleClick(Player p, int slot) {
|
||||
int points = getPoints(p.getName());
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if (points >= 50) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 50);
|
||||
activateCY(p.getName(), 9, 0);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "50"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
if (points >= 20) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 20);
|
||||
p.setLevel(p.getLevel() + 10);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "20"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 31:
|
||||
if (cyInstance != null) {
|
||||
p.openInventory(
|
||||
createCYShopGUI(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCYClick(Player p, int slot) {
|
||||
if (slot < 0 || slot >= cyItems.size()
|
||||
|| slot >= 45) return;
|
||||
Map<String, Object> item = cyItems.get(slot);
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
if (stock == -1) {
|
||||
p.sendMessage("§c该商品已下架");
|
||||
return;
|
||||
}
|
||||
int points = getPoints(p.getName());
|
||||
if (points < price) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getDb().deductPoints(p.getName(), price);
|
||||
int actDays = lifetime ? 0 : days;
|
||||
activateCY(p.getName(), slots, actDays);
|
||||
|
||||
String tl = lifetime
|
||||
? "终身" : (days + "天");
|
||||
p.sendMessage("§a购买成功!获得 "
|
||||
+ tl + " +" + slots + "格 背包空间");
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
public class PointsManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Object cyInstance = null;
|
||||
private Method cyActivationMethod = null;
|
||||
private Method cyGetShopItemsMethod = null;
|
||||
|
||||
private final List<Map<String, Object>> cyItems =
|
||||
new ArrayList<>();
|
||||
|
||||
public PointsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
discoverCY();
|
||||
}
|
||||
|
||||
private void discoverCY() {
|
||||
try {
|
||||
org.bukkit.plugin.Plugin p = Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (p != null && p.isEnabled()) {
|
||||
cyInstance = p;
|
||||
cyGetShopItemsMethod = p.getClass()
|
||||
.getMethod("onSdf1GetShopItems");
|
||||
cyActivationMethod = p.getClass()
|
||||
.getMethod("onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] CY_beibao 联控已连接");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshCYItems() {
|
||||
cyItems.clear();
|
||||
if (cyInstance == null
|
||||
|| cyGetShopItemsMethod == null) return;
|
||||
try {
|
||||
Object result =
|
||||
cyGetShopItemsMethod.invoke(cyInstance);
|
||||
if (result == null) return;
|
||||
String data = result.toString();
|
||||
if (data.isEmpty()) return;
|
||||
String[] items = data.split(";");
|
||||
for (String itemStr : items) {
|
||||
String[] p = itemStr.split("\\|");
|
||||
if (p.length < 8) continue;
|
||||
Map<String, Object> item =
|
||||
new LinkedHashMap<>();
|
||||
item.put("id", p[0]);
|
||||
item.put("name", p[1]);
|
||||
item.put("days",
|
||||
Integer.parseInt(p[2]));
|
||||
item.put("slots",
|
||||
Integer.parseInt(p[3]));
|
||||
item.put("price",
|
||||
Integer.parseInt(p[4]));
|
||||
item.put("stock",
|
||||
Integer.parseInt(p[5]));
|
||||
item.put("icon", p[6]);
|
||||
item.put("lifetime",
|
||||
"1".equals(p[7]));
|
||||
cyItems.add(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 拉取CY商品失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void activateCY(String name,
|
||||
int slots, int days) {
|
||||
try {
|
||||
if (cyInstance == null
|
||||
|| cyActivationMethod == null) return;
|
||||
cyActivationMethod.invoke(
|
||||
cyInstance, name, slots, days);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] CY激活失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public int getPoints(String name) {
|
||||
return ((Number) plugin.getDb()
|
||||
.getField(name, "points")).intValue();
|
||||
}
|
||||
|
||||
public Inventory createShopGUI(Player p) {
|
||||
int points = getPoints(p.getName());
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§d§l积分商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
inv.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a游戏币充值",
|
||||
"§7消耗积分兑换游戏币",
|
||||
"§7100积分 = $1000",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(12, mkItem(Material.ENDER_CHEST,
|
||||
"§b背包空间",
|
||||
"§7消耗积分兑换背包格子",
|
||||
"§750积分 = 9格永久空间",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
|
||||
"§e经验瓶",
|
||||
"§7消耗积分兑换经验",
|
||||
"§720积分 = 10级经验",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
if (cyInstance != null) {
|
||||
inv.setItem(31, mkItem(Material.CHEST,
|
||||
"§6背包商城",
|
||||
"§7来自CY_beibao的商品",
|
||||
"",
|
||||
"§e点击查看"));
|
||||
}
|
||||
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public Inventory createCYShopGUI(Player p) {
|
||||
refreshCYItems();
|
||||
int points = getPoints(p.getName());
|
||||
int size = Math.max(54,
|
||||
((cyItems.size() / 9) + 1) * 9);
|
||||
if (size > 54) size = 54;
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§lCY背包商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
for (int i = 0; i < cyItems.size()
|
||||
&& i < 45; i++) {
|
||||
Map<String, Object> item = cyItems.get(i);
|
||||
String name = (String) item.get("name");
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + price + " 积分");
|
||||
if (lifetime) {
|
||||
lore.add("§7格子: §e终身 +" + slots);
|
||||
} else {
|
||||
lore.add("§7格子: §e+" + slots);
|
||||
lore.add("§7时间: §e" + days + "天");
|
||||
}
|
||||
if (stock == 0) {
|
||||
lore.add("§7库存: §a无限");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§7库存: §c已下架");
|
||||
} else {
|
||||
lore.add("§7库存: §e" + stock);
|
||||
}
|
||||
lore.add("");
|
||||
if (points >= price && stock != -1) {
|
||||
lore.add("§e点击购买");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§c已下架");
|
||||
} else {
|
||||
lore.add("§c积分不足");
|
||||
}
|
||||
inv.setItem(i, mkItem(Material.CHEST,
|
||||
"§a" + name,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
inv.setItem(size - 1,
|
||||
mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
public void addItem(String name,
|
||||
int price, String cmd2) {
|
||||
// 追加到商品配置文件
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.io.FileWriter fw =
|
||||
new java.io.FileWriter(file, true);
|
||||
fw.write(name + ":" + price
|
||||
+ ":" + cmd2 + "\n");
|
||||
fw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int index) {
|
||||
File file = new File(
|
||||
plugin.getDataFolder(),
|
||||
"ShopItems.txt");
|
||||
try {
|
||||
java.util.List<String> lines =
|
||||
new java.util.ArrayList<>();
|
||||
java.io.BufferedReader br =
|
||||
new java.io.BufferedReader(
|
||||
new java.io.FileReader(
|
||||
file));
|
||||
String line;
|
||||
int i = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.trim().isEmpty()
|
||||
|| line.startsWith("#")) {
|
||||
lines.add(line);
|
||||
continue;
|
||||
}
|
||||
if (i != index) {
|
||||
lines.add(line);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
br.close();
|
||||
java.io.PrintWriter pw =
|
||||
new java.io.PrintWriter(file);
|
||||
for (String l : lines) pw.println(l);
|
||||
pw.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void handleClick(Player p, int slot) {
|
||||
int points = getPoints(p.getName());
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
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债券系统不可用");
|
||||
}
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2().msg("points_insufficient")
|
||||
.replace("{points}", String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if (points >= 50) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 50);
|
||||
activateCY(p.getName(), 9, 0);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "50"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
if (points >= 20) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 20);
|
||||
p.setLevel(p.getLevel() + 10);
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_purchase_success")
|
||||
.replace("{count}", "20"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 31:
|
||||
if (cyInstance != null) {
|
||||
p.openInventory(
|
||||
createCYShopGUI(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleCYClick(Player p, int slot) {
|
||||
if (slot < 0 || slot >= cyItems.size()
|
||||
|| slot >= 45) return;
|
||||
Map<String, Object> item = cyItems.get(slot);
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
if (stock == -1) {
|
||||
p.sendMessage("§c该商品已下架");
|
||||
return;
|
||||
}
|
||||
int points = getPoints(p.getName());
|
||||
if (points < price) {
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("points_insufficient")
|
||||
.replace("{points}",
|
||||
String.valueOf(points)));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getDb().deductPoints(p.getName(), price);
|
||||
int actDays = lifetime ? 0 : days;
|
||||
activateCY(p.getName(), slots, actDays);
|
||||
|
||||
String tl = lifetime
|
||||
? "终身" : (days + "天");
|
||||
p.sendMessage("§a购买成功!获得 "
|
||||
+ tl + " +" + slots + "格 背包空间");
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public class QuestStorage {
|
||||
|
||||
private volatile Connection conn;
|
||||
private final String dbPath;
|
||||
private final Plugin plugin;
|
||||
private final Object connLock = new Object();
|
||||
|
||||
public QuestStorage(File dataFolder, Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
String path = "";
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
dataFolder, "quest_data.db");
|
||||
path = dbFile.getPath();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
dbPath = path;
|
||||
|
||||
conn = openConnection();
|
||||
if (conn != null) {
|
||||
initTable();
|
||||
}
|
||||
}
|
||||
|
||||
private Connection openConnection() {
|
||||
String url = "jdbc:sqlite:" + dbPath
|
||||
+ "?journal_mode=WAL"
|
||||
+ "&synchronous=NORMAL"
|
||||
+ "&busy_timeout=5000";
|
||||
try {
|
||||
return DriverManager.getConnection(url);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量级连接检查 — 仅检查本地状态,不执行SQL
|
||||
* 避免 isValid() 在主线程上阻塞
|
||||
*/
|
||||
private void ensureConnection() {
|
||||
try {
|
||||
if (conn != null && !conn.isClosed()) {
|
||||
return;
|
||||
}
|
||||
} catch (SQLException ignored) {}
|
||||
synchronized (connLock) {
|
||||
try { if (conn != null) conn.close(); }
|
||||
catch (Exception ignored) {}
|
||||
conn = openConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private void initTable() {
|
||||
try {
|
||||
conn.createStatement().executeUpdate(
|
||||
"CREATE TABLE IF NOT EXISTS quest_conditions ("
|
||||
+ "player TEXT NOT NULL, "
|
||||
+ "condition_key TEXT NOT NULL, "
|
||||
+ "value TEXT DEFAULT '1', "
|
||||
+ "timestamp INTEGER, "
|
||||
+ "PRIMARY KEY (player, condition_key))");
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCondition(String player, String condition) {
|
||||
setConditionRaw(player, condition, "1");
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步写入 — 不阻塞主线程
|
||||
* 使用 Bukkit 异步调度,在独立线程中执行 SQLite 写操作
|
||||
*/
|
||||
public void setConditionRaw(String player,
|
||||
String condition, String value) {
|
||||
final String sql =
|
||||
"INSERT OR REPLACE INTO quest_conditions "
|
||||
+ "(player, condition_key, value, timestamp) "
|
||||
+ "VALUES (?, ?, ?, ?)";
|
||||
final long ts = System.currentTimeMillis();
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
writeSync(sql, player, condition, value, ts);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在异步线程中执行写入,带重试
|
||||
*/
|
||||
private void writeSync(String sql, String player,
|
||||
String condKey, String value, long timestamp) {
|
||||
synchronized (connLock) {
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
ensureConnection();
|
||||
if (conn == null) return;
|
||||
PreparedStatement ps = conn.prepareStatement(sql);
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, condKey);
|
||||
ps.setString(3, value);
|
||||
ps.setLong(4, timestamp);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
return;
|
||||
} catch (SQLException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && (msg.contains("SQLITE_BUSY")
|
||||
|| msg.contains("database is locked"))) {
|
||||
try {
|
||||
Thread.sleep(200 + attempt * 200);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步读取(主线程)
|
||||
* WAL 模式下读操作不阻塞写,可安全在主线程执行
|
||||
*/
|
||||
public Object getCondition(String player,
|
||||
String condition) {
|
||||
synchronized (connLock) {
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
ensureConnection();
|
||||
if (conn == null) return null;
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT value FROM quest_conditions "
|
||||
+ "WHERE player = ? "
|
||||
+ "AND condition_key = ?");
|
||||
ps.setString(1, player);
|
||||
ps.setString(2, condition);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
String val = rs.getString("value");
|
||||
rs.close();
|
||||
ps.close();
|
||||
try {
|
||||
if (val.contains("."))
|
||||
return Double.parseDouble(val);
|
||||
return Long.parseLong(val);
|
||||
} catch (NumberFormatException e) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
return null;
|
||||
} catch (SQLException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && (msg.contains("SQLITE_BUSY")
|
||||
|| msg.contains("database is locked"))) {
|
||||
try {
|
||||
Thread.sleep(100 + attempt * 100);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
synchronized (connLock) {
|
||||
try {
|
||||
if (conn != null && !conn.isClosed()) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player
|
||||
.PlayerResourcePackStatusEvent;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent
|
||||
.ConcurrentHashMap;
|
||||
|
||||
public class RadioDownloadListener
|
||||
implements Listener {
|
||||
|
||||
private final Main plugin;
|
||||
private final java.util.logging.Logger log;
|
||||
|
||||
private final ConcurrentHashMap<UUID, Integer>
|
||||
failCount = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<UUID, Boolean>
|
||||
blocked = new ConcurrentHashMap<>();
|
||||
|
||||
private static final int MAX_FAILED = 3;
|
||||
|
||||
public RadioDownloadListener(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.log = plugin.getLogger();
|
||||
}
|
||||
|
||||
public void onPlayerQuit(UUID uuid) {
|
||||
failCount.remove(uuid);
|
||||
blocked.remove(uuid);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onStatus(
|
||||
PlayerResourcePackStatusEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
UUID uuid = p.getUniqueId();
|
||||
String status =
|
||||
e.getStatus().name();
|
||||
|
||||
log.info("[Radio] === 资源包状态 ===");
|
||||
log.info("[Radio] 玩家: "
|
||||
+ p.getName());
|
||||
log.info("[Radio] UUID: " + uuid);
|
||||
log.info("[Radio] 状态: "
|
||||
+ status);
|
||||
log.info("[Radio] 原始状态枚举: "
|
||||
+ e.getStatus().getClass()
|
||||
.getSimpleName()
|
||||
+ "."
|
||||
+ e.getStatus().name());
|
||||
|
||||
if (blocked.containsKey(uuid)) {
|
||||
log.info("[Radio] 已熔断,跳过: "
|
||||
+ p.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case "SUCCESS":
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✓ 资源包加载成功");
|
||||
failCount.remove(uuid);
|
||||
blocked.remove(uuid);
|
||||
break;
|
||||
|
||||
case "ACCEPTED":
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 客户端已接受,"
|
||||
+ "等待下载...");
|
||||
break;
|
||||
|
||||
case "DECLINED":
|
||||
log.warning("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✗ 拒绝了资源包");
|
||||
/* log.warning("[Radio] "
|
||||
+ "可能原因: "
|
||||
+ "1.客户端设置禁用资源包"
|
||||
+ " 2.已缓存拒绝记录"
|
||||
+ " 3.SSL证书不匹配");*/
|
||||
handleFailure(p, uuid);
|
||||
break;
|
||||
|
||||
case "FAILED_DOWNLOAD":
|
||||
log.warning("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✗ 下载失败");
|
||||
/* log.warning("[Radio] "
|
||||
+ "可能原因: "
|
||||
+ "1.URL无法访问"
|
||||
+ " 2.SSL证书错误"
|
||||
+ " 3.网络超时"
|
||||
+ " 4.文件不存在(404)");*/
|
||||
handleFailure(p, uuid);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 未知状态: "
|
||||
+ status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFailure(Player p, UUID uuid) {
|
||||
int failNum = failCount.getOrDefault(uuid, 0) + 1;
|
||||
failCount.put(uuid, failNum);
|
||||
|
||||
log.warning("[Radio] " + p.getName()
|
||||
+ " 失败次数: " + failNum + "/" + MAX_FAILED);
|
||||
|
||||
if (failNum >= MAX_FAILED) {
|
||||
blocked.put(uuid, true);
|
||||
log.severe("[Radio] " + p.getName()
|
||||
+ " 已达最大失败次数,停止重试,"
|
||||
+ "玩家继续留在服务器");
|
||||
// 不踢出,只记录熔断,玩家正常游玩
|
||||
p.sendMessage("§e§l[Radio] 资源包加载失败"
|
||||
+ ",已取消下载资源包,部分画面音频可能出现问题"
|
||||
+ "\n§d§l若想继续下载,请退出重进。若持续失败,请上报管理员");
|
||||
|
||||
return;
|
||||
}
|
||||
// 打了包没测试
|
||||
p.sendMessage("§e§l[Radio] 下载失败,"
|
||||
+ "重试... (" + failNum + "/" + MAX_FAILED + ")");
|
||||
|
||||
// 5秒后重试
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin, () -> {
|
||||
if (p.isOnline() && plugin.radio != null) {
|
||||
plugin.radio.sendResourcePack(p);
|
||||
}
|
||||
}, 100L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RefundRecord {
|
||||
public final UUID playerUUID;
|
||||
public final String playerName;
|
||||
public final String itemId;
|
||||
public final String itemName;
|
||||
public final Material material;
|
||||
public final Material shulkerMaterial;
|
||||
public int amount;
|
||||
public int totalPaid;
|
||||
public final long timestamp;
|
||||
|
||||
// 普通购买
|
||||
public RefundRecord(UUID uuid, String name,
|
||||
String itemId, String itemName,
|
||||
Material material, int amount,
|
||||
int totalPaid, long timestamp) {
|
||||
this.playerUUID = uuid;
|
||||
this.playerName = name;
|
||||
this.itemId = itemId;
|
||||
this.itemName = itemName;
|
||||
this.material = material;
|
||||
this.shulkerMaterial = null;
|
||||
this.amount = amount;
|
||||
this.totalPaid = totalPaid;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// 盒装购买
|
||||
public RefundRecord(UUID uuid, String name,
|
||||
String itemId, String itemName,
|
||||
Material material,
|
||||
Material shulkerMat,
|
||||
int amount, int totalPaid,
|
||||
long timestamp) {
|
||||
this.playerUUID = uuid;
|
||||
this.playerName = name;
|
||||
this.itemId = itemId;
|
||||
this.itemName = itemName;
|
||||
this.material = material;
|
||||
this.shulkerMaterial = shulkerMat;
|
||||
this.amount = amount;
|
||||
this.totalPaid = totalPaid;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis()
|
||||
- timestamp > 300000L;
|
||||
}
|
||||
|
||||
public int getSecondsLeft() {
|
||||
long left = 300000L
|
||||
- (System.currentTimeMillis()
|
||||
- timestamp);
|
||||
return (int) Math.max(0, left / 1000);
|
||||
}
|
||||
}
|
||||
@@ -1,248 +1,282 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
||||
*/
|
||||
public class RiskControlManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 每个玩家名 -> 连续失败次数
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个IP -> 关联的可疑玩家列表
|
||||
private final Map<String, Set<String>> ipSuspicious =
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定玩家的失败计数(登录成功时调用)
|
||||
*/
|
||||
public void resetFailAttempts(String 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后重置风控状态
|
||||
*/
|
||||
public void onLoginSuccess(String playerName) {
|
||||
resetFailAttempts(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的冷却记录(可由定时任务调用)
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
verifyCooldown.entrySet().removeIf(
|
||||
e -> now - e.getValue() > COOLDOWN_MS * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家的失败尝试记录(调试用)
|
||||
*/
|
||||
public Map<String, Integer> getAllFailAttempts() {
|
||||
return Collections.unmodifiableMap(failAttempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有风控数据(管理员调试用)
|
||||
*/
|
||||
public void clearAll() {
|
||||
failAttempts.clear();
|
||||
ipSuspicious.clear();
|
||||
verifyCooldown.clear();
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 每个玩家名 -> 连续失败次数
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个玩家名 -> 封禁到期时间戳(毫秒)
|
||||
private final Map<String, Long> banUntil =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public RiskControlManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前密码错误阈值(从配置读取)
|
||||
*/
|
||||
private int getMaxFailAttempts() {
|
||||
return plugin.getConfig2().passwordFailThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封禁时长(毫秒)(从配置读取)
|
||||
*/
|
||||
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);
|
||||
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) {
|
||||
failAttempts.remove(playerName);
|
||||
banUntil.remove(playerName);
|
||||
}
|
||||
|
||||
// ==================== 清理 ====================
|
||||
|
||||
/**
|
||||
* 清理过期的封禁记录(可由定时任务调用)
|
||||
* 同步解除过期玩家的Bukkit封禁
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
// 收集过期的玩家名
|
||||
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();
|
||||
banUntil.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class SalesStatsManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
private final Map<String, Integer> sessionSales =
|
||||
new ConcurrentHashMap<>();
|
||||
private int sessionRevenue = 0;
|
||||
private int sessionOrders = 0;
|
||||
private int sessionItems = 0;
|
||||
private int sessionRefunds = 0;
|
||||
private int sessionRefundAmount = 0;
|
||||
private long sessionStart = System.currentTimeMillis();
|
||||
|
||||
// 状态
|
||||
private boolean waitingForEmpty = false;
|
||||
private boolean hasReported = false;
|
||||
private BukkitRunnable waitTask = null;
|
||||
|
||||
public SalesStatsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
startMonitor();
|
||||
}
|
||||
|
||||
// ★ 每10秒检测一次
|
||||
private void startMonitor() {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int online = Bukkit.getOnlinePlayers().size();
|
||||
|
||||
if (online > 0) {
|
||||
// 有玩家在线 → 重置等待状态
|
||||
if (waitingForEmpty) {
|
||||
// 刚有人进来了,清除已汇报标记
|
||||
// 下次没人时会重新汇报
|
||||
}
|
||||
waitingForEmpty = false;
|
||||
hasReported = false;
|
||||
cancelWaitTask();
|
||||
} else if (!waitingForEmpty && !hasReported) {
|
||||
// 没人了,且还没汇报过 → 开始1分钟倒计时
|
||||
waitingForEmpty = true;
|
||||
startWaitCountdown();
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 200L, 200L);
|
||||
}
|
||||
|
||||
// ★ 1分钟等待
|
||||
private void startWaitCountdown() {
|
||||
cancelWaitTask();
|
||||
waitTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int online = Bukkit.getOnlinePlayers().size();
|
||||
|
||||
if (online > 0) {
|
||||
// 等待期间有人进来 → 取消,等下次没人
|
||||
waitingForEmpty = false;
|
||||
hasReported = false;
|
||||
waitTask = null;
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1分钟没人 → 汇报一次
|
||||
if (!hasReported) {
|
||||
hasReported = true;
|
||||
report();
|
||||
}
|
||||
waitingForEmpty = false;
|
||||
waitTask = null;
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
waitTask.runTaskLater(plugin, 1200L); // 60秒
|
||||
}
|
||||
|
||||
private void cancelWaitTask() {
|
||||
if (waitTask != null) {
|
||||
waitTask.cancel();
|
||||
waitTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 记录 =====
|
||||
|
||||
public void recordSale(String itemName, int price) {
|
||||
sessionSales.merge(itemName, 1, Integer::sum);
|
||||
sessionRevenue += price;
|
||||
sessionOrders++;
|
||||
sessionItems++;
|
||||
}
|
||||
|
||||
public void recordRefund(int amount) {
|
||||
sessionRefunds++;
|
||||
sessionRefundAmount += amount;
|
||||
}
|
||||
|
||||
// ===== 汇报 =====
|
||||
|
||||
public void report() {
|
||||
if (sessionItems == 0 && sessionRefunds == 0) {
|
||||
// plugin.getLogger().info("[Sales] 无数据,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
long minutes =
|
||||
(System.currentTimeMillis() - sessionStart)
|
||||
/ 60000;
|
||||
if (minutes < 1) minutes = 1;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("§6§l========== 销量汇总 ==========");
|
||||
sb.append("\n§7时长: §f").append(minutes).append("分钟");
|
||||
sb.append("\n§7订单: §f").append(sessionOrders).append("笔");
|
||||
sb.append("\n§7商品: §f").append(sessionItems).append("件");
|
||||
sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚");
|
||||
sb.append("\n§7退款: §c").append(sessionRefunds)
|
||||
.append("次(§c").append(sessionRefundAmount)
|
||||
.append("枚§7)");
|
||||
sb.append("\n§6§l------------------------------");
|
||||
|
||||
if (!sessionSales.isEmpty()) {
|
||||
sb.append("\n§7销量排行:");
|
||||
List<Map.Entry<String, Integer>> sorted =
|
||||
new ArrayList<>(sessionSales.entrySet());
|
||||
sorted.sort((a, b) ->
|
||||
b.getValue() - a.getValue());
|
||||
int rank = 1;
|
||||
for (Map.Entry<String, Integer> e : sorted) {
|
||||
if (rank > 10) break;
|
||||
sb.append("\n§e").append(rank).append(". ")
|
||||
.append(e.getKey())
|
||||
.append(" §7x").append(e.getValue());
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
sb.append("\n§6§l==============================");
|
||||
|
||||
String msg = sb.toString();
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.isOp()) {
|
||||
for (String line : msg.split("\n"))
|
||||
p.sendMessage(line);
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[Sales] 汇报完成");
|
||||
resetSession();
|
||||
}
|
||||
|
||||
public void resetSession() {
|
||||
sessionSales.clear();
|
||||
sessionRevenue = 0;
|
||||
sessionOrders = 0;
|
||||
sessionItems = 0;
|
||||
sessionRefunds = 0;
|
||||
sessionRefundAmount = 0;
|
||||
sessionStart = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ShopCategory {
|
||||
private final String name;
|
||||
private final String fileName;
|
||||
private final List<ShopItem> items;
|
||||
|
||||
public ShopCategory(String name, String fileName) {
|
||||
this.name = name;
|
||||
this.fileName = fileName;
|
||||
this.items = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getFileName() { return fileName; }
|
||||
public List<ShopItem> getItems() { return items; }
|
||||
|
||||
public ShopItem getItem(String id) {
|
||||
for (ShopItem item : items) {
|
||||
if (item.getId().equalsIgnoreCase(id))
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addItem(ShopItem item) {
|
||||
items.add(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
public class ShopItem {
|
||||
private final String id;
|
||||
private final String displayName;
|
||||
private final Material material;
|
||||
private int buyPrice;
|
||||
private int sellPrice;
|
||||
private int stock;
|
||||
private int hourlySales;
|
||||
private int totalSales;
|
||||
public ShopItem(String id, String displayName,
|
||||
Material material, int buyPrice,
|
||||
int sellPrice, int stock,
|
||||
int hourlySales, int totalSales) {
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
this.material = material;
|
||||
this.buyPrice = buyPrice;
|
||||
this.sellPrice = sellPrice;
|
||||
this.stock = stock;
|
||||
this.hourlySales = hourlySales;
|
||||
this.totalSales = totalSales;
|
||||
}
|
||||
|
||||
public int getEffectiveSellPrice() {
|
||||
if (sellPrice <= 0) return 0;
|
||||
return sellPrice;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public Material getMaterial() { return material; }
|
||||
public int getBuyPrice() { return buyPrice; }
|
||||
public int getSellPrice() { return sellPrice; }
|
||||
public int getStock() { return stock; }
|
||||
public int getHourlySales() { return hourlySales; }
|
||||
public int getTotalSales() { return totalSales; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ public class TicketManager {
|
||||
"§7类型: §f" + type,
|
||||
"§7状态: " + stxt(status),
|
||||
"§7提交者: §f" + requester,
|
||||
"§7奖励: §e" + reward + " 积分"));
|
||||
"§7奖励: §e" + reward + " 债券"));
|
||||
|
||||
g.setItem(12, mk(Material.BOOK,
|
||||
"§7详细描述",
|
||||
@@ -242,6 +242,13 @@ public class TicketManager {
|
||||
"§a标记完结",
|
||||
"§7确认工单处理完成"));
|
||||
}
|
||||
// ★ 服务商上报完结后:管理员确认完结
|
||||
if ("completed".equals(status)) {
|
||||
g.setItem(19, mk(Material.LIME_DYE,
|
||||
"§a确认完结",
|
||||
"§7确认服务商处理完成",
|
||||
"§7输入评分1-5"));
|
||||
}
|
||||
// 驳回(非已关闭/已完结状态)
|
||||
if (!"closed".equals(status)
|
||||
&& !"resolved".equals(status)
|
||||
@@ -410,7 +417,7 @@ public class TicketManager {
|
||||
"§a§l[工单] §f创建成功!"
|
||||
+ " #§e" + id
|
||||
+ " 奖励: §e"
|
||||
+ price + "积分");
|
||||
+ price + "债券");
|
||||
}
|
||||
notifyAdmins(p.getName(), id);
|
||||
} else {
|
||||
@@ -1017,23 +1024,18 @@ public class TicketManager {
|
||||
// 管理员:标记完结(已回复状态)
|
||||
if (adm(p) && "replied".equals(st)
|
||||
&& asg.equals("admin")) {
|
||||
|
||||
// 管理员:标记完结(已回复状态)
|
||||
if (adm(p) && "replied".equals(st)
|
||||
&& asg.equals("admin")) {
|
||||
Map<String, Object> tk =
|
||||
plugin.getDb().getTicket(id);
|
||||
String tkType = sv(tk, "type");
|
||||
if (TYPE_DELETE.equals(tkType)) {
|
||||
approveDeleteTicket(p, id);
|
||||
return true;
|
||||
}
|
||||
plugin.getDb().completeTicket(id);
|
||||
p.sendMessage("§a工单 #" + id
|
||||
+ " 已标记完结");
|
||||
openDetail(p, id);
|
||||
Map<String, Object> tk =
|
||||
plugin.getDb().getTicket(id);
|
||||
String tkType = sv(tk, "type");
|
||||
if (TYPE_DELETE.equals(tkType)) {
|
||||
approveDeleteTicket(p, id);
|
||||
return true;
|
||||
}
|
||||
plugin.getDb().completeTicket(id);
|
||||
p.sendMessage("§a工单 #" + id
|
||||
+ " 已标记完结");
|
||||
openDetail(p, id);
|
||||
return true;
|
||||
}
|
||||
// 服务商:抢单
|
||||
if (plugin.getDb()
|
||||
@@ -1044,6 +1046,19 @@ public class TicketManager {
|
||||
openDetail(p, id);
|
||||
return true;
|
||||
}
|
||||
// ★ 管理员:确认服务商完结(输入评分)
|
||||
if (adm(p) && "completed".equals(st)) {
|
||||
p.closeInventory();
|
||||
plugin.getChatInput()
|
||||
.getState(p).type =
|
||||
ChatInputManager.InputType
|
||||
.TICKET_SCORE;
|
||||
plugin.getChatInput()
|
||||
.getState(p).ticketId = id;
|
||||
p.sendMessage(
|
||||
"§e输入评分1-5:");
|
||||
return true;
|
||||
}
|
||||
// 服务商:标记完结
|
||||
if (plugin.getDb()
|
||||
.isServiceProvider(p.getName())
|
||||
@@ -1263,44 +1278,34 @@ public class TicketManager {
|
||||
int score = nv(t, "score");
|
||||
ConfigManager cfg =
|
||||
plugin.getConfig2();
|
||||
|
||||
// ★ 提交者获得工单奖励(债券)
|
||||
if (reward > 0) {
|
||||
plugin.getDb().addPoints(
|
||||
requester, reward);
|
||||
plugin.getBonds().addBonds(
|
||||
requester, reward,
|
||||
BondManager.TX_TICKET_REWARD, "",
|
||||
"System", "工单 #" + tid + " 奖励");
|
||||
Player rp = Bukkit.getPlayer(
|
||||
requester);
|
||||
if (rp != null && rp.isOnline()) {
|
||||
rp.sendMessage(
|
||||
"§e§l[工单] §f获得 "
|
||||
+ reward
|
||||
+ " 积分奖励");
|
||||
+ " 债券奖励");
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 服务商获得债券奖励
|
||||
if (!assigned.isEmpty()) {
|
||||
int pts = (int) (score
|
||||
int bondReward = (int) (score
|
||||
* cfg.providerPointsPerScore);
|
||||
if (pts > 0) {
|
||||
plugin.getDb().addPoints(
|
||||
assigned, pts);
|
||||
}
|
||||
double money =
|
||||
cfg.baseEconomyReward
|
||||
+ score
|
||||
* cfg.providerEconomyPerScore;
|
||||
boolean ok = false;
|
||||
try {
|
||||
if (plugin.getEconomy() != null) {
|
||||
OfflinePlayer op =
|
||||
Bukkit.getOfflinePlayer(
|
||||
assigned);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(
|
||||
op, money);
|
||||
ok = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 经济失败: "
|
||||
+ e.getMessage());
|
||||
if (bondReward > 0) {
|
||||
plugin.getBonds().addBonds(
|
||||
assigned, bondReward,
|
||||
BondManager.TX_TICKET_SERVICE, "",
|
||||
"System", "工单 #" + tid
|
||||
+ " 服务奖励(评分"
|
||||
+ score + ")");
|
||||
}
|
||||
Player pp =
|
||||
Bukkit.getPlayer(assigned);
|
||||
@@ -1309,13 +1314,9 @@ public class TicketManager {
|
||||
"§a§l[工单] §f工单 #"
|
||||
+ tid
|
||||
+ " 确认完结");
|
||||
if (pts > 0)
|
||||
pp.sendMessage("§7积分: §e"
|
||||
+ pts);
|
||||
if (ok)
|
||||
pp.sendMessage("§7经济: §a$"
|
||||
+ String.format(
|
||||
"%.0f", money));
|
||||
if (bondReward > 0)
|
||||
pp.sendMessage("§7债券: §e"
|
||||
+ bondReward + " 枚");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1410,7 +1411,7 @@ public class TicketManager {
|
||||
plugin.getDb().updateTicketField(tid,
|
||||
"admin_confirmed", 1);
|
||||
Player target =
|
||||
Bukkit.getPlayer(requester);
|
||||
Bukkit.getPlayerExact(requester);
|
||||
if (target != null && target.isOnline()) {
|
||||
plugin.backupInventory(target);
|
||||
target.sendMessage(
|
||||
@@ -1430,10 +1431,17 @@ public class TicketManager {
|
||||
final String name = requester;
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
plugin.getDb().deleteUser(name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 删号工单已执行");
|
||||
int deleted =
|
||||
plugin.getDb().deleteUser(name);
|
||||
if (deleted > 0) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 删号工单已执行");
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 删号工单未执行(精准匹配下账号不存在,未误删)");
|
||||
}
|
||||
}, 100L);
|
||||
}
|
||||
|
||||
@@ -1559,7 +1567,7 @@ public class TicketManager {
|
||||
lore.add("§7提交者: §f"
|
||||
+ requester);
|
||||
lore.add("§7奖励: §e" + reward
|
||||
+ " 积分");
|
||||
+ " 债券");
|
||||
lore.add("");
|
||||
lore.add("§7点击查看详情");
|
||||
im.setLore(lore);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Sdf1_game寻宝系统桥接类
|
||||
* 用于Sdf1_login调用Sdf1_game的clearPlayerAllClaims方法
|
||||
*/
|
||||
public class TreasureBridge {
|
||||
|
||||
private Object treasureManager;
|
||||
private Method clearPlayerAllClaimsMethod;
|
||||
private boolean hooked = false;
|
||||
|
||||
/**
|
||||
* 桥接Sdf1_game插件
|
||||
* @return 是否桥接成功
|
||||
*/
|
||||
public boolean hook() {
|
||||
Plugin sdf1_game = Bukkit.getPluginManager()
|
||||
.getPlugin("Sdf1_game");
|
||||
|
||||
if (sdf1_game == null || !sdf1_game.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取Sdf1_game的Main实例
|
||||
Object mainInstance = sdf1_game;
|
||||
|
||||
// 获取TreasureManager实例
|
||||
treasureManager = findTreasureManager(mainInstance);
|
||||
if (treasureManager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取clearPlayerAllClaims方法
|
||||
clearPlayerAllClaimsMethod = treasureManager.getClass()
|
||||
.getMethod("clearPlayerAllClaims", String.class);
|
||||
|
||||
hooked = true;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定玩家的所有领取记录
|
||||
* @param playerName 玩家名
|
||||
* @return 清除的记录数量,失败返回-1
|
||||
*/
|
||||
public int clearPlayerAllClaims(String playerName) {
|
||||
if (!isHooked()) return -1;
|
||||
try {
|
||||
int result = (int) clearPlayerAllClaimsMethod.invoke(
|
||||
treasureManager, playerName);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找TreasureManager实例
|
||||
* @param mainInstance Sdf1_game的Main实例
|
||||
* @return TreasureManager实例
|
||||
*/
|
||||
private Object findTreasureManager(Object mainInstance) {
|
||||
Class<?> c = mainInstance.getClass();
|
||||
|
||||
// 策略1: getTreasureManager() 方法
|
||||
try {
|
||||
Method m = c.getMethod("getTreasureManager");
|
||||
Object r = m.invoke(mainInstance);
|
||||
if (r != null) return r;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 策略2: treasureManager 字段
|
||||
try {
|
||||
Field f = c.getDeclaredField("treasureManager");
|
||||
f.setAccessible(true);
|
||||
Object r = f.get(mainInstance);
|
||||
if (r != null) return r;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 策略3: 遍历字段按类型名查找
|
||||
try {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (f.getType().getSimpleName()
|
||||
.equals("TreasureManager")) {
|
||||
f.setAccessible(true);
|
||||
Object r = f.get(mainInstance);
|
||||
if (r != null) return r;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已桥接
|
||||
* @return 桥接状态
|
||||
*/
|
||||
public boolean isHooked() {
|
||||
return hooked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class UpdateChecker {
|
||||
|
||||
// ===== SSL证书信任所有(解决PKIX错误) =====
|
||||
static {
|
||||
try {
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||
}
|
||||
};
|
||||
SSLContext sc = SSLContext.getInstance("TLS");
|
||||
sc.init(null, trustAllCerts, new SecureRandom());
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final String currentVersion;
|
||||
|
||||
// ===== 仓库信息 =====
|
||||
private static final String GITHUB_OWNER = "ypsdf1";
|
||||
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 =
|
||||
"https://caoyuan.ypshidifu.cn/update_check.html";
|
||||
|
||||
// 仓库页面链接
|
||||
private static final String GITHUB_LINK =
|
||||
"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;
|
||||
|
||||
// 熔断阈值
|
||||
private static final int TRIP_THRESHOLD = 5;
|
||||
|
||||
// 熔断后重试间隔(2分钟)
|
||||
private static final long RETRY_DELAY_MS = 2 * 60 * 1000;
|
||||
|
||||
// ===== 熔断状态(内存态,重启自动清除) =====
|
||||
private volatile boolean circuitTripped = false;
|
||||
private volatile int failCount = 0;
|
||||
private volatile boolean manualSuccess = false;
|
||||
|
||||
// ===== 自动检测禁用(持久化到文件) =====
|
||||
private volatile boolean autoCheckDisabled = false;
|
||||
private final File disabledFile;
|
||||
|
||||
public UpdateChecker(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.currentVersion = plugin.getDescription().getVersion();
|
||||
this.disabledFile = new File(plugin.getDataFolder(), "update_disabled.dat");
|
||||
loadDisabledState();
|
||||
}
|
||||
|
||||
// ========== 持久化禁用状态 ==========
|
||||
|
||||
private void loadDisabledState() {
|
||||
if (disabledFile.exists()) {
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(disabledFile))) {
|
||||
String line = br.readLine();
|
||||
if (line != null) {
|
||||
autoCheckDisabled = "true".equals(line.trim());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDisabledState() {
|
||||
try (PrintWriter pw = new PrintWriter(new FileWriter(disabledFile))) {
|
||||
pw.println(autoCheckDisabled ? "true" : "false");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 公开方法 ==========
|
||||
|
||||
/**
|
||||
* onEnable 调用(异步静默)
|
||||
* 熔断或禁用则直接跳过
|
||||
*/
|
||||
public void checkOnEnable() {
|
||||
if (autoCheckDisabled) return;
|
||||
if (circuitTripped) {
|
||||
// 熔断状态下,2分钟后重试
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* /sdf1_login update 调用(异步)
|
||||
* 忽略熔断和禁用状态,强制检测
|
||||
*/
|
||||
public void checkUpdate(CommandSender sender) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(sender, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2分钟后重试
|
||||
*/
|
||||
private void scheduleRetry() {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (circuitTripped && !manualSuccess) {
|
||||
plugin.getLogger().info("[Sdf1_login] 熔断重试中...");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true));
|
||||
}
|
||||
}, RETRY_DELAY_MS / 50); // ticks
|
||||
}
|
||||
|
||||
// ========== 核心检测逻辑 ==========
|
||||
private void doCheck(CommandSender sender, boolean isAuto) {
|
||||
|
||||
if (isAuto) {
|
||||
if (autoCheckDisabled || circuitTripped) {
|
||||
return;
|
||||
}
|
||||
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(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);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
ghThread.setDaemon(true);
|
||||
|
||||
// 通道2:Gitee
|
||||
Thread geThread = new Thread(() -> {
|
||||
if (!cancelled.get()) {
|
||||
geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK));
|
||||
if (geResult.get().isSuccess()) {
|
||||
cancelled.set(true);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
geThread.setDaemon(true);
|
||||
|
||||
gypThread.start();
|
||||
ghThread.start();
|
||||
geThread.start();
|
||||
|
||||
try {
|
||||
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 (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] git.ypshidifu.cn/GitHub/Gitee 均失败,尝试第三路备选 BaoTa...");
|
||||
CheckResult fbResult = tryFallbackChannel();
|
||||
if (fbResult != null && fbResult.isSuccess()) {
|
||||
chosen = fbResult;
|
||||
plugin.getLogger().info("[Sdf1_login] 第三路备选 BaoTa 检查成功: " + fbResult.version);
|
||||
} 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) {
|
||||
if (sb.length() > 0) sb.append("\n");
|
||||
sb.append("§7Gitee: §c").append(ge.error);
|
||||
}
|
||||
if (fbResult != null && fbResult.error != null) {
|
||||
if (sb.length() > 0) sb.append("\n");
|
||||
sb.append("§7BaoTa: §c").append(fbResult.error);
|
||||
}
|
||||
if (failCount >= TRIP_THRESHOLD) {
|
||||
circuitTripped = true;
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 更新检测已熔断,连续失败" + failCount + "次");
|
||||
scheduleRetry();
|
||||
}
|
||||
if (sender != null) {
|
||||
final String msg = sb.toString();
|
||||
final boolean tripped = circuitTripped;
|
||||
final int fc = failCount;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
sender.sendMessage("§c§l[更新] 检测失败");
|
||||
sender.sendMessage(msg);
|
||||
sender.sendMessage("§e失败次数: " + fc + "/" + TRIP_THRESHOLD);
|
||||
if (tripped) {
|
||||
sender.sendMessage("§c已熔断,2分钟后自动重试,或重启服务器重置");
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 至少一个通道成功
|
||||
failCount = 0;
|
||||
|
||||
// 检查禁用信号
|
||||
if (isDisableSignal(chosen.version)) {
|
||||
autoCheckDisabled = true;
|
||||
saveDisabledState();
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 远程版本信号 \"" + chosen.version + "\",自动更新检测已禁用");
|
||||
if (sender != null) {
|
||||
Bukkit.getScheduler().runTask(plugin, () ->
|
||||
sender.sendMessage("§e[更新] 远程已禁用自动更新检测"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动调用:成功才解除熔断和禁用
|
||||
if (!isAuto) {
|
||||
manualSuccess = true; // 标记手动成功,不再执行后续自动检查
|
||||
if (circuitTripped) {
|
||||
circuitTripped = false;
|
||||
}
|
||||
if (autoCheckDisabled) {
|
||||
autoCheckDisabled = false;
|
||||
saveDisabledState();
|
||||
}
|
||||
}
|
||||
|
||||
final String src = chosen.source;
|
||||
final String lnk = chosen.link;
|
||||
final String rv = chosen.version;
|
||||
|
||||
// ★ 自动检查也记录日志(含下载链接 + 颜色提醒)
|
||||
if (isAuto) {
|
||||
boolean hasUpdate = isNewer(rv, currentVersion);
|
||||
String status = hasUpdate
|
||||
? "(有新版本!)"
|
||||
: "(已是最新)";
|
||||
// 使用Bukkit.getConsoleSender().sendMessage()输出彩色日志
|
||||
Bukkit.getConsoleSender().sendMessage("§a[Sdf1_login] §e更新检测成功 via: " + src
|
||||
+ " | 本地: " + currentVersion + " | 远程: " + rv + " §7" + status);
|
||||
if (hasUpdate) {
|
||||
Bukkit.getConsoleSender().sendMessage("§a[Sdf1_login] §b下载: " + lnk);
|
||||
}
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (sender == null) return;
|
||||
|
||||
sender.sendMessage("§e[Sdf1_login] [更新] 检测成功 via: §a" + src);
|
||||
sender.sendMessage("§e[Sdf1_login] [更新] 本地版本: §f" + currentVersion);
|
||||
sender.sendMessage("§e[Sdf1_login] [更新] 远程版本: §f" + rv);
|
||||
|
||||
if (isNewer(rv, currentVersion)) {
|
||||
sender.sendMessage("§c[Sdf1_login] [更新] 发现新版本: " + rv);
|
||||
sender.sendMessage("§a[Sdf1_login] 请前往仓库下载更新:");
|
||||
sender.sendMessage("§a§l" + lnk);
|
||||
} else {
|
||||
sender.sendMessage("§a[Sdf1_login] [更新] 已是最新版本");
|
||||
}
|
||||
|
||||
// 显示其他失败通道的错误信息
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 禁用信号判断 ==========
|
||||
|
||||
private boolean isDisableSignal(String version) {
|
||||
if (version == null) return false;
|
||||
String v = version.trim().toLowerCase();
|
||||
return "999".equals(v) || "关闭".equals(v) || "false".equals(v) || "禁用".equals(v);
|
||||
}
|
||||
|
||||
// ========== HTTP 请求 ==========
|
||||
|
||||
/**
|
||||
* 第三路备选:从宝塔静态HTML解析版本信息
|
||||
* HTML中用 <!--UPDATE_DATA 包裹结构化数据:
|
||||
* pluginName|version|downloadLink
|
||||
*/
|
||||
private CheckResult tryFallbackChannel() {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(FALLBACK_HTML);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "Sdf1-Plugin-UpdateChecker/1.0");
|
||||
conn.setConnectTimeout(TIMEOUT_MS);
|
||||
conn.setReadTimeout(TIMEOUT_MS);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
if (code != 200) {
|
||||
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||
"BaoTa: HTTP " + code);
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
|
||||
// 解析 <!--UPDATE_DATA ... UPDATE_DATA--> 块
|
||||
String html = sb.toString();
|
||||
|
||||
// ★ 去除 Cloudflare 注入的 CDN-CGI 链接(<a href=...>...</a>)
|
||||
html = html.replaceAll("<a\\s[^>]*>[^<]*</a>", "");
|
||||
|
||||
int start = html.indexOf("<!--UPDATE_DATA");
|
||||
int end = html.indexOf("UPDATE_DATA-->");
|
||||
if (start < 0 || end < 0 || end <= start) {
|
||||
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||
"BaoTa: 无法解析更新数据块");
|
||||
}
|
||||
String block = html.substring(start, end);
|
||||
|
||||
// 逐行查找匹配当前插件名的记录
|
||||
String[] lines = block.split("\n");
|
||||
for (String l : lines) {
|
||||
l = l.trim();
|
||||
// 跳过空行、注释标记行、HTML注入标签
|
||||
if (l.isEmpty() || l.startsWith("<!--") || l.startsWith("<")) continue;
|
||||
if (l.startsWith("Sdf1_login|")) {
|
||||
// 格式: Sdf1_login|1.0|https://...
|
||||
String[] parts = l.split("\\|");
|
||||
if (parts.length >= 2) {
|
||||
String ver = parts[1].trim();
|
||||
String link = parts.length >= 3 ? parts[2].trim() : GITHUB_LINK;
|
||||
return new CheckResult(ver, "BaoTa", link, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||
"BaoTa: 未找到 Sdf1_login 的版本信息");
|
||||
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
return new CheckResult(null, "BaoTa", FALLBACK_HTML, "BaoTa: 连接超时");
|
||||
} catch (Exception e) {
|
||||
return new CheckResult(null, "BaoTa", FALLBACK_HTML,
|
||||
"BaoTa: " + e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static class CheckResult {
|
||||
final String version;
|
||||
final String source;
|
||||
final String link;
|
||||
final String error;
|
||||
|
||||
CheckResult(String version, String source, String link, String error) {
|
||||
this.version = version;
|
||||
this.source = source;
|
||||
this.link = link;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
return version != null;
|
||||
}
|
||||
}
|
||||
|
||||
private CheckResult tryChannel(String apiUrl, String sourceName, String link) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(apiUrl);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
|
||||
conn.setRequestProperty("Accept", "*/*");
|
||||
conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
conn.setRequestProperty("Connection", "keep-alive");
|
||||
conn.setConnectTimeout(TIMEOUT_MS);
|
||||
conn.setReadTimeout(TIMEOUT_MS);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
|
||||
if (code == 404) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)");
|
||||
}
|
||||
if (code == 403) {
|
||||
String errBody = "";
|
||||
try { BufferedReader er = new BufferedReader(new InputStreamReader(conn.getErrorStream())); StringBuilder eb = new StringBuilder(); String el; while ((el = er.readLine()) != null) eb.append(el); er.close(); errBody = eb.toString(); } catch (Exception ignored) {}
|
||||
String detail = errBody.isEmpty() ? "" : " | " + errBody.substring(0, Math.min(errBody.length(), 200));
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)" + detail);
|
||||
}
|
||||
if (code != 200) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code);
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
reader.close();
|
||||
|
||||
String tag = parseTagName(sb.toString());
|
||||
if (tag == null) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": 无法解析版本号");
|
||||
}
|
||||
|
||||
return new CheckResult(tag, sourceName, link, null);
|
||||
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": 连接超时");
|
||||
} catch (java.net.ConnectException e) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": 连接被拒绝");
|
||||
} catch (java.net.UnknownHostException e) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": DNS解析失败");
|
||||
} catch (Exception e) {
|
||||
return new CheckResult(null, sourceName, link, sourceName + ": " + e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String parseTagName(String json) {
|
||||
// tags API 返回数组: [{"name":"v1.1",...}]
|
||||
// releases API 返回对象: {"tag_name":"v1.1"}
|
||||
|
||||
int idx = json.indexOf("\"tag_name\"");
|
||||
if (idx >= 0) {
|
||||
int colon = json.indexOf(":", idx);
|
||||
int q1 = json.indexOf("\"", colon + 1);
|
||||
int q2 = json.indexOf("\"", q1 + 1);
|
||||
if (q2 > q1) {
|
||||
return cleanTag(json.substring(q1 + 1, q2));
|
||||
}
|
||||
}
|
||||
|
||||
idx = json.indexOf("\"name\"");
|
||||
if (idx >= 0) {
|
||||
int colon = json.indexOf(":", idx);
|
||||
int q1 = json.indexOf("\"", colon + 1);
|
||||
int q2 = json.indexOf("\"", q1 + 1);
|
||||
if (q2 > q1) {
|
||||
return cleanTag(json.substring(q1 + 1, q2));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String cleanTag(String tag) {
|
||||
if (tag.startsWith("v") || tag.startsWith("V")) {
|
||||
tag = tag.substring(1);
|
||||
}
|
||||
return tag.isEmpty() ? null : tag;
|
||||
}
|
||||
|
||||
// ========== 版本比较 ==========
|
||||
// ★ 只要本地≠远程,就视为有更新
|
||||
|
||||
private boolean isNewer(String remote, String local) {
|
||||
if (remote == null || local == null) return false;
|
||||
// 版本比较:远程≠本地即视为有更新(用户即将推新版本)
|
||||
return !remote.trim().equals(local.trim());
|
||||
}
|
||||
|
||||
private int parseNum(String s) {
|
||||
try {
|
||||
return Integer.parseInt(s.replaceAll("[^0-9]", ""));
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +1,102 @@
|
||||
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;
|
||||
|
||||
public class VerificationManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public VerificationManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isBedrockPlayer(Player p) {
|
||||
String uuid = p.getUniqueId().toString();
|
||||
return uuid.contains("f851ba4")
|
||||
|| uuid.startsWith("00000000-0000-0000");
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return Bukkit.getServer().getOnlineMode();
|
||||
}
|
||||
|
||||
public void verifyPremiumAsync(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
Thread t = new Thread(() -> doVerify(player, callback),
|
||||
"Sdf1_login-PremiumVerify");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void doVerify(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
String name = player.getName();
|
||||
String uuid = player.getUniqueId().toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证开始 ======");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 玩家: " + name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 当前UUID: " + uuid);
|
||||
|
||||
try {
|
||||
UUID offlineUuid = UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
String offlineStr = offlineUuid.toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 离线UUID: " + offlineStr);
|
||||
|
||||
boolean isOfflineUuid =
|
||||
player.getUniqueId().equals(offlineUuid);
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] UUID匹配离线格式: "
|
||||
+ isOfflineUuid);
|
||||
|
||||
if (!isOfflineUuid) {
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 正版玩家");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(true);
|
||||
return;
|
||||
}
|
||||
|
||||
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] 结论: 纯离线玩家");
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(false);
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 验证异常: "
|
||||
+ e.getMessage());
|
||||
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;
|
||||
}
|
||||
}
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
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;
|
||||
|
||||
public VerificationManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isBedrockPlayer(Player p) {
|
||||
String uuid = p.getUniqueId().toString();
|
||||
return uuid.contains("f851ba4")
|
||||
|| uuid.startsWith("00000000-0000-0000");
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return Bukkit.getServer().getOnlineMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步验证玩家身份
|
||||
* 注意:此方法仅用于判断UUID格式,不执行自动登录
|
||||
* 自动登录由onJoin中的检查点0/1/2控制
|
||||
*/
|
||||
public void verifyPremiumAsync(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
Thread t = new Thread(() -> doVerify(player, callback),
|
||||
"Sdf1_login-PremiumVerify");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void doVerify(Player player,
|
||||
Consumer<Boolean> callback) {
|
||||
String name = player.getName();
|
||||
String uuid = player.getUniqueId().toString();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证开始 ======");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 玩家: " + name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 当前UUID: " + uuid);
|
||||
|
||||
try {
|
||||
java.util.UUID offlineUuid = java.util.UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name)
|
||||
.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] 结论: 正版玩家(非离线UUID)");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 离线UUID → 无法通过UUID区分正版/盗版,一律视为非正版
|
||||
// 正版玩家需通过 /mslogin OAuth验证 或 /login 手动登录
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] 结论: 离线UUID玩家,需手动登录");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] (正版玩家请使用 /mslogin 或 /正版 命令验证)");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] ====== 验证结束 ======");
|
||||
callback.accept(false);
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 验证异常: "
|
||||
+ e.getMessage());
|
||||
callback.accept(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class WelcomeManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final Random random = new Random();
|
||||
private final List<String> registerMsgs =
|
||||
new ArrayList<>();
|
||||
private final List<String> loginMsgs =
|
||||
new ArrayList<>();
|
||||
|
||||
public WelcomeManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
loadMessages();
|
||||
}
|
||||
|
||||
public void loadMessages() {
|
||||
registerMsgs.clear();
|
||||
loginMsgs.clear();
|
||||
File f = new File(
|
||||
plugin.getDataFolder(),
|
||||
"欢迎仪式.txt");
|
||||
if (!f.exists()) {
|
||||
writeDefault(f);
|
||||
}
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(f),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
String section = "";
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("\uFEFF"))
|
||||
line = line.substring(1);
|
||||
String t = line.trim();
|
||||
if (t.isEmpty()
|
||||
|| t.startsWith("#"))
|
||||
continue;
|
||||
if (t.equals("注册")
|
||||
|| t.equals("注册:")) {
|
||||
section = "register";
|
||||
continue;
|
||||
}
|
||||
if (t.equals("登录")
|
||||
|| t.equals("登录:")) {
|
||||
section = "login";
|
||||
continue;
|
||||
}
|
||||
// 支持 = 和 : 两种分隔
|
||||
if (section.isEmpty()) {
|
||||
if (t.startsWith("注册")
|
||||
&& (t.contains("=")
|
||||
|| t.contains(":"))) {
|
||||
int idx = t.indexOf('=');
|
||||
int idx2 = t.indexOf(':');
|
||||
if (idx < 0) idx = idx2;
|
||||
else if (idx2 >= 0)
|
||||
idx = Math.min(idx, idx2);
|
||||
String v = t.substring(idx + 1)
|
||||
.trim();
|
||||
if (!v.isEmpty())
|
||||
registerMsgs.add(
|
||||
colorize(v));
|
||||
continue;
|
||||
}
|
||||
if (t.startsWith("登录")
|
||||
&& (t.contains("=")
|
||||
|| t.contains(":"))) {
|
||||
int idx = t.indexOf('=');
|
||||
int idx2 = t.indexOf(':');
|
||||
if (idx < 0) idx = idx2;
|
||||
else if (idx2 >= 0)
|
||||
idx = Math.min(idx, idx2);
|
||||
String v = t.substring(idx + 1)
|
||||
.trim();
|
||||
if (!v.isEmpty())
|
||||
loginMsgs.add(
|
||||
colorize(v));
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!t.isEmpty()) {
|
||||
if (section.equals("register"))
|
||||
registerMsgs.add(
|
||||
colorize(t));
|
||||
else
|
||||
loginMsgs.add(
|
||||
colorize(t));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[欢迎] 加载失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
plugin.getLogger().info(
|
||||
"[欢迎] 加载完成: 注册="
|
||||
+ registerMsgs.size()
|
||||
+ " 登录=" + loginMsgs.size());
|
||||
}
|
||||
|
||||
public void onRegister(Player p) {
|
||||
sendRandom(p, registerMsgs, "注册");
|
||||
}
|
||||
|
||||
public void onLogin(Player p) {
|
||||
sendRandom(p, loginMsgs, "登录");
|
||||
}
|
||||
|
||||
private void sendRandom(Player p,
|
||||
List<String> msgs, String tag) {
|
||||
if (msgs.isEmpty()) return;
|
||||
String msg = msgs.get(
|
||||
random.nextInt(msgs.size()));
|
||||
msg = msg.replace("{player}",
|
||||
p.getName());
|
||||
final String finalMsg = msg;
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin,
|
||||
() -> Bukkit
|
||||
.broadcastMessage(
|
||||
finalMsg), 10L);
|
||||
}
|
||||
|
||||
private String colorize(String msg) {
|
||||
return msg.replace("&", "\u00a7");
|
||||
}
|
||||
|
||||
private void writeDefault(File f) {
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
pw.println("# 欢迎仪式配置");
|
||||
pw.println("# &为颜色代码");
|
||||
pw.println("# {player} = 玩家名");
|
||||
pw.println();
|
||||
pw.println("注册:");
|
||||
pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]");
|
||||
pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]");
|
||||
pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]");
|
||||
pw.println();
|
||||
pw.println("登录:");
|
||||
pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]");
|
||||
pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]");
|
||||
pw.println("&b&l{player} &b回来了!&7[登录]");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: 1.0
|
||||
version: 2.91
|
||||
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: /删除账号 [玩家名]
|
||||
@@ -60,18 +60,12 @@ commands:
|
||||
deny:
|
||||
description: 拒绝OA请求
|
||||
usage: /deny <编号> [理由]
|
||||
sdf1debug:
|
||||
description: 调试指令
|
||||
usage: /sdf1debug <玩家> take.<阶段>
|
||||
email:
|
||||
description: 绑定邮箱
|
||||
usage: /email <邮箱>
|
||||
绑定:
|
||||
description: 绑定邮箱(中文)
|
||||
usage: /绑定 <邮箱>
|
||||
调试:
|
||||
description: 调试模式
|
||||
usage: /调试
|
||||
绑定邮箱:
|
||||
description: 绑定邮箱
|
||||
usage: /绑定邮箱
|
||||
@@ -86,4 +80,107 @@ commands:
|
||||
usage: /垃圾清理
|
||||
menu:
|
||||
description: 雪球菜单开关控制
|
||||
usage: /menu <开/关>
|
||||
usage: /menu <开/关>
|
||||
pvp :
|
||||
description: PVP榜单
|
||||
usage: /pvp
|
||||
cypay :
|
||||
description: 草原探险债券
|
||||
usage: /cypay
|
||||
printer :
|
||||
description: 打印债券流水
|
||||
usage: /printer
|
||||
shop :
|
||||
description: 游戏商人
|
||||
usage: /shop
|
||||
商店:
|
||||
description: 打开商店
|
||||
usage: /商店
|
||||
# testorder:
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
protect:
|
||||
description: 区域保护
|
||||
usage: /protect
|
||||
landrec:
|
||||
description: 领地记录系统(回声碎片发放)
|
||||
usage: /landrec give [玩家]
|
||||
传送:
|
||||
description: 传送至领地
|
||||
usage: /传送 <领地名>
|
||||
update:
|
||||
description: 检查所有插件更新(集群控制)
|
||||
usage: /update
|
||||
web:
|
||||
description: 生成Web端登录令牌
|
||||
usage: /web
|
||||
控制台:
|
||||
description: 生成Web端登录令牌(中文版)
|
||||
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
|
||||
|
||||